Сумин Андрей, 7 мая 2007.
У меня часто возникает задача плана: сделать JavaScript компонент (далее просто компонент) и вставить его в разные места на сайте. Как правило, это выливается в некоторый файл на серверном языке, который:
Все хорошо, но есть неприятные моменты:
Все это отнимает время и силы иногда нескольких людей.
Прежде чем что-то менять, необходимо понять, что хочется получить в результате. Если поставить себя на место серверного программиста, то наверное, есть желание в том месте, где находится компонент на странице, оставить метку и не заботиться о JavaScript файлах. На клиентской стороне нужно понимать, где на странице находится компонент и что это за компонент, плюс реализовать три последних пункта из первого списка, т.е. сделать следующие действия:
Отметку компонента я всегда ставлю какому-нибудь узлу дерева. У меня не было случаев, когда это нельзя было сделать, плюс, если что-то не так с JavaScript, этот узел может обеспечивать минимальный необходимый функционал.
После долгих экспериментов я остановился на том, что удобнее всего отмечать узел особенным именем класса. Также личный опыт показал, что не следует привязываться к этому имени класса в CSS.
<div class="component"></div>
Теперь найти все узлы, которые являются компонентами, не сложно. Любая современная JavaScript библиотека поддерживает CSS селекторы.
var components = getElementsByCSSSelector('.component');
Я специально не советую какую-то конкретную JavaScript библиотеку, подчеркивая, что эти приемы не завязаны на какую-то из них, да и вообще можно обойтись совсем без сторонних библиотек. Если же быть честным, то на момент написания статьи я перестал использовать свои методы поиска компонентов и стал использовать prototype, но думаю что jQuery или base2, или что-то еще тоже отлично справятся с этой задачей.
После того, как найден компонент, нужно понять, что это за компонент. Я считаю, что самый простой и удобный способ - указать тип компонента (спасибо Виталию Харисову) это передать хеш объект в атрибуте onclick.
<div class="component" onclick="return {type:'Type'}"></div>
Значение атрибута onclick передается методу onclick при инициализации узла дерева браузером. Теперь мы можем выполнить метод onclick у найденного узла и получить хеш объект со всей нужной нам информацией.
var node = components[index];
var type = node.onclick().type;
Рядом с типом можно передавать инициализирующие параметры компонента.
<div class="component" onclick="return {type:'Type', params: { ... }}"></div>
Я не буду подробно рассказывать о методах динамической загрузки JavaScript файлов, основные сложности наблюдаются в старых браузерах, в последних версиях популярных браузеров работает следующий код:
createScript = function (src, charset){
var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.setAttribute('charset', charset);
script.setAttribute('src', src);
// InsertBefore for IE.
// IE crashes on using appendChild before the head tag has been closed.
var head = document.getElementsByTagName('head').item(0);
head.insertBefore(script, head.firstChild);
}
После того как компонент найден, известен его тип нужно чтобы на странице появилась JavaScript функциональность для его работы. Зная тип компонента можно загрузить файл с описанием его поведения, но современная реальность такова, что в одном файле поведение компонента не находится, как правило, это несколько файлов. Конечный файл с описанием поведения зависит от библиотек, которые являются общими для нескольких компонентов. В свою очередь библиотеки могу зависеть от других библиотек и так далее. Мне чтобы сделать механизм загрузки функционала компонентов пришлось для файла с какой-либо функциональностью реализовать следующие возможности:
В общем случае каждый файл должен иметь возможность сделать следующее:
require(
// указать зависимость от других файлов
['file1', 'file2' ...],
// получить уведомление о их загрузке (выполнение функции)
function(){
// инициализация
// сигнализировать о своей готовности
loaded('file');
}
);
Каждый файл из списка по окончанию своей загрузки и инициализации должен вызвать функцию loaded со своим именем в качестве аргумента. Функция require должна дождаться вызовов loaded от всех файлов из списка ['file1', 'file2' ...] и вызвать переданную функцию function. Если файл однажды загрузился или в процессе загрузки, не следует загружать его заново.
Я не буду приводить конкретные реализации этой функциональности в статье, как я писал выше, реализация может сильно зависеть от окружения и используемых библиотек. Ссылки на пример с моей последней реализацией есть в конце статьи.
Итак, теперь можно найти компонент, определить его тип, загрузить все файлы, которые реализуют его функционал, и проинициализировать компонент.
var components = getElementsByCSSSelector('.component');
var node = components[index];
var component = component.onclick();
require(
[getFileName(component.type)],
function(){
window[getObjectName(component.type)].init(node, component.params);
}
);
В моей реализиции у каждого файла есть уникальное имя, которое можно транслировать как в путь к JavaScript файлу, так и в имя объекта, который лежит в этом файле (функции getFileName и getObjectName).
В результате на стороне сервера теперь нужно только лишь отметить узел дерева как компонент и указать его тип, всю логику по загрузке JavaScript файлов и инициализации выполнит клиент. Это решение легко переходит с проекта на проект, с одного серверного языка на другой.
Пример из жизни тут: http://jsx.ru/Texts/ModulesInJS/example.html.
Он немного сложнее, используются алиасы, но о них я обязательно напишу позже.
Пример файла, который динамически загружает скрипты тут: http://jsx.ru/scripts/b/1.1.0/jsx.js.
Ищет компоненты на странице тут: http://jsx.ru/scripts/b/1.1.0/Components.js
Обсудить статью можно тут: http://andrewsumin.livejournal.com/13127.html?view=84551