english version

Модульность в JavaScript, алиасы

Проблема

В прошлой заметке я остановился на уникальных именах файлов. В этой статье я опишу, что это и как я их использую.

Реализовав функцию указания зависимостей require , я могу не заботится о порядке подключения файлов, однако в самих файлах я должен указать пути к файлам, от которых они зависят. Указание пути прямыми ссылками приведет к куче проблем:

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

Вообще это неудобно, хочется указать зависимость от функционала или объекта, а не от файла.

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

После активных обсуждений с Сергеем Бережным появилась идея делить файлы на группы (общие и относящиеся к проекту библиотеки) и присваивать файлам имена типа "{alias}.name", которое можно транслировать как в имя файла "alias/name.js", так и в имя объекта "alias.name". alias будет идентифицировать группу файлов. Это позволит использовать файлы следующим образом:

alias.object = {
  method: function(){
    alias.object1.method();
    alias.object2.method();

    loaded('{alias}.object');
  }
};
require(
  ['{alias}.object1', '{alias}.object2' ...],
  alias.object.method
);

Алиас как путь к файлу

Итак, во всех файлах у нас есть строка вида "{alias}.name", надо вместо нее подставить реальный путь к файлу. Оказалось, удобно подключить на странице файл, который лежит там же, где и все остальные файлы с этим алиасом, и взять его путь как основу.

<script src="path/alias.js"></script>
          

При загрузке этот файл устанавливает значение алиаса, причем само значение берется из scr тега script, т.е. поменяв путь к срипту, мы меняем значение алиаса.

var alias = {};
getBaseAndSetAlias('alias', 'alias.js', 'utf-8');
          

Первой строкой создается объект, чтобы все объекты из этого алиаса были его методами. Во второй строке вызывается метод ядра, который берет путь к файлу (второй аргумент) и присваивает его алиасу (первый аргумент). Третий аргумент указывает кодировку файлов с данным алиасом. Ниже я приведу возможную реализацию этого метода.

Для начала надо получить тег script по имени файла.

var scriptsByFileName = [];
  function getScriptByFileName (file, listener, /* private */ tries){
  
    // если файл уже спрашивали раньше и он был найден
    if(scriptsByFileName[file]){
      listener(scriptsByFileName[file]);
      return;
    }
  
    // устанавливаем количество попыток нахождения
    // файла в ноль, если оно не пришло
    tries = tries || 0;

    // тут надо отфильтровать один файл из списка
    // по имени
    var scripts = document.getElementsByTagName('script');
    for (var i = 0, l = scripts.length; i < l; i++){
      var src = scripts[i].getAttribute('src');
      if(src && src.indexOf(file) >= 0){
        scriptsByFileName[file] = scripts[i];
        break;
      }
    }

    // если тег найден, то вызываем listener с тегом
    // в качестве параметра
    if(scriptsByFileName[file]){
      listener(scriptsByFileName[file]);

    // в противном случае пробуем позже
    }else if(tries < 100){
      window.setTimeout(function (){getScriptByFileName(file, listener, ++tries)}, 10);
    }
}
          

Этот метод у меня сделан асинхронным, потому что иногда можно выполнять код тега скрипт, а сам тег может еще не будет в документе. Все последующие методы тоже должны быть асинхронными.

Теперь можно легко вычислить путь к файлам.

function getBase(file, listener){
  function returnBase(script){
      var src = script.getAttribute('src');
      listener(src.substring(0, src.indexOf(file)));
  }
  getScriptByFileName(file, returnBase);
};
          

И установить значение алиаса.

var aliases = {};
  function setAlias(name, value){
    aliases[name] = value;
};
          

Функция, которая выполняет все это getBaseAndSetAlias.

function getBaseAndSetAlias(alias, file, charset){
  this.getBase(file, function(base){
    setAlias(alias, base);
  });
};
          

Теперь можно строить пути к файлам по строкам вида "{alias}.name".

function construct(string){
  
  // замена точек на слэши
  string = string.replace(/\./g, '/');
  
  // замена алиаса на его значение
  function replacealias(match){
    var alias = match.substr(1, match.length - 2);
    return aliases[alias];
  }
  string = string.replace(/\{[^\}]+\}/ig, replacealias);

  // удаляю лишние двойные слэши
  var http = string.match(/^https?\:\/\//i) || '';
  string = string.replace(/^https?\:\/\//i, '');
  string = string.replace(/\/\//ig, '/');

  // результирующий путь
  return http + string + '.js';
};
          

В результате можно во всех зависимостях и уведомлениях (require, loaded) писать имена файлов вида "{alias}.name", а при смене месторасположения файлов менять путь только в теге script в HTML.

Алиас в имени объекта

Превращать "{alias}.name" в имя объекта мне нужно всего в одном месте, но в очень важном. Указывая тип компонента, я использую точно такой же метод указания, только теперь не на файл, а на объект.

function getComponent(location){

  // запоминаю алиас
  var alias = /^\{[^\}]+\}/.exec(location);
  alias = alias[0].substr(1, alias[0].length - 2);

  // путь к обекту
  var name = location.replace('{' + alias + '}.', '').split('.');

  // получаю объект
  var Component = window[alias];
  for (var i = 0, l = name.length; i < l; i++){
    Component = Component[name[i]];
  }

  // возвращаю полученный объект
  return Component;
}
          

Когда объект получен, можно вызвать его инициализирующий метод и получить компонент на странице.

Результат

Теперь я в своем коде использую следующие конструкции:

<script src="path/alias.js"></script>
      
<div class="component" onclick="return {type:'{alias}.name'}"></div>
          
require(
  ['{alias}.object1', '{alias}.object2' ...],
  alias.object.method
);
          

И изменение месторасположения файлов требует изменения пути всего в одном месте.

Примеры

Примеры можно взять те же, что и в предыдущей статье.
Пример файла, который динамически загружает скрипты, тут: http://jsx.ru/scripts/b/1.1.0/jsx.js.
Ищет компоненты на странице тут: http://jsx.ru/scripts/b/1.1.0/Components.js