LABjs и RequireJS: загрузка ресурсов JavaScript интересным способом

Кайл Симпсон (Kyle Simpson), Джеймс Берк (James Burke) | 18 августа 2010 г.

С загрузкой ресурсов JavaScript связано много проблем, зависящих от реализованного подхода. Растущее количество веб-сайтов и приложений использует сложные системы ресурсов JavaScript, и многие разработчики надеются на более простые, быстрые и удобные в обслуживании схемы управления зависимостями. Авторы двух проектов с открытым кодом, LABjs и RequireJS, обсуждают различные перспективы достижения этих целей.

Может показаться, что разговор о загрузке ресурсов JavaScript не может представлять ничего интересного. Тег <script>используется около 20 лет, так о чем еще тут можно разговаривать? В этой статье мы собираемся рассмотреть некоторые примеры, для которых требуются более мощные и гибкие решения, чем базовый тег <script>.

Не существует единого решения, удовлетворяющего всем целям и потребностям. Следует учесть не только различные возможности синтаксиса, но и различные компромиссы, зависящие от того, какие случаи использования являются первоочередными. LABjs и RequireJS воплощают многие из этих компромиссов.

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

Эта статья в основном фокусируется на загрузке ресурсов в среды на основе браузеров, но, как будет показано позднее, появление JavaScript на стороне сервера и необходимость "загружать" модули на стороне сервера оказывают определенное влияние на подход к загрузке браузера.

Загрузка скриптов

Сначала давайте убедимся, что все мы находимся на одной "странице". Темой нашего разговора является проблема необходимости загружать файлы JavaScript (либо "локально" в том же домене, либо "удаленно", то есть междоменно) на веб-страницу. Как вам, конечно, известно, обычно это выполняется с помощью старых добрых тегов <script>.

Тем не менее существуют определенные ограничения использования тегов <script>, особенно с ростом сложности или объема. Во-первых, использование тегов <script> в HTML-разметке предполагает, что во время создания известны все ресурсы, которые должны быть загружены, и что известны все возможные зависимости между ними, чтобы можно было правильно упорядочить используемые теги <script>. Например, при наличии десятков файлов, связанных пересекающимися зависимостями, нужно отсортировать всю эту неразбериху вручную и указать правильную последовательность тегов <script> — задача, которая, очевидно, может оказаться утомительной и внести свой "вклад" в усложнение стандартов разработки и поддержки веб-сайта/группы разработчиков.

Во-вторых, до браузеров последнего поколения тег<script> отрицательно влиял на быстродействие браузеров. Конкретно говоря, теги <script>вызывают "блокировки", то есть они вызывают остановку всего другого, что могло загружаться или происходить на странице, на время своей загрузки и своего выполнения. Они не только останавливают все остальные части страницы, но обычно и загрузку всех остальных тегов <script>.

Поэтому, если в строке используется 10 тегов <script>, они будут загружаться по порядку, по одному (то есть последовательно), выполняясь после своей загрузки. Понятно, что это приведет к резкому замедлению загрузки страниц, особенно если один из ресурсов размещен в удаленном местоположении, которое либо не работает, либо перегружено и реагирует медленно.

Даже в свежайших версиях браузеров новейшего поколения мы обнаружили только частичное улучшение этих последовательных загрузок и выполнений. Большинство новых браузеров сегодня достаточно разумны, чтобы загружать сценарии параллельно, но они все равно будут ждать, пока загруженные скрипты не выполнятся по одному, последовательно. Кроме того, так как браузеру нужно учесть, что в файле может встретиться что-то наподобие document.write(), браузер должен блокировать визуализацию страницы, пока не будет выполнен каждый файл. Иллюстрации "водопадной диаграммы", приведенные на рис. 1 и 2, показывают, как загрузка и блокирующее поведение скрипта влияют на оставшуюся часть страницы в различных браузерах.


Рис. 1. "Водопадное поведение" загрузки скрипта в FF 3.0, последовательная загрузка и последовательное выполнение


Рис. 2. "Водопадное поведение" загрузки скрипта в FF 3.5+, параллельная загрузка и последовательное выполнение

