Pull to refresh

Сборка проекта без единой глобальной переменной

Reading time 6 min
Views 3.9K
Представьте, у вас есть проект, состоящий из нескольких модулей и, например, jQuery или любая другая библиотеки в CDN. У вас есть огромное желание не показывать пользователю ваши глобальные переменные и по возможности не показывать jQuery и $. Ну и, конечно, сделать все без изменения кода проекта.
Причины для сокрытия глобалов могут быть разные: для красоты, из соображений безопасности, для затруднения анализа кода и другие. Пользователь взаимодействует с вашим кодом, используя события, которые он не сможет сломать — больше ему ничего и не нужно.

Самый очевидный способ — создать единственный namespace в который пассивно экспортировать прочие объекты, а jQuery и $ в конце удалить.

После сборки код будет какой-то такой:
(function(window, undefined){
    // include ./js/YourNamespace.js
    var YourNamespace = (function () {
        // что-то ещё
        return {};
    }());
    // include ./js/YourNamespace/SomeObject.js
    YourNamespace.SomeObject = (function () {
        // что-то ещё
        return function () {

        };
    }());
    // Cleanup
    delete window.$;
    delete window.jQuery;
}(window));

Это идеальный вариант, но чаще бывает не так. Посмотрите ваш код, такой ли он?

Под катом универсальное решение, позволяющее собрать любой код без единой глобальной переменной.

Понятия активный/пассивный импорт/экспорт


Активный экспорт
(function (window) {
    var ModuleA = {};

    // Export
    window.ModuleA = ModuleA;
}(window));
Модуль ModuleA активно экспортируется

Пассивный экспорт
var ModuleC = (function (window) {
    var ModuleC = {};

    // Export
    return ModuleC;
}(window));
Модуль ModuleC пассивно экспортируется

Активный импорт
(function (window, $) {
    console.log($);
}(this, jQuery));
Модуль jQuery активно импортируется

Пассивный импорт
(function (window) {
    console.log(ModuleC);
}(this));
Модуль ModuleC пассивно импортируется

Сборка


Имеем несколько модулей, которые как попало импортируют/экспортируют. Мы знаем какие модули использует каждый. Мы можем обернуть каждый модуль в замыкание в которое передать список всех объектов, которые он использует. Правда тут есть небольшая проблема: если модуль А использует модуль Б, а модуль Б использует модуль А и все они подключаются последовательно, то при подключении модуля А, обернутого в замыкание будет ошибка ReferenceError — модуля Б ещё нет. Эта проблема решается пробросом объекта в замыкание А после загрузки модуля Б. Если модуль пассивно экспортирует и если мы его обернем в замыкание, то пропадет глобальная переменная. Эта проблема решается пробросом локальной переменной в глобальный контекст.
Мы знаем список глобальных переменных, которые мы создали в процессе инициализации скрипта — просто удаляем их через delete для очистки глобалов.

Пример


Разберем пример проекта, состоящий из jQuery и 3 модулей, которые как попало импортируют/экспортируют.

Листинги модулей

// Uses ModuleC, $
(function (window) {
    var ModuleA = {
        a: 'ModuleA.a',
        b: 2,
        d: function () {
            console.log(ModuleC.c === 'ModuleC.c');
            console.log(typeof $ === 'function');
        }
    };

    // Export
    window.ModuleA = ModuleA;
}(window));

// Uses ModuleA
var ModuleC = (function (window) {
    var ModuleC = {
        a: 1,
        b: 2,
        c: 'ModuleC.c',
        d: function () {
            console.log(ModuleA.a === 'ModuleA.a');
        }
    };

    // Export
    return ModuleC;
}(window));

// Uses ModuleA, ModuleC
ModuleA.c();
window.setTimeout(function () {
    ModuleC.d();
    ModuleA.d();
}, 0);

Модули подключаются в такой последовательности: jQuery отдельно, ModuleA+ModuleC+ModuleD. Модуль А зависит от модуля С, модуль С зависит от модуля А (конфликт, описанный выше). Модуль А активно экспортирует, модуль С пассивно экспортирует себя, Модуль Д ничего не экспортирует.

Собираем

// Глобальное замыкание нашей сборки
(function(window, undefined){
    // Хелпер для проброса переменных в контекст
    var Medium = {
        wait: function (varName, callback) {/* какая-то своя логика */},
        ready: function (varName, varValue) {/* какая-то своя логика */}
   };

// Добавляем модуль A, обрамляем его в дополнительное замыкание
(function (ModuleC, $) {
    // Т.к. модуль ModuleC ещё не готов, то ожидаем его
    Medium.wait('ModuleC', function (value) {ModuleC = value;});

    (function (window) {
        var ModuleA = {
            a: 'ModuleA.a',
            b: 2,
            d: function () {
                console.log(ModuleC.c === 'ModuleC.c');
                console.log(typeof $ === 'function');
            }
        };

        // Export
        window.ModuleA = ModuleA;
    }(window));

} (undefined, $));

// Добавляем модуль C, обрамляем его в дополнительное замыкание
(function (ModuleA) {

    var ModuleC = (function (window) {
        var ModuleC = {
            a: 1,
            b: 2,
            c: 'ModuleC.c',
            d: function () {
                console.log(ModuleA.a === 'ModuleA.a');
            }
        };
    
        // Export
        return ModuleC;
    }(window));

    // Т.к. модуль ModuleC пассивно экспортируется, то он не попадает в глобалы, активно экспортируем его
    window.ModuleC = ModuleC;

    // Теперь модуль C готов - пробрасываем его в замыкание А
    Medium.ready('ModuleC', ModuleC);

} (ModuleA));

// Добавляем модуль Д, обрамляем его в дополнительное замыкание
(function (ModuleA,ModuleС) {

    // Uses ModuleA, ModuleC
    ModuleA.c();
    window.setTimeout(function () {
        ModuleC.d();
        ModuleA.d();
    }, 0);

} (ModuleA,ModuleС));

// Теперь можно удалить глобальные переменные - все необходимые переменные уже проброшены в контекст модулей
try { 
delete window.$;
delete window.jQuery;
delete window.ModuleA;
delete window.ModuleС;
} catch (e){ // IE фикс
window.$ = undefined;
window.jQuery = undefined;
window.ModuleA = undefined;
window.ModuleС = undefined;
}

// Все работает как и работало раньше и в глобалах чисто!
}(window));

