HTML и CSSXSLTJavaScriptИзображенияСофтEtc
Володя Колесников

Инклюд в яваскрипте 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 КБ.