К счастью, браузер позволяет выполнять ряд вещей асинхронно и по запросу, поэтому многие из этих проблем производительности можно решить с помощью динамической загрузки нужных файлов JavaScript. Существуют десятки различных “загрузчиков JavaScript” различной сложности и с различными возможностями. Многие из них являются частью специальных пакетов средств, например загрузчики, входящие в Dojo и YUI.

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

Хотя LABjs и RequireJS используют различные подходы, существует ряд преимуществ, которыми оба этих решения обладают по сравнению с базовыми тегами <script> в HTML:

1. Динамически создаваемые элементы script, позволяющие избежать блокировки визуализации браузером

Благодаря добавлению скриптов на страницу с помощью динамически создаваемых элементов script браузер больше не будет блокировать визуализацию, ожидая загрузки скрипта, как показано в примере кода 1.

function addScript(url) {
    var script = document.createElement('script');
    script.type = 'text/javascript';
    document.getElementsByTagName('head')[0].appendChild(script);
}

Пример кода 1. Упрощенный пример функции, динамически создающей тег script и добавляющей его в DOM.

Вставка элементов script на лету обладает двусторонним эффектом: страница будет "разблокирована", поэтому она может продолжить загружать свои ресурсы параллельно со скриптами, но это также полностью отделит загрузку скриптов как от событий готовности DOM (DOMContentLoaded), так и от событий window.onload.

Если вы привыкли привязывать поведение к событиям готовности DOM/загрузки, предполагая, что это гарантирует загрузку всех скриптов (как это происходит с обычными тегами <script>), это теперь не так. Вместо этого, чтобы гарантировать загрузку скриптов до выполнения дополнительного кода, понадобится использовать механизмы обратного вызова загрузчика.

Кроме того, так как скрипт может выполняться после загрузки страницы, в скрипте нельзя использовать document.write(), в противном случае контент страницы будет стерт. Скрипты, в которых используется document.write(), не следует загружать с помощью динамического загрузчика скриптов.

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

2. Скрипты можно загрузить в любое время

Так как элементы script добавляются на страницу динамически, для скриптов может использоваться “ленивая загрузка” в любой точке жизненного цикла страниц, например, после временной задержки, или когда пользователь нажимает кнопку. Это может помочь распределить затраты на загрузку кода по времени жизни страницы.

3. Задайте зависимости скриптов

Загрузчики скриптов позволяют задать зависимости между скриптами. Синтаксис и подход к обработке зависимостей — это одно из основных различий между загрузчиками скриптов, но все они предлагают более надежное управление зависимостями по сравнению с обычными тегами <script>.

Почему бы просто не объединить файлы?

Прежде чем погрузиться в конкретные загрузчики, важно ответить на один конкретный вопрос, часто возникающий при обсуждении загрузчиков скриптов: “Зачем же нужен загрузчик скриптов, если я могу просто объединить все свои файлы вместе и загрузить один файл? Разве это будет плохо работать?”

Это сложный вопрос, но давайте коротко на нем остановимся. Если нужно более подробное обсуждение, Кайл рекомендует в качестве полного сообщения блога "Почему бы просто не объединить файлы?".

  1. Один файл 50 КБ может создавать меньше накладных расходов, чем два файла 25 КБ, но он также может загружаться вдвое дольше, так как браузер не сможет использовать преимущество параллельной загрузки. Но не следует и загружать пятьдесят файлов по 1 КБ — это было бы ужасно! Но параллельная загрузка 2-3 файлов может быть быстрее, чем загрузка одного большого файла. Просто попробуйте и посмотрите на результат.
  2. Один файл объемом 50 КБ кэшируется целиком. Пусть этот файл фактически состоит из 5 различных файлов по 10 КБ на сервере, 4 из которых достаточно стабильны, а один регулярно меняется (например, подстраивая версию UX веб-сайта). Затем при каждом изменении хотя бы одного символа в нестабильном файле всем пользователям придется полностью загружать новую копию единого объединенного файла объемом 50 КБ, несмотря на то, что более 40 КБ этого файла не менялись и, возможно, не изменятся. Возможно, файлы, чтобы минимизировать ненужную повторную загрузку, следует упаковывать вместе в зависимости от их нестабильности/частоты изменения?
  3. Некоторые файлы могут находиться в другом домене. Часто люди загружают скрипты из удаленных сетей доставки контента (Content Delivery Network, CDN), например из библиотек JavaScript. Размещение этих файлов в своем домене просто для того, чтобы объединить их, противоречит стремлению к повышению производительности, так как вместо широких труб Google для них будут использоваться собственные каналы ограниченной пропускной способности.

