Почему AMD?
- Цели модуля
- Современный веб
- CommonJS
- AMD
- Определение модуля
- Именованные модули
- Упрощения
- Совместимость с CommonJS
- AMD в настоящее время
- Что вы можете сделать
Эта страница рассказывает о силах дизайна и использовании API асинхронного определения модуля (AMD) для JavaScript-модулей, API модуля, поддерживаемого RequireJS. Есть отдельная страница, посвященная общему подходу к модулям в веб.
Цели модуля
Что такое JavaScript-модули? В чем их назначение?
- Определение: как инкапсулировать фрагмент кода в полезную единицу и как зарегистрировать его возможности/экспортировать значение для модуля.
- Ссылки на зависимости: как ссылаться на другие блоки кода.
Современный веб
(function () {
var $ = this.jQuery;
this.myExample = function () {};
}());
Как определяются фрагменты JavaScript-кода сегодня?
- Определены через немедленно выполняемую фабричную функцию.
- Ссылки на зависимости выполняются через имена глобальных переменных, загруженных через тег HTML script.
- Зависимости указаны очень слабо: разработчик должен знать правильный порядок зависимостей. Например, файл, содержащий Backbone, не может предшествовать тегу jQuery.
- Требуется дополнительное средство разработки для замены набора тегов script одним тегом для оптимизированного развертывания.
Это может быть сложно управлять на крупных проектах, особенно когда скрипты начинают иметь много зависимостей, которые могут перекрываться и вкладываться. Ручное написание тегов script не очень масштабируется, и это исключает возможность загрузки скриптов по требованию.
CommonJS
var $ = require('jquery');
exports.myExample = function () {};
Участники оригинального списка CommonJS (CJS) решили разработать формат модулей, который работал бы с современным языком JavaScript, но не обязательно был бы ограничен ограничениями среды браузерного JS. Надежды заключались в использовании некоторых временных мер в браузере и, в идеале, повлиять на разработчиков браузеров, чтобы они реализовали решения, которые позволили бы их формату модулей лучше работать в родном виде. Временные меры:
- Использовать сервер для преобразования CJS-модулей в нечто пригодное для использования в браузере.
- Или использовать XMLHttpRequest (XHR) для загрузки текста модулей и выполнения преобразований/парсинга текста в браузере.
Формат CJS-модуля разрешал только один модуль на файл, поэтому для объединения нескольких модулей в один файл для целей оптимизации/объединения использовался «транспортный формат».
При этом группе CommonJS удалось разработать ссылки на зависимости и способы обработки циклических зависимостей, а также получения некоторых свойств текущего модуля. Однако они не полностью одобрили некоторые аспекты среды браузера, которые нельзя изменить, но которые по-прежнему влияют на дизайн модуля:
- загрузка через сеть
- присущая асинхронность
Это также означало, что на веб-разработчиков возлагалась большая ответственность за реализацию формата, а временные меры означали худшие возможности отладки. Отладка на основе eval или отладка нескольких файлов, конкатенированных в один файл, имеет практические недостатки. Эти недостатки могут быть устранены в инструментах браузера в будущем, но конечный результат: использование CommonJS-модулей в самых распространенных средах JS, браузере, в настоящее время не является оптимальным.
AMD
define(['jquery'] , function ($) {
return function () {};
});
Формат AMD возник из желания получить формат модулей, который был бы лучше, чем нынешняя практика «написать кучу тегов script с неявными зависимостями, которые нужно вручную упорядочить», и что было легко использовать непосредственно в браузере. Нечто с хорошими характеристиками отладки, не требующее специфических для сервера инструментов для начала работы. Он вырос из реального опыта Dojo с использованием XHR + eval и стремления избежать его недостатков в будущем.
Это улучшение по сравнению с текущей веб-практикой «глобальные переменные и теги script», потому что:
- Использует практику CommonJS использования строковых идентификаторов для зависимостей. Ясное объявление зависимостей и избегание использования глобальных переменных.
- Идентификаторы могут быть сопоставлены с различными путями. Это позволяет заменять реализации. Это отлично подходит для создания макетов для тестирования. Для приведенного выше примера код просто ожидает нечто, что реализует API и поведение jQuery. Это не обязательно jQuery.
- Инкапсулирует определение модуля. Предоставляет инструменты для предотвращения загрязнения глобального пространства имен.
- Четкий путь к определению значения модуля. Использовать «return value;» или условное выражение CommonJS «exports», что может быть полезно для циклических зависимостей.
Это улучшение по сравнению с CommonJS-модулями, потому что:
- Он лучше работает в браузере, у него наименьшее количество проблем. Другие подходы имеют проблемы с отладкой, использованием разных доменов/CDN, использованием file:// и необходимостью инструментов, специфичных для сервера.
- Определяет способ включения нескольких модулей в один файл. В терминах CommonJS для этого используется термин «транспортный формат», и эта группа не договорилась о транспортном формате.
- Позволяет установить функцию в качестве возвращаемого значения. Это очень полезно для конструкторских функций. В CommonJS это более неудобно, всегда необходимо устанавливать свойство в объекте exports. Node поддерживает module.exports = function () {}, но это не часть спецификации CommonJS.
Определение модуля
Использование JavaScript-функций для инкапсуляции было задокументировано как шаблон модуля:
(function () {
this.myGlobal = function () {};
}());
Этот тип модуля полагается на добавление свойств к глобальному объекту для экспорта значения модуля, и с помощью этой модели трудно объявлять зависимости. Предполагается, что зависимости будут сразу доступны при выполнении этой функции. Это ограничивает стратегии загрузки зависимостей.
AMD решает эти проблемы, выполнив:
- Регистрирует фабричную функцию, вызвав define(), вместо немедленного выполнения.
- Передает зависимости в виде массива строковых значений, не захватывая глобальные переменные.
- Выполняет фабричную функцию только после загрузки и выполнения всех зависимостей.
- Передает зависимые модули в качестве аргументов фабричной функции.
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {
//Define the module value by returning a value.
return function () {};
});
Именованные модули
Обратите внимание, что приведенный выше модуль не объявляет имя для себя. Это делает модуль очень портативным. Это позволяет разработчику разместить модуль в другом пути, чтобы присвоить ему другой идентификатор/имя. Загрузчик AMD присвоит модулю идентификатор, основанный на том, как он упоминается другими скриптами.
Однако инструментам, которые объединяют несколько модулей вместе для повышения производительности, нужен способ присваивать имена каждому модулю в оптимизированном файле. Для этого AMD допускает строку в качестве первого аргумента define():
//Calling define with module ID, dependency array, and factory function
define('myModule', ['dep1', 'dep2'], function (dep1, dep2) {
//Define the module value by returning a value.
return function () {};
});
Вы должны избегать самостоятельного именования модулей и размещать только один модуль в файле во время разработки. Однако для инструментов и производительности решение модулей должно иметь способ идентификации модулей в созданных ресурсах.
Упрощения
Приведенный выше пример AMD работает во всех браузерах. Однако существует риск несовпадения имен зависимостей с именами аргументов функций, и это может выглядеть немного странно, если у вашего модуля много зависимостей:
define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
"oauth", "blade/jig", "blade/url", "dispatch", "accounts",
"storage", "services", "widgets/AccountPanel", "widgets/TabButton",
"widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
"jquery.textOverflow"],
function (require, $, object, fn, rdapi,
oauth, jig, url, dispatch, accounts,
storage, services, AccountPanel, TabButton,
AddAccount, less, osTheme) {
});
Чтобы упростить это и упростить простое обрамление CommonJS-модулей, поддерживается этот вид define, иногда называемый «упрощенным обрамлением CommonJS»:
define(function (require) {
var dependency1 = require('dependency1'),
dependency2 = require('dependency2');
return function () {};
});
Загрузчик AMD будет разбирать вызовы require('') с помощью Function.prototype.toString(), а затем внутренне преобразует вышеупомянутый вызов define в это:
define(['require', 'dependency1', 'dependency2'], function (require) {
var dependency1 = require('dependency1'),
dependency2 = require('dependency2');
return function () {};
});
Это позволяет загрузчику асинхронно загрузить dependency1 и dependency2, выполнить эти зависимости, а затем выполнить эту функцию.
Не все браузеры дают работоспособные результаты Function.prototype.toString(). По состоянию на октябрь 2011 года браузеры PS 3 и более старые версии Opera Mobile не поддерживают их. Эти браузеры, скорее всего, потребуют оптимизированной сборки модулей для ограничений сети/устройства, поэтому просто выполните сборку с помощью оптимизатора, который знает, как преобразовать эти файлы в нормализованный массив зависимостей, как в оптимизаторе RequireJS.
Поскольку количество браузеров, которые не могут поддерживать этот поиск toString(), очень мало, безопасно использовать этот упрощенный вид для всех ваших модулей, особенно если вы хотите выровнять имена зависимостей с переменными, которые будут содержать их значения модуля.
Совместимость с CommonJS
Несмотря на то, что этот упрощенный вид называется «упрощенным обрамлением CommonJS», он не полностью совместим с CommonJS-модулями. Однако случаи, которые не поддерживаются, скорее всего, поломаются в браузере, поскольку они обычно предполагают синхронную загрузку зависимостей.
Большинство CJS-модулей, около 95% по моим (совершенно ненаучным) личным наблюдениям, идеально совместимы с упрощенным обрамлением CommonJS.
Модули, которые выходят из строя, — это те, которые выполняют динамический расчет зависимости, все, что не использует строковый литерал для вызова require(), и все, что не выглядит как декларативный вызов require(). Так что такие вещи терпят неудачу:
//BAD
var mod = require(someCondition ? 'a' : 'b');
//BAD
if (someCondition) {
var a = require('a');
} else {
var a = require('a1');
}
Эти случаи обрабатываются обработчиком-require, require([moduleName], function (){}) обычно присутствующим в загрузчиках AMD.
Модель выполнения AMD лучше согласована с тем, как специфицируются модули ECMAScript Harmony. CommonJS-модули, которые не будут работать в обертке AMD, также не будут работать в качестве модуля Harmony. Поведение выполнения кода AMD более совместимо с будущими версиями.
Избыточность против полезности
Одним из критических замечаний по поводу AMD, по крайней мере, по сравнению с CJS-модулями, является то, что он требует определенного уровня вдавливания и обертывания функциями.
Но вот простая истина: предполагаемое дополнительное написание и уровень вдавливания для использования AMD не важны. Вот куда уходит ваше время при программировании:
- Размышления над проблемой.
- Чтение кода.
Большая часть вашего времени, затрачиваемого на кодирование, уходит на размышления, а не на написание. Хотя меньше слов обычно предпочтительнее, есть предел тому, как этот подход приносит пользу, и дополнительное написание в AMD не так уж много.
Большинство веб-разработчиков используют обертки функций, чтобы не загрязнять страницу глобальными переменными. Вид функции, обернутой вокруг функциональности, является очень распространенным явлением и не увеличивает стоимость чтения модуля.
Существуют также скрытые затраты в формате CommonJS:
- затраты на зависимость от инструментария
- крайние случаи, которые выходят из строя в браузерах, например, доступ к разным доменам
- худшая отладка, стоимость, которая продолжает увеличиваться со временем
AMD-модули требуют меньше инструментария, в них меньше проблем с крайними случаями и лучшая поддержка отладки.
Что важно: возможность фактически делиться кодом с другими. AMD — это путь наименьшего сопротивления к этой цели.
Иметь работающую, легко отлаживаемую систему модулей, которая работает в современных браузерах, означает получение реального опыта в создании лучшей системы модулей для JavaScript в будущем.
AMD и связанные с ним API помогли показать следующее для любой будущей системы модулей JS:
- Возврат функции в качестве значения модуля, особенно конструкторской функции, приводит к лучшему проектированию API. Node имеет module.exports для этого, но возможность использования "return function (){}" намного чище. Это означает, что не нужно получать доступ к "module" для выполнения module.exports, и это выражение кода более ясное.
- Динамическая загрузка кода (выполняется в системах AMD через require([], function (){})) является основным требованием. CJS обсуждал это, выдвинул некоторые предложения, но они не получили полного одобрения. Node не поддерживает эту потребность, вместо этого полагаясь на синхронное поведение require(''), что не переносимо в веб.
- Плагины загрузчика невероятно полезны. Это помогает избежать вложенных фигурных скобок, часто встречающихся в программировании на основе обратных вызовов.
- Селективное отображение одного модуля для загрузки из другого расположения упрощает предоставление эмуляторов объектов для тестирования.
- Для каждого модуля должно быть не более одного действия ввода-вывода, и оно должно быть простым. Веб-браузеры не терпят нескольких обращений к вводу-выводу для поиска модуля. Это противоречит нескольким поиском путей, которые делает Node сейчас, и избегает использования свойства package.json "main". Просто используйте имена модулей, которые легко сопоставляются с одним местоположением на основе расположения проекта, используя разумную конвенцию по умолчанию, которая не требует громоздкой конфигурации, но позволяет выполнять простую настройку при необходимости.
- В идеале должно быть "выборочное включение", которое можно использовать, чтобы код более старого JS мог участвовать в новой системе.
Если система модулей JS не может обеспечить указанные функции, это ставит ее в существенное невыгодное положение по сравнению с AMD и его связанными API вокруг callback-require, плагинов загрузчика и идентификаторов модулей на основе путей.
AMD в настоящее время
По состоянию на середину октября 2011 года AMD уже широко используется в сети:
- jQuery 1.7
- Dojo 1.7
- EmbedJS
- Ender-ассоциированные модули, такие как bonzo, qwery, bean и domready
- Используется Firebug 1.8+
- Упрощённый оболочка CommonJS может использоваться в Jetpack/Add-on SDK для Firefox
- Используется в частях сайтов BBC (наблюдалось по исходному коду, не является официальной рекомендацией AMD/RequireJS)
Что можно сделать
Если вы пишете приложения:
- Используйте оптимизатор RequireJS в командной строке или как HTTP-сервис с надстройкой AMD almond.
Если вы являетесь автором скрипта/библиотеки:
-
Вы можете вызвать define(), если оно доступно. Преимущество в том, что вы можете написать свою библиотеку, не полагаясь на AMD, а просто участвуете в ней, если она доступна. Это позволяет потребителям ваших модулей:
- избегать вывода глобальных переменных на странице
- использовать больше возможностей для загрузки кода, отложенной загрузки
- использовать существующие инструменты AMD для оптимизации своего проекта
- участвовать в работоспособной системе модулей JS в браузере сегодня.
Если вы пишете загрузчики кода/движки/среды для JavaScript:
- Реализуйте API AMD. Существует список рассылки и тесты совместимости. Реализация AMD позволит уменьшить объем кода для систем с несколькими модулями и помочь в доказательстве работоспособности системы модулей JavaScript в сети. Это может быть использовано для улучшения поддержки нативных модулей в ECMAScript.
- Также поддерживайте callback-require и плагины загрузчика. Плагины загрузчика — это отличный способ уменьшить эффект вложенных обратных вызовов, который может встречаться в коде на основе обратных вызовов/асинхронных действий.
© jQuery Foundation and other contributors
Licensed under the MIT License.
http://requirejs.org/docs/whyamd.html