Отсутствие модулей - это одна из самых неприятных проблем джаваскрипта, даже когда пишешь не слишком большое приложение. Когда пишешь большое, полноценное внутрибраузерное приложение, это становится проблемой первого порядка - без системы модулей что-то писать становится невозможно.
Конечно, эту проблему решают разными способами и давно, но всерьëз я рассматриваю только один - AMD, Asynchronous Module Definition. Почему только его - потому что другие обычно либо являются кальками модулей с сервера (которые поголовно все синхронные), либо требуют танцев и прогибания под их проблемы.
Конечно, при каких-то условиях можно себе позволить кальку с CommonJS, например Stitch, если не беспокоит то, что и при разработке, и при деплое придëтся все модули собирать в один файл. Меня это не устраивает, я хочу разные файлы
- чтобы при разработке было проще понимать, где проблемы и чтобы окончательное приложение могло грузить разный набор фич в зависимости от обстоятельств. Первое должны решать source maps, но их поддержка только начинает появляться в браузерах.
Формат
Короче, CommonJS меня не устраивает, делать свой велосипед я не хочу, поэтому из кросс-фреймворковых вариантов у нас только AMD. В своей канонической форме модули выглядят вот так:
define(['dep1', 'dep2'], function(dep1, dep2) {
return {my: 'exports'};
});
Такой вариант меня мало радует, если честно, писать так очень неприятно, особенно когда количество зависимостей достигает десятка. И поддерживать зависимости сразу в списке загрузки и аргументов - это кошмар. Поэтому мы используем то, что в документации RequireJS называется “прокладка CommonJS”:
define(function(require) {
var dep1 = require('dep1');
var dep2 = require('dep2');
return {my: 'exports'};
});
Здесь есть один важный нюанс - require
должен вызываться со строкой в качестве
аргумента. В смысле это должна быть обязательно строка, а не переменная или
выражение. Потому что тело модуля разбирается без исполнения, чтоб достать все
зависимости. Но мне не кажется, что это недостаток - в питоне, например, import module
- тоже никаких переменных не терпит.
А если они таки нужны, то всегда можно загрузить их дополнительно асинхронно:
define(function(require) {
require([varWithDep], function(dep) {
});
});
Так как пишем мы на кофескрипте, то выглядит это всë вполне пристойно:
define (require) ->
dep1 = require 'dep1'
{property} = require 'dep2'
return my: 'exports'
Один лишний уровень вложенности - такая плата за модульную систему джаваскрипта, приспособленную к браузерам. %)
Сборка
Теперь в index.html
грузится только один Самый Главный модуль, который уже
сам (в зависимостях или как ему нравится) грузит всех остальных и запускает
приложение. Всë хорошо, но выходит у нас, например, сотня модулей - при
разработке это хорошо, но пользователям такое давать не хочется. :)
Специально для решения этой проблемы у RequireJS есть r.js -
сборщик. Интерфейс у него, конечно, не очень приятный - опции в странном
формате, документация только в вебе (r.js --help
говорит идти на гитхаб читать
README
). Но в целом работает он нормально - говоришь ему, вот мой файл, он
начинает его парсить, ищет зависимости и собирает это всë в один файл.
Это почти хорошо, если забыть моë желание грузить только то, что будет
использоваться. Наше приложение состоит из нескольких больших модулей (скажем,
подприложений), и при различных обстоятельствах нужно загружать только
некоторые из них. При этом r.js
всë это не даëт сделать прямо, или просто мне
не хватило мозгов или терпения понять, что с ним делать.
В какой-то момент я просто сдался и написал свой сборщик, который, начиная
с заданного файла, собирает в один файл только то, что было импортировано с
относительным путëм - т.е. require './some/dep'
. Для нас это работает, так как
каждое подприложение импортирует свои файлы через относительные пути, а всякие
библиотеки - через абсолютные.
Тут кстати важный момент, что ядро приложения ничего не знает про внутренности
подприложений, и грузит только главный файл - subapp/init.js
, в который всë и
собирается в результате. Иначе оно бы пыталось загружать несуществующие
файлы. Можно, конечно, сделать маппинг для RequireJS, расширение того, что
описан ниже.
Еще один нюанс - скрипт простой и не терпит каких-то девиаций от простого
define(function(require) {})
.
Кеширование
Еще, очевидно, всë хочется закешировать всë в хлам, но приложение обновляется
часто. Выход - в Nginx дописывается expires 12m;
, а в index.html
добавляется
конфигурация для RequireJS, соответствие нормального пути к модулю - пути с
мд5-суммой. Т.е.:
require.config({paths: {"path/to/mod.js": "path/to/mod.js?md5sum"}});
При изменении содержимого меняется мд5-сумма, и все пользователи получат новый
код. Главное - не кешировать index.html
, по крайней мере надолго. :)
Собирается такой словарь другим скриптом.
Закругление
Всë это работает и весьма неплохо. У RequireJS есть конкуренты, тоже AMD-загрузчики, навскидку - curl.js, SeaJS, еще кто-то был - не приходит в голову - но у них всех менее активная разработка, поддержка и коммьюнити. Ничего особенного они не предоставляют, поэтому дëргаться смысла не вижу.