Вывод: правильным ответом не всегда является один файл. Загрузка меньшего числа файлов обычно лучше загрузки большего числа файлов. Для многих веб-сайтов оптимальной будет загрузка от 1 до 3 скриптов. Все веб-сайты/приложения будут слегка отличаться друг от друга, поэтому нужно просто поэкспериментировать и посмотреть на результаты. Вполне вероятно, что при внимательном анализе вы выберете несколько скриптов.

Даже если действительно используется только один скрипт, использование загрузчика скриптов все равно выгодно, так как загрузка одного большого слоноподобного файла может выполняться параллельно с другими ресурсами страницы. Только оно это может заметно увеличить быстродействие веб-сайта. Попробуйте, и все получится!

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

LABjs

Средство LABjs (Loading And Blocking JavaScript, загрузка и блокировка JavaScript) с самого начала создавалось как удобный загрузчик скриптов. Под “удобством” я подразумеваю, что основной целью является максимальная простота использования при выполнении поставленных функциональных целей. Я стремился сделать LABjs доступным большинству веб-сайтов — то есть средством, с помощью которого даже владельцы веб-сайтов с минимальными навыками смогут осуществить загрузку скриптов LABjs, обладая очень малыми знаниями JavaScript.

На ранних стадиях разработки LABjs практически все, кто проверял и испытывал это решение, замечали, что оно резко улучшало быстродействие Интернета (то есть оно уменьшало время загрузки страниц, ускоряя загрузку ресурсов JavaScript). Я быстро понял, что главной функциональной целью LABjs была и должна быть следующая: добиться максимально возможной оптимизации в загрузке скриптов, выполняемой единообразно для всех браузеров.

Поэтому LABjs — это повышающий быстродействие загрузчик скриптов, по замыслу и по функционалу. При дальнейшем продвижении важно понимать это стремление.

Выше мы рассмотрели, как элементы тегов <script> в HTML-разметке будут блокировать загрузку других ресурсов страницы, таким образом, ухудшая быстродействие. Но мы отметили, что динамическая загрузка скриптов эффективно справляется с этим узким местом. За этим решением скрывается динамическая вставка элементов script в DOM. Когда браузер обнаруживает такое событие, он все равно будет загружать и выполнять этот ресурс, но во время этих действий работа браузера над оставшейся частью страницы не прекращается. Основным преимуществом является возможность указания множества скриптов для одновременной загрузки, и по возможности браузер будет загружать их параллельно, что значительно сокращает общее время загрузки.

Проблема заключается в том, что при попытке динамической загрузки двух и более скриптов одновременно многие браузеры не гарантируют нужный порядок их выполнения. Это означает, что при наличии зависимостей между двумя скриптами тут же возникает состояние конкуренции, из-за которой скрипты могут выполняться не в правильном/планируемом порядке.

Поэтому моя цель в работе над LABjs состояла в создании замены тега <script>, которая выглядела бы максимально похожей на тег <script>, но решала проблемы с производительностью, используя динамическую загрузку указанного скрипта. Но при этом также важна возможность задать порядок выполнения для зависимостей, чтобы инструмент LABjs “отслеживал” порядок выполнения, но при этом мог загружать все скрипты параллельно для оптимизации производительности.

Такой набор требований очень трудно сбалансировать в одном интерфейсе API, но интерфейс API LABjs представляет собой мою лучшую попытку решить эту задачу. Типичный пример использования нескольких тегов script приведен в примере кода 2, а эквивалентная загрузка, выполняемая с помощью синтаксиса LABjs, показана в примере кода 3.

