Объединение javascript файлов
Эпоха тёплого лампового WEB 1.0 давно прошла, и в последнее время мы имеем дело со страницами, которые кишат так называемой динамичностью. Динамичность может быть обеспечена при помощи JavaScript, VbScript и плагинами вроде Java, Flash, Silverlight. В этой статье я хочу затронуть одну из возможных оптимизаций web-сайта — объединение всех javascript файлов в один.
Зачем?
Основных причин две:
- Повышение скорости загрузки страницы.
- Снижение нагрузки на сервер.
Начнём с «повышения скорости загрузки». Зачастую web-сайт просто пестрит скриптами и их общее число может перевалить за 50. Впрочем, это уже будет «клинический случай». Но хотя бы 15-30 встречается регулярно. На каждый чихскрипт браузер посылает запрос и, в зависимости от ответа, либо грузит его полностью, либо забирает из кеша. 15 запросов это много. На это нужно время. Да, все современные браузеры загружают файлы параллельно, но это не повод их так эксплуатировать. В моём случае скорость загрузки страницы возросла в несколько раз.
Касательно снижения нагрузки на сервер — всё сложнее. Снижение числа запросов в любом случае улучшает ситуацию, но вот насколько — я сказать затрудняюсь, т.к. я не админ. Я полагаю, что для снижения нагрузки можно найти массу более простых и действенных решений. Возможно, это, так называемая, экономия спичек на фоне пожара. Но в качестве побочного эффекта — сгодится.
Как?
Полагаю, что основных способов всего два:
- Обернуть все файлы анонимными функциями, которые нужно будет вызывать единожды, по мере необходимости. Либо писать модульный код, где каждый файл может содержать 1 или несколько модулей, которые сами по себе не запускаются.
- Весь код каждого файла поместить в строку, которую eval-ить по первому требованию.
В первом случае мы избавляемся от ненавистного многими eval-а, но заставляем браузер компилировать неиспользуемый код. Во втором случае мы используем eval, заставляя браузер компилировать часть уже скомпилированного файла. Выглядит это всё примерно так:
// анонимные функции.
window.__js = {
'engine': function(){ /* код */ },
'some': function(){ /* код */ }
};
// строки
window.__js = {
'engine': '/* код */',
'some': '/* код */'
};
Но у меня не было выбора, т.к. используемый движок кишит кодом «сомнительного качества». Такой код я не могу обернуть в анонимную функцию, потому, что:
// файл 1
function some(){
...
}
// файл 2
some();
Если обернуть оба файла в анонимные функции и после этого выполнить, то мы получим ошибку — браузер не сможет найти some. Причина кроется в том, что полученный код:
function(){
function some(){
...
};
}
Вовсе не приводит к window.some !== undefined; Функция some определяется в области видимости (scope) анонимной функции, а вовсе не window, как это было бы, если бы она была определена в отдельном файле. Решение этой проблемы нашлось в jQuery. Дело в том, что выполнить javascript-код в глобальной области видимости можно используя:
( window.execScript || function( data ) {
window[ "eval" ].call( window, data );
} )( data );
В зависимости от браузера мы вызываем либо execScript, либо запускаем привычный нам eval, задавая ему this равным window. Такой подход используется в jQuery начиная с версии 1.6. В более ранних версиях создавался тег <script />, в который помещался нужный код, и этот скрипт прикреплялся к документу.
Сжатие и обфускация
Параллельно сборке всех файлов в список мы можем над ними поиздеваться. Во первых их можно сжать, во вторых испортить их читабельность. Для этого можно воспользоваться YUI Compressor-ом или любым его аналогом. В конечном итоге мы получаем несколько меньше кода без форматирования (отступы, лишние пробелы, укороченные имена локальных переменных и пр.), сжатого в одну строку.
Компоновка
Самая простая часть задачи. Алгоритм прост, как валенок:
- Пробегаем по списку файлов (можно воспользоваться маской, к примеру: js/*.js).
- Запоминаем дату изменения файла.
- Сверяем её с датой создания уже сжатого файла, если таковой имеется.
- Если файл обновлён, или же сжатой копии нет вовсе — сжимаем и сохраняем в отдельном каталоге (либо используем префиксы, например: min_#{file_name}.
- Пробегаем по списку сжатых файлов, поочерёдно добавляя их содержимое в массив.
- Сохраняем результат в «итоговый единый javascript-файл»
Активировать этот алгоритм можно либо вручную, либо при загрузке каждой страницы.
Отладка
Жизнь программиста была бы прекрасна, если бы не многочисленные баги, которые имеют привычку появляться не вовремя и хорошо прятаться. Тут наша с вами затея терпит крах по всем фронтам. Наш код нечитаем, firebug на нём виснет, и ошибки указывают невесть куда. К тому же большинство переменных имеют вид a, b, c. На помощь к нам приходит Сhrome. Дело в том, что он умеет «де-обфусцировать» код до вполне читабельного состояния (контекстное меню во вкладке Scripts).
Например:
function oa(a) {
var b = 1, c = 0, d;
if (!D(a)) {
b = 0;
for (d = a[s] - 1; d >= 0; d--) c = a.charCodeAt(d), b = (b << 6 & 268435455) + c + (c << 14), c = b & 266338304, b = c != 0 ? b ^ c >> 21 : b;
}
return b;
}
Результат весьма далёк от оригинала, но такое уже можно хотя бы прочитать. К сожалению есть некоторые проблемы с постановкой точек останова и их срабатыванием. Но на безрыбье и рак рыба. Жить можно.
Финальный штрих
Если в конец кода, который будет пропущен через eval добавить конструкцию /* //@ sourceURL=#{name}*/, chrome покажет нам заданный #{name} в списке скриптов. К сожалению, в Firefox этот механизм у меня не заработал. Комментарий-обёртка мне понадобился для IE.
Использование
Локально работать с «единым файлом» чертовски неудобно, поэтому можно написать примерно такой велосипед:
window.__js_ready = {};
function __include( name )
{
// проверяем не выполнялся ли он ранее
if( !__js_ready[ name ] )
{
__js_ready[ name ] = true;
// выполняем скрипт
if( ONLINE ) // "продакшн"
{
( window.execScript || function( data ) {
window[ "eval" ].call( window, __js[ name ] );
} )( data );
}
else // локально
{
// добавляем скрипт любым понравившимся вам методом, например
// через document.write
}
}
}
В html коде:
<script type="text/javascript">
__include( 'engine' );
</script>
<script type="text/javascript">
// используем "engine"
</script>
Разумеется, вариантов реализации подключения скрипта может быть множество. Да и этот можно улучшить. Например, поставив движок сайта на «событийную основу». Т.е. выполнять какой-либо код только тогда, когда выполнился ряд условий, например: были загружены все требуемые модули.
Минусы
- Необходимость компилирования «единого скрипта».
- Трудность отладки.
- Пользователь при первом запуске грузит сразу все скрипты сайта. Впрочем, минус надуманный в случае использования gzip.