Проблема захвата переменной


Данное решение позволяет только затруднить анализ кода и очистить глобальный контекст от лишних переменных (для красоты).
Не секрет, что скрипт, встроенный до подключения наших скриптов (типичный userscript или extension) может перехватить объект ModuleA и ModuleС до того как мы их удалим, используя watch, __defineSetter__, ES5 set и возможно через минимальный setInterval.

У этой проблемы есть 3 решения:
1. не использовать активный экспорт (шанс захвата равен нулю, необходимо менять код)
2. использовать случайное имя глобальной переменной (шанс захвата стремится к нулю, необходимо менять код)
3. удалить наблюдателей и таймеры перед началом сборки (шанс захвата равен нулю)

Будем использовать способ 3, изменим нашу сборку:
// Глобальное замыкание нашей сборки
(function(window, undefined){
// Удаляем __defineSetter__, ES5 set, watch
window.unwatch && window.unwatch('ModuleA');
window.unwatch && window.unwatch('ModuleС');
try { 
delete window.ModuleA;
delete window.ModuleС;
} catch (e){ // IE фикс
window.ModuleA = undefined;
window.ModuleС = undefined;
}

// Убиваем таймеры
var maxIntervalId = window.setInterval(function (){}, 1e10);
var maxTimeoutlId = window.setTimeout(function (){}, 1e10);

while (maxIntervalId--) {
    window.clearInterval(maxIntervalId);
}

while (maxTimeoutlId--) {
    window.clearTimeout(maxTimeoutlId);
}

// Подключаем как обычно

// ...


}(window));

Теперь и вы овладели самой сложной техникой JavaScript Ninja — «Сокрытие глобальных переменных»!

Исключение

Данный метод не поможет вам, если вы специально сорите в глобалы, используете вызов событий через onsmth="...", т.е. делаете плохо ;) Метод не сработает если у вас в глобале находится не объект.

Ninja JavaScript Builder — Ninjs


Чтобы вам не писать реализацию данного метода сборки я написал тузлу — Ninjs. Это сборщик проекта на JavaScript по методу, описанному выше. Билдер как ни есть лучше оправдывает своё название — он как нинзя незаметно делает своё дело и подчищает следы.

Проект находится на github github.com/azproduction/ninjs
Использует Node.js для сборки.
Пока не имеет регистрации в npm, но обязательно будет.
Пока не умеет предотвращать захват переменных — будет.

Использование

Использовать Ninjs очень просто — нам достаточно знать какие переменные импортируют/экспортируют наши модули и где они лежат. Все зависимости Ninjs разрешит за вас.
var ninjs = new (require('../Ninjs.js').Ninjs);

ninjs
.add({ // Добавляем ModuleA
    file: './files/ModuleA.js',
    // Он пассивно импортирует ModuleC и ModuleB
    imports: ['ModuleC', 'ModuleB'],
    // Активно экспортирует ModuleA
    exports: 'ModuleA'
})
.add({ // Добавляем ModuleB
    file: './files/ModuleB.js',
    // Он пассивно импортирует ModuleA и активно импотирует jQuery
    imports: 'ModuleA',
    // Активно экспортирует ModuleB
    exports: 'ModuleB'
})
.add({ // Добавляем ModuleD
    file: './files/ModuleD.js',
    //  Он пассивно импортирует ModuleA, ModuleB and ModuleC
    imports: ['ModuleA', 'ModuleB', 'ModuleC']
    // Ничего не экспортирует
})
.add({ // Добавляем ModuleC
    file: './files/ModuleC.js',
    // Он пассивно импортирует ModuleA, ModuleB
    imports: ['ModuleA', 'ModuleB'],
    // Он пассивно экспортирует ModuleC
    exports: 'ModuleC',
    // Добавляем ModuleC в активный экспорт, иначе ничего не получится
    forceExports: 'ModuleC'
})
// Чистим глобалы
.cleanup('ModuleA', 'ModuleB', 'ModuleC', '$', 'jQuery')
// Выводим STDOUT
.print(true);

// Либо записываем значение в переменную и передаем код минимизатору, как вам удобнее
// .print(false);

Код примера и код модулей находится тут github.com/azproduction/ninjs/tree/master/examples

Следите за обновлениями. Предложения, пожелания и критика приветствуются!

PS Ищу Ninja иконку для превращения её в логотип Ninjs.
Tags:
Hubs:
+42
Comments 35
Comments Comments 35

Articles