;-)
  • Инвентарь
  • Техногрет
  • Володя Колесников

    Инклюд в яваскрипте 19 марта 2007


     
    Задача. Упростить создание больших проектов.

    Инклюд 

    Современный крупный сайт невозможно представить без яваскрипта, и чем ближе разработчик желает приблизить свое приложение к тому, что называется вебом 2.0, тем больше становится доля яваскрипта в общем объеме программного кода.

    Большое число скриптов труднее структурировать, а элемент <head> каждой страницы превращается в нечто подобное:

    01 
    02 
    03 
    04 
    05 
    06 
    07 
    08 
    <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 что-нибудь вроде:

    01 
    02 
    03 
    04 
    05 
    06 
    07 
    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):

    01 
    02 
    03 
    04 
    05 
    06 
    07 
    08 
    09 
    10 
    11 
    12 
    13 
    14 
    //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"), а саму функцию сделать так:

    01 
    02 
    03 
    js.module = function(path) {
    	js.loadedModules[path] = true;
    }
    				

    Теперь инклюд будет знать о том, что скрипт с данным адресом уже загружен, даже если js-файл подключен с помощью тега <script>.

    Ну а кроме того, каждая загрузка скрипта — отдельный запрос на сервер. Поэтому если всегда необходимо загружать группу файлов как единый набор, оптимально объединить их в один большой. Тогда и серверу придется выполнять меньше операций, и трафик между браузером и сервером сократится. А нам в итоге нужно написать:

    01 
    02 
    03 
    04 
    05 
    06 
    07 
    08 
    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, будет утомительно и громоздко.

    Объявление неймспейса имен удобно вынести в функцию объявления модуля. Например, так:

    01 
    02 
    03 
    04 
    05 
    06 
    07 
    08 
    09 
    10 
    11 
    12 
    13 
    14 
    15 
    16 
    17 
    18 
    19 
    20 
    21 
    22 
    23 
    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 более покладиста, но не балует подсветкой синтаксиса. Проблема решается подключением отлаживаемых скриптов тегом <script>.


    Версии 

    Обычная (не требует других библиотек)   4,56 КБ,
    сжатая   2,72 КБ,
    для prototype.js   3,44 КБ,
    для prototype.js сжатая   1,95 КБ.