<html>
<head>
<script src="script1.js"></script>
<script src="script2-a.js"></script>
<script src="script2-b.js"></script>
<script type="text/javascript">
    initScript1();
    initScript2();
</script>
<script src="script3.js"></script>
<script type="text/javascript">
    initScript3();
</script>
</head>
<body>
...
</body>
</html>

Пример кода 2. Обычные теги <script>

<html>
<head>
<script src="LAB.js"></script>
<script type="text/javascript">
$LAB
.script("script1.js").wait()
.script("script2-a.js")
.script("script2-b.js")
.wait(function(){
    initScript1();
    initScript2();
})
.script("script3.js")
.wait(function(){
    initScript3();
});
</script>
</head>
<body>
...
</body>
</html>

Пример кода 3. Синтаксис LABjs для загрузки скриптов

Как можно видеть, синтаксис LABjs пытается быть максимально близким к заменяемой разметке, позволяя веб-разработчикам преобразовать существующую разметку в загрузку скриптов, оптимизированную для быстродействия. На рис. 3 показано повышение быстродействия, достигнутое благодаря использованию LABjs, по сравнению с загрузкой при использовании обычных тегов <script>. Обратите внимание, что при этом сократилось не только время загрузки страницы с 16,84 до 6,24 секунды (в 2,7 раза!), но и время готовности модели DOM страницы (DOM-ready, точка в которой страница готова  использованию пользователями) примерно с 10 до 0,4 секунды! Это заметные улучшения быстродействия, как реальные, так и воспринимаемые.


Рис. 3. Улучшения быстродействия благодаря LABjs

Примечания к интерфейсу API LABjs

  1. $LAB всегда запускает цепочку LABjs, определенную как все вызовы .script() и .wait(), связанные друг с другом, и первоначальную ссылку $LAB. Можно создать любое нужное число независимых цепочек $LAB, и они будут работать полностью независимо. Если нужно задать зависимости между двумя скриптами, для которых интерфейс LABjs должен обеспечить порядок выполнения, эти два скрипта должны быть заданы в одной цепочке $LAB. С другой стороны, если при использовании полностью независимых скриптов нужно разделить их, чтобы предотвратить любые перекрестные влияния, возможным решением являются независимые цепочки $LAB.
  2. .script(…) может получать любое число строковых параметров (представляющих относительные или абсолютные URL-адреса ресурсов скриптов). URL-адреса могут указывать на любые местоположения, в домене страницы или в другом домене. Например, LABjs можно использовать для загрузки jQuery из Google CDN и для загрузки Google Analytics с серверов GA, а также для загрузки множества других скриптов с пользовательского сервера.
  3. .script(…) также может принимать массив в качестве любого параметра (обрабатывать его в точности ожидаемым образом), либо он может принимать объект с парами "ключ/значение". Синтаксис параметров объектов позволяет управлять отдельными аспектами конкретной загрузки скриптов, включая параметр “тип”, необходимость двойной сверки источника скрипта с предыдущими загрузками и т. д.
  4. .wait() без параметров используется, чтобы подать цепочке $LAB сигнал о том, что она должна обеспечить фиксированный порядок выполнения. Другими словами, все скрипты перед вызовом .wait() должны быть выполнены (в произвольном порядке) до любого из скриптов после .wait(). И снова, .wait() никак не повлияет на параллельную загрузку, контролируя только порядок выполнения цепочки.
  5. .wait(…) с переданной в качестве параметра функцией (или ссылкой на функцию) — это то же самое, что и пустой вызов .wait() с тем исключением, что переданная функция рассматривается как обычный встроенный код тега <script> (см. пример кода 3). Это называется “связыванием”, которое используется с целью привязки фрагмента встроенного скрипта для выполнения в нужном порядке оставшейся части цепочки.

По умолчанию при использовании LABjs все скрипты загружаются параллельно (включая “предварительные загрузки”, если в цепочке есть вызовы .wait()) и выполняются как можно быстрее, если иное не указано вызовом .wait(). Но есть ряд параметров настройки, позволяющих управлять поведением. Эти параметры могут быть установлены либо глобально (влияя на все цепочки $LAB на странице), либо для цепочки $LAB (влияя только на эту цепочку). Пример задания глобальных настроек и настроек для цепочки см. в примере кода 4.

