solovyov.net

RequireJS & AMD

Отсутствие модулей - это одна из самых неприятных проблем джаваскрипта, даже когда пишешь не слишком большое приложение. Когда пишешь большое, полноценное внутрибраузерное приложение, это становится проблемой первого порядка - без системы модулей что-то писать становится невозможно.

Конечно, эту проблему решают разными способами и давно, но всерьëз я рассматриваю только один - 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, еще кто-то был - не приходит в голову - но у них всех менее активная разработка, поддержка и коммьюнити. Ничего особенного они не предоставляют, поэтому дëргаться смысла не вижу.