| Володя Колесников
Инклюд в яваскрипте 19 марта 2007 |
| Задача. | Упростить создание больших проектов. | ||
Современный крупный сайт невозможно представить без яваскрипта, и чем ближе разработчик желает приблизить свое приложение к тому, что называется вебом 2.0, тем больше становится доля яваскрипта в общем объеме программного кода.
Большое число скриптов труднее структурировать, а элемент <head> каждой страницы превращается в нечто подобное:
0102030405060708
<script src="/js/als/widget/Box.js" type="text/javascript" encoding="UTF-8"></script> <script src="/js/als/utils/Text.js" type="text/javascript" encoding="UTF-8"></script> <script src="/js/domain/ClientsInfo/Widget/Properties.js" type="text/javascript" encoding="UTF-8"></script> <script src="/js/domain/ClientsInfo/Widget/Address.js" type="text/javascript" encoding="UTF-8"></script> <script src="/js/domain/ClientsInfo/Widget/Person.js" type="text/javascript" encoding="UTF-8"></script> <script src="/js/domain/ClientsInfo/Widget/Company.js" type="text/javascript" encoding="UTF-8"></script> <script src="/js/domain/ClientsInfo/Page/Index.js" type="text/javascript" encoding="UTF-8"></script> ... и еще 30 таких же ...
Но это полбеды. Ведь файл Page/Index.js зависит от Company.js, а тот в свою очередь от Widget/Person.js и Widget/Address.js. А те от Widget/Date.js и Box.js и т. д.
Причем загружать их нужно именно в указанной последовательности. А если страниц много? А если, например, хочется разделить какой-нибудь из виджетов на два файла? Или добавить пару новых классов? Или объединить несколько скриптов в один большой файл для ускорения загрузки?
Ведь все зависимости удобно было бы хранить непосредственно в js-файлах.
Почти в любом «взрослом» языке для этого существует конструкция include. В яваскрипте ее нет, но при должном желании ее удается сымитировать. Представьте, как удобно было бы написать в начале Company.js что-нибудь вроде:
01020304050607
js.include('als.Template');
js.include('als.utils.Text');
js.include('als.widget.Box');
js.include('domain.ClientsInfo.Widget.Properties');
js.include('domain.ClientsInfo.widget.Address');
js.include('domain.ClientsInfo.widget.Person');
...
Что же мешает так сделать? Или веб 2.0 способен лишь на раскрашивание кнопок?
Какие препятствия поджидают нас? Во-первых, инклюд должен полностью отрабатывать до начала выполнения кода. Во-вторых, не исключены зависимости нескольких файлов от одного модуля и загружать его дважды совсем не хочется. В-третьих, требуется какой-нибудь механизм загрузки файлов с сервера. Для начала препятствий хватит.
На дворе 2007 год, и подавляющее большинство браузеров поддерживают XHTTPRequest именно им и следует воспользоваться. Этот объект работает в двух режимах: асинхронном (когда указывается функция обратного вызова) и синхронном (запрос происходит непосредственно во время вызова xhttp.open()). Нам нужен второй: при этом код загрузится прямо на месте js.include. А дальше достаточно выполнить eval(xhttp.responseText):
0102030405060708091011121314
//js.js
js = {};
js.loadedModules = {};
┘
js.include = function(path) {
if(js.loadedModules[path]) return;
var transport = js.getXHTTPTransport();
transport.open('GET', js.rootUrl + path.replace(/\./g, '/') + '.js', false);
transport.send(null);
var code = transport.responseText;
eval(code);
}
Вот и все. Теперь перед загрузкой скриптов достаточно подключить js.js и инклюд работает.
Полезно вынести в отдельную команду объявление загруженного модуля. Например, в начале каждого файла писать js.module("js.Event"), а саму функцию сделать так:
010203
js.module = function(path) {
js.loadedModules[path] = true;
}
Теперь инклюд будет знать о том, что скрипт с данным адресом уже загружен, даже если js-файл подключен с помощью тега <script>.
Ну а кроме того, каждая загрузка скрипта отдельный запрос на сервер. Поэтому если всегда необходимо загружать группу файлов как единый набор, оптимально объединить их в один большой. Тогда и серверу придется выполнять меньше операций, и трафик между браузером и сервером сократится. А нам в итоге нужно написать:
0102030405060708
js.module("domain.Class1");
domain.Class1 = function() {┘}
js.module("domain.Class2");
js.include("domain.Class1");
domain.Class2 = function() {┘}
domain.Class2.prototype = new domain.Class1();
Замечание | То, что объявление модуля вынесено в самое начало файла (до инклюдов), позволяет избежать зацикливания даже в тех случаях, когда между файлами возникает циклическая зависимость. |
Раз уж мы раскладываем файлы по папкам вроде domain/ClientsInfo/Widget/Person.js, логично было бы в файле Person.js иметь описание класса domain.ClientsInfo.Widget.Person. Но если попытаться создать класс domain.ClientsInfo.Widget.Person при несуществующем domain.ClientsInfo.Widget, интерпретатор яваскрипта выдаст ошибку. Между тем, проверять в начале каждого файла, что существует объект domain.ClientsInfo.Widget, а вместе с ним и domain.ClientsInfo, и просто domain, будет утомительно и громоздко.
Объявление неймспейса имен удобно вынести в функцию объявления модуля. Например, так:
0102030405060708091011121314151617181920212223
js.evalProperty = function(object, name, value) {
if(object) {
if(!object[name]) object[name] = value || true;
return object[name];
}
return null;
}
js.evalPath = function(path, context, value) {
context = context || window;
var pos = path.indexOf('.');
if(pos == -1) {
return js.evalProperty(context, path, value);
} else {
var name = path.substring(0, pos);
var path = path.substring(pos + 1);
var obj = js.evalProperty(context, name, value);
return js.evalPath(path, obj, value);
}
}
js.module = function(path) {
js.loadedModules[path] = true;
js.evalPath(path);
}
Дебаггинг | Загруженный код выполняется с помощью функции eval(). Это означает, что в «Мозилле» отладить его не удастся. Visual Studio более покладиста, но не балует подсветкой синтаксиса. Проблема решается подключением отлаживаемых скриптов тегом |
Обычная (не требует других библиотек) 4,56 КБ,
сжатая 2,72 КБ,
для prototype.js 3,44 КБ,
для prototype.js сжатая 1,95 КБ.