<html>
<head>
<script src="LAB.js"></script>
<script type="text/javascript">
$LAB.setGlobalDefaults({BasePath:"/some/path/"});
$LAB.setOptions({AlwaysPreserveOrder:true})
.script("script1.js")
.script("script2-a.js")
.script("script2-b.js")
.wait(function(){
    initScript1();
    initScript2();
});
$LAB
.script("script3.js")
.script("script4.js")
.wait(function(){
    initScript3();
    initScript4();
});
</script>
</head>
<body>
...
</body>
</html>

Пример кода 4. Задание настроек по умолчанию $LAB, глобально и для цепочки

Список параметров настройки и их назначение см. в документации на интерфейс API.

Дополнительные возможности LABjs

Мне часто задают вопрос: “Что, если мне понадобится определить, какие скрипты будут загружаться, во время выполнения в зависимости от некоторого условия?” Этот и похожие на него вопросы убедили меня задокументировать здесь (в очень общем виде) способ, который можно использовать для выжимания дополнительных возможностей из интерфейса API цепочек, подобного LABjs.

Во-первых, $LAB.script(…) может принимать в качестве параметра массив, позволяя программно создать массив в переменной, а затем передать его в один вызов .script(). Но у этого подхода есть ограничение — он не позволяет точно вставлять в цепочку вызовы .wait(). К счастью, есть способ и для тех, кому нужна такая возможность!

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

Это означает, что можно программно создать массив элементов, концептуально представляющий отдельные вызовы цепочки $LAB, а затем имитировать вызовы цепочки, выполняя вызов функции и сохраняя возвращенное значение как объект, используемый для выполнения следующего вызова функции. Пример см. в примере кода 5.

<html>
<head>
<script src="LAB.js"></script>
<script type="text/javascript">
var _queue = ["script1.js",null], $L = $LAB;
</script>
 
...
</head>
<body>
<script type="text/javascript">
if (something) { // скрипт 2 готовится к загрузке
    _queue.push("script2-a.js","script2-b.js",function(){
        initScript1();
        initScript2();
    });
}
else {    // скрипт 2 на будет загружаться, поэтому просто запускается script-1
    _queue.push(function(){
        initScript1();
    });
}
 
</script>
 
...
 
<script type="text/javascript">
if (somethingElse) {
    _queue.push("script-3.js",function(){
        initScript3();
    });
}
</script>
 
...
 
<script type="text/javascript">
 
for (var i=0, len=_queue.length; i<len; i++) {
    if (typeof _queue[i] == "string") { // найден источник строки скрипта
        $L = $L.script(_queue[i]);
    }
    else if (!_queue[i]) { // найдено условие null/false
        $L = $L.wait();
    }
    else if (typeof _queue[i] == "function") { // найдена встроенная функция
        $L = $L.wait(_queue[i]);
    }
}
 
</script>
 
</body>
</html>

Пример кода 5. Имитация цепочки $LAB с помощью цикла for-loop

Развитие LABjs

В настоящее время (и в основном) LABjs представляет собой средство загрузки скриптов, оптимизирующее производительность на стороне клиента. В нем используется знание автором дерева зависимостей этих скриптов, позволяющего создать правильную цепочку $LAB. Этот тип использования действительно хорошо соответствует большинству обычных веб-сайтов, в которых уже используются теги <script>, но он не очень подходит для сложных сред разработки с десятками взаимосвязанных модулей, вложенными зависимостями и т. п.

Хотя подобная среда не является целевой областью использования LABjs, это не означает, что в этом случае использование LABjs окажется бесполезным. Как всегда, основной задачей в подобном проекте является разработка серверного компонента для LABjs, который может помочь в решении этих более сложных задач. Серверный компонент LABjs сам по себе написан на JavaScript, с надеждой/целью возможности его выполнения практически во всех средах JavaScript на стороне сервера, таких как Node.js, Narwhal и т. д.

