english version

Модульность в JavaScript, динамическая загрузка

Проблема

У меня часто возникает задача плана: сделать JavaScript компонент (далее просто компонент) и вставить его в разные места на сайте. Как правило, это выливается в некоторый файл на серверном языке, который:

  • вставляет компонент;
  • обеспечивает подключение необходимых JavaScript файлов;
  • следит за очередностью подключения файлов;
  • не позволяет нескольких загрузок одного файла.

Все хорошо, но есть неприятные моменты:

  • для каждого нового компонента необходимо создавать серверную функциональность;
  • если есть необходимость поддерживать несколько проектов на разных серверных технологиях, то на каждом нужно реализовывать (т.е. дублировать) функционал, описанный выше.

Все это отнимает время и силы иногда нескольких людей.

Предлагаемое решение

Прежде чем что-то менять, необходимо понять, что хочется получить в результате. Если поставить себя на место серверного программиста, то наверное, есть желание в том месте, где находится компонент на странице, оставить метку и не заботиться о JavaScript файлах. На клиентской стороне нужно понимать, где на странице находится компонент и что это за компонент, плюс реализовать три последних пункта из первого списка, т.е. сделать следующие действия:

  • найти компонент на странице;
  • определить, что это за компонент;
  • обеспечить подключение необходимых JavaScipt файлов;
  • следить за очередностью подключения файлов;
  • не позволять нескольких загрузок одного файла.

Тег как компонент

Отметку компонента я всегда ставлю какому-нибудь узлу дерева. У меня не было случаев, когда это нельзя было сделать, плюс, если что-то не так с 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 файлов

Я не буду подробно рассказывать о методах динамической загрузки 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