Представьте, у вас есть проект, состоящий из нескольких модулей и, например, jQuery или любая другая библиотеки в CDN. У вас есть огромное желание не показывать пользователю ваши глобальные переменные и по возможности не показывать jQuery и $. Ну и, конечно, сделать все без изменения кода проекта.
Причины для сокрытия глобалов могут быть разные: для красоты, из соображений безопасности, для затруднения анализа кода и другие. Пользователь взаимодействует с вашим кодом, используя события, которые он не сможет сломать — больше ему ничего и не нужно.
Самый очевидный способ — создать единственный namespace в который пассивно экспортировать прочие объекты, а jQuery и $ в конце удалить.
После сборки код будет какой-то такой:
Это идеальный вариант, но чаще бывает не так. Посмотрите ваш код, такой ли он?
Под катом универсальное решение, позволяющее собрать любой код без единой глобальной переменной.
Активный экспорт
Пассивный экспорт
Активный импорт
Пассивный импорт
Имеем несколько модулей, которые как попало импортируют/экспортируют. Мы знаем какие модули использует каждый. Мы можем обернуть каждый модуль в замыкание в которое передать список всех объектов, которые он использует. Правда тут есть небольшая проблема: если модуль А использует модуль Б, а модуль Б использует модуль А и все они подключаются последовательно, то при подключении модуля А, обернутого в замыкание будет ошибка ReferenceError — модуля Б ещё нет. Эта проблема решается пробросом объекта в замыкание А после загрузки модуля Б. Если модуль пассивно экспортирует и если мы его обернем в замыкание, то пропадет глобальная переменная. Эта проблема решается пробросом локальной переменной в глобальный контекст.
Мы знаем список глобальных переменных, которые мы создали в процессе инициализации скрипта — просто удаляем их через delete для очистки глобалов.
Разберем пример проекта, состоящий из jQuery и 3 модулей, которые как попало импортируют/экспортируют.
Модули подключаются в такой последовательности: jQuery отдельно, ModuleA+ModuleC+ModuleD. Модуль А зависит от модуля С, модуль С зависит от модуля А (конфликт, описанный выше). Модуль А активно экспортирует, модуль С пассивно экспортирует себя, Модуль Д ничего не экспортирует.
Данное решение позволяет только затруднить анализ кода и очистить глобальный контекст от лишних переменных (для красоты).
Не секрет, что скрипт, встроенный до подключения наших скриптов (типичный userscript или extension) может перехватить объект ModuleA и ModuleС до того как мы их удалим, используя watch, __defineSetter__, ES5 set и возможно через минимальный setInterval.
У этой проблемы есть 3 решения:
1. не использовать активный экспорт (шанс захвата равен нулю, необходимо менять код)
2. использовать случайное имя глобальной переменной (шанс захвата стремится к нулю, необходимо менять код)
3. удалить наблюдателей и таймеры перед началом сборки (шанс захвата равен нулю)
Будем использовать способ 3, изменим нашу сборку:
Теперь и вы овладели самой сложной техникой JavaScript Ninja — «Сокрытие глобальных переменных»!
Данный метод не поможет вам, если вы специально сорите в глобалы, используете вызов событий через onsmth="...", т.е. делаете плохо ;) Метод не сработает если у вас в глобале находится не объект.
Чтобы вам не писать реализацию данного метода сборки я написал тузлу — Ninjs. Это сборщик проекта на JavaScript по методу, описанному выше. Билдер как ни есть лучше оправдывает своё название — он как нинзя незаметно делает своё дело и подчищает следы.
Проект находится на github github.com/azproduction/ninjs
Использует Node.js для сборки.
Пока не имеет регистрации в npm, но обязательно будет.
Пока не умеет предотвращать захват переменных — будет.
Использовать Ninjs очень просто — нам достаточно знать какие переменные импортируют/экспортируют наши модули и где они лежат. Все зависимости Ninjs разрешит за вас.
Код примера и код модулей находится тут github.com/azproduction/ninjs/tree/master/examples
Следите за обновлениями. Предложения, пожелания и критика приветствуются!
PS Ищу Ninja иконку для превращения её в логотип Ninjs.
Причины для сокрытия глобалов могут быть разные: для красоты, из соображений безопасности, для затруднения анализа кода и другие. Пользователь взаимодействует с вашим кодом, используя события, которые он не сможет сломать — больше ему ничего и не нужно.
Самый очевидный способ — создать единственный 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.