По плану модуль LABjs на стороне сервера должен уметь выполнять постобработку (либо во время выполнения, либо во время создания) HTML-файлов и файлов скриптов, чтобы автоматически определить дерево/граф зависимостей и создать оптимальные цепочки LABjs для этих ресурсов. Он будет учитывать всю вышеупомянутую логику балансировки, включая объединение файлов (но не всегда в один файл). Например, серверный компонент LABjs сможет обнаруживать, какие файлы изменяются чаще других, и использовать эти сведения для упаковки решений.

Цель серверного компонента LABjs — дополнить загрузку на стороне клиента, предоставляя механизм автоматической оптимизации использования LABjs для создания точной среды. Фактически использования загрузчика на стороне клиента можно будет избежать там, где это доступно. В этом случае оптимизацию загрузки ресурсов скриптов в веб-сайте/приложении всегда можно будет доверить серверному компоненту.

Другим следствием этого подхода станет возможность управлять сложными иерархиями скриптов/модулей, даже со вложенными зависимостями, "объявленными" непосредственно в файлах скриптов, и не заботиться об отслеживании того, как модуль LABjs может и должен загружать эти файлы. Серверный компонент LABjs выполнит всю эту рутину за вас! Теперь все будет оптимизировано!

$LAB.summary()

Итог: если нужно загрузить любой скрипт, из любого места, в любой точке времени жизни страницы, и выполнить это максимально оптимальным образом, одновременно обеспечивая выполнение необходимого порядка зависимостей, LABjs, вероятно, будет одним из самых мощных из доступных решений. Оптимизированная загрузка — это как раз то, для чего средство LABjs и предназначено. Помните, это повышающий быстродействие загрузчик скриптов.

RequireJS

Корни RequireJS лежат в Dojo и CommonJS, поэтому это средство поощряет считать скрипты модулями. Чем скрипт отличается от модуля? Обычно скрипт — это просто файл JavaScript, который может добавлять что-то в глобальное пространство имен и может содержать подразумеваемые зависимости. Модули пытаются ограничить свое влияние на глобальное пространство имен и более явно объявляют о своих прямых зависимостях.

Я надеялся создать загрузчик скриптов, который позволил бы лучше реализовывать включение модулей и работал бы непосредственно с модулями CommonJS. Но группа CommonJS выбрала для своих модулей формат, предполагающий синхронную загрузку модулей с локального диска. Хотя он позволяет выполнять синхронную загрузку скриптов в браузер с помощью XMLHttpRequest, он не так хорошо работает, как код, загруженный с помощью элементов script, его тяжелее отлаживать и при его использовании тяжелее выполнять междоменные запросы скриптов, например, при загрузке скриптов из CDN. Кроме того, у меня есть ряд вопросов к формату модулей CommonJS.

RequireJS фокусируется на предоставлении браузеру дружественного к тегам <script> формата загрузки модулей, который пытается соответствовать целям CommonJS, но хорошо работает в браузере. Хотя RequireJS предоставляет отличные средства создания модульного Javascript, это решение также поддерживает загрузку современных скриптов для браузеров. RequireJS также поставляется с инструментом оптимизации, позволяющим объединять и группировать скрипты в набор уменьшенных скриптов, который меньше по размеру и быстро загружается.

В примере кода 6 показан основной формат загрузки скриптов с помощью RequireJS.

<html>
<head>
<script src="require.js"></script>
<script type="text/javascript">
//Скрипты будут загружаться параллельно
//и асинхронно, и они могут быть выполнены
//в порядке, отличающемся от приведенного ниже.
require([
        "script1.js",
        "script2-a.js",
        "script2-b.js",
        "script3.js"
    ],
    function(){
        //Эта функция вызывается после того, как все скрипты
        //загружены и выполнены.
        initScript1();
        initScript2();
        initScript3();
    }
);
</script>
</head>
<body>
...
</body>
</html>

Пример кода 6. Синтаксис RequireJS для первого примера LABjs

В то время как LABjs поддерживает принудительный порядок выполнения, вставляя вызовы .wait() в цепочку, RequireJS исходно не поддерживает выполнение асинхронно загруженных скриптов в заданном порядке. Но благодаря усилиям Алекса Секстона (Alex Sexton), начиная с RequireJS 0.12, подключаемый модуль order предлагает базовую поддержку этой возможности.

В RequireJS используется архитектура подключаемых модулей, позволяющая добавлять новые типы загрузки или зависимостей. Существуют подключаемые модули для пакетов строк i18n, загружающие и текстовые файлы (которые могут быть встроены с помощью средства оптимизации), рассматривающие службу JSONP как зависимость и обеспечивающие порядок выполнения зависимостей.

Подключаемый модуль order использует те же приемы, что и LABjs: тип=”script/cache” имитирует тип MIME параллельной загрузки скриптов, но позволяет вручную выполнять их в нужном порядке.

Нужно отметить, что этот прием нужен/эффективен только в IE, Chrome и Safari. Для Firefox/Gecko и Opera этот прием не работает, но, к счастью, этого и не требуется, так как эти браузеры автоматически гарантируют правильный порядок выполнения даже для динамически загруженных элементов script.

Подключаемые модули отличаются от имени ресурса восклицательным знаком, как показано в примере кода 7.

<html>
<head>
<script src="require.js"></script>
<script type="text/javascript">
//Так как использован подключаемый модуль order,
//Скрипты будут загружаться параллельно
//и асинхронно, но выполняться
//в заданном ниже порядке.
require([
        "order!script1.js",
        "order!script2-a.js",
        "order!script2-b.js",
        "order!script3.js"
    ],
    function(){
        //Эта функция вызывается после того, как все скрипты
        //загружены и выполнены.
        initScript1();
        initScript2();
        initScript3();
    }
);
</script>
</head>
<body>
...
</body>
</html>

Пример кода 7. Использование подключаемого модуля order для обеспечения порядка выполнения скриптов

Модули RequireJS

RequireJS поощряет использование модулей. Модули в RequireJS определяются с помощью вызова функции require.def(), в которой задаются имя модуля, его зависимости и обратный вызов функции. Обратный вызов функции выполняется один раз, когда зависимости готовы, и используется для определения модуля. Если в зависимости также используется вызов require.def() для определения самой зависимости, то можно напрямую обработать зависимость в обратном вызове функции.

require.def("lamp", ["Light"], function (Light) {
    //Эта функция вызывается после того, модуль зависимости
    //"Light" загружен и определен. Аргумент
    //функции, Light, будет ссылкой на модуль Light.
    //Можно также использовать require("Light") внутри
    //этой функции для получения модуля Light.
 
    //Значение, возвращаемое этой функцией
    //будет определением модуля "lamp".
    return {
        light: new Light(),
        on: function () {
            this.light.on();
        },
        off: function () {
            this.light.off();
        }
    }
});

Пример кода 8. Определение модуля с помощью require.def()

Обратите внимание, что имя модуля и его зависимости не содержат суффикс “.js”. URL-адреса .js можно использовать напрямую для загрузки скриптов, не участвующих в определении модуля с четкой областью применения. Но с элементами, определяющими модули с помощью require.def(), всегда следует использовать для зависимости то же самое имя, которое используется для зависимости в первом аргументе соответствующего вызова require.def(). Для загрузки скрипта RequireJS преобразует имя “lamp” в “путь/к/lamp.js”.

Модули RequireJS могут не экспортировать значения в глобальное пространство имен, и, так как для ссылок на все зависимости применяются строковые значения, на странице можно использовать несколько версий модуля.

Формат модулей RequireJS аналогичен формату модулей CommonJS, и в RequireJS предусмотрен запускаемый из командной строки скрипт, который можно использовать для преобразования модулей CommonJS в синтаксис RequireJS, что позволяет использовать модули CommonJS в среде на основе браузера. При наличии серверного процесса, который может упаковать модули CommonJS в формат CommonJS Transport D, такого как Transporter Криса Зипа (Kris Zyp), RequireJS позволяет использовать этот код напрямую с помощью адаптера RequireJS Transport D.

С использованием модулей сценарии в нашем основном примере могут быть написаны следующим образом:

//Содержание script1.js:
require.def("script1", function () {
    //Выполните здесь действия initScript1()
    //var script1 = ...;
    return script1;
});
 
//Содержание script2-a.js:
require.def("script2-a", ["script1"], function (script1) {
    //Выполните здесь действия initScript2(), касающиеся только script2-a.
    //При необходимости используйте script1.
    return {};
});
 
//Содержание script2-b.js:
require.def("script2-b", ["script1", "script2-a"],
    function (script1, script2a) {
        //Выполните здесь действия initScript2(), касающиеся только script2-b.
        //При необходимости используйте script1 и script2a.
        return {};
    }
);
 
//Содержание script3.js:
require.def("script3", ["script1", "script2-a", "script2-b"],
    function (script1, script2a, script2b) {
        //Выполните здесь initScript3(). При необходимости
        //используйте script1, script2a и script2b.
        return {};
    }
);

Пример кода 9. Примеры скриптов как модулей RequireJS

Средство оптимизации RequireJS

Средство оптимизации RequireJS упрощает объединение скриптов, и RequireJS можно настроить для заблаговременной приоритетной загрузки этих объединенных слоев скриптов, таким образом, предоставляя максимальное быстродействие и одновременно сохраняя небольшие размеры и модульность источников JavaScript.

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

Если с помощью средства версии script2-a.js иscript2-b.js, определяемые с помощью require.def(), объединены в слой с именем script2layer.js, то в примере кода 10 показано код HTML, обеспечивающий максимально быстрое выполнение кода, в котором используется три запроса скриптов.

<html>
<head>
<script src="require.js"></script>
<script type="text/javascript">
//Можно передать объект конфигурации в качестве первого
//необходимого аргумента. "priority" указывает загрузчику
//загрузить сначала этот набор скриптов, параллельно
//и асинхронно, перед трассировкой других зависимостей.
require({
        priority: ["script1", "script2layer", "script3"]
    },
    ["script1", "script2-a", "script2-b", "script3"],
    function(){
        //Выполните здесь с помощью модулей
        //все нужные действия на уровне страницы
    })
;
</script>
</head>
<body>
...
</body>
</html>

Пример кода 10. Примеры скриптов как модулей RequireJS

Используя средство оптимизации с параметром конфигурации priority, затем можно поэкспериментировать с правильным количеством загружаемых скриптов, обеспечивающим оптимальную производительность. Так как require() можно вызвать в любой момент жизненного цикла страницы, можно отложить загрузку некоторых скриптов до последующего действия пользователей.

Сравнение LABjs и RequireJS

LABjs лучше всего работает на страницах с современными скриптами, которые нужно эффективно загружать в заданном порядке, а все зависимости которых могут быть отслежены на верхнем уровне. Эффективность LABjs обеспечивается параллельной загрузкой всех скриптов, независимо от их месторасположения, и выполнением их в соответствующем порядке зависимостей. Чтобы упростить управление зависимостями предполагается разработать серверный компонент LABjs. После уменьшения и сжатия размер LABjs составляет ~2,2 КБ.

RequireJS позволяет эффективно загружать современные скрипты, но стимулирует применение модульного подхода с общим кодом CommonJS и разрешением вложенных зависимостей. Встроенное средство оптимизации может уменьшить количество загружаемых скриптов и уменьшить размер шрифтов для дальнейшего улучшения быстродействия. RequireJS позволяет повторно использовать созданные модули во многих средах JS. RequireJS можно развернуть несколькими способами: от самой базовой версии объемом 2,9 КБ, не поддерживающей подключаемые модули, использование нескольких версий и готовность страницы, до версии deluxe объемом 6 КБ, включающей поддержку подключаемых модулей, использования нескольких версий и готовности страницы, а также подключаемые модули text, i18n, JSONP и order. Все размеры указаны после уменьшения и сжатия с помощью gzip.

Независимо от конкретных потребностей, использование любого загрузчика вместо обычных тегов <script> почти наверняка улучшит работу не только веб-сайта/приложения, но и конечных пользователей.