Почему веб-модули?
- Проблема
- Решение
- API загрузки скриптов
- Асинхронность против синхронности
- Загрузка скриптов: XHR
- Загрузка скриптов: Веб-рабочие потоки
- Загрузка скриптов: document.write()
- Загрузка скриптов: head.appendChild(script)
- Обертывание функций
На этой странице обсуждается, почему модули на веб-страницах полезны и какие механизмы можно использовать сегодня для их включения. На отдельной странице рассматриваются причины проектирования для конкретного формата обернутой функции, используемого RequireJS.
Проблема
- Веб-сайты превращаются в веб-приложения
- Сложность кода растет по мере увеличения сайта
- Сборка становится сложнее
- Разработчик хочет отдельные JS-файлы/модули
- Развертывание требует оптимизированного кода в одном или нескольких HTTP-запросах
Решение
Фронтенд-разработчикам нужно решение с:
- Неким аналогом #include/import/require
- Возможностью загрузки вложенных зависимостей
- Простотой использования для разработчика, но при этом поддержкой инструмента оптимизации, помогающего при развертывании
API загрузки скриптов
В первую очередь необходимо решить вопрос с API загрузки скриптов. Вот несколько кандидатов:
- Dojo: dojo.require("some.module")
- LABjs: $LAB.script("some/module.js")
- CommonJS: require("some/module")
Все они отображаются как загрузка some/path/some/module.js. В идеале мы могли бы выбрать синтаксис CommonJS, поскольку он, вероятно, станет более распространённым со временем, и мы хотим повторно использовать код.
Мы также хотим некий синтаксис, позволяющий загружать обычные файлы JavaScript, которые существуют сегодня — разработчик не должен переписывать весь свой JavaScript, чтобы получить преимущества загрузки скриптов.
Однако нам нужно что-то, что хорошо работает в браузере. CommonJS require() — это синхронный вызов, ожидается, что он сразу вернёт модуль. Это не работает в браузере.
Асинхронность против синхронности
Этот пример должен проиллюстрировать основную проблему для браузера. Предположим, у нас есть объект Employee, и мы хотим, чтобы объект Manager наследовался от объекта Employee. Принимая этот пример, мы можем закодировать его так, используя наш API загрузки скриптов:
var Employee = require("types/Employee");
function Manager () {
this.reports = [];
}
//Error if require call is async
Manager.prototype = new Employee();
Как указывает комментарий выше, если require() асинхронный, этот код не будет работать. Однако синхронная загрузка скриптов в браузере убивает производительность. Что делать?
Загрузка скриптов: XHR
Заманчиво использовать XMLHttpRequest (XHR) для загрузки скриптов. Если используется XHR, то мы можем изменить текст выше — мы можем использовать регулярное выражение для поиска вызовов require(), убедиться, что мы загружаем эти скрипты, а затем использовать eval() или элементы script, у которых текст тела установлен на текст скрипта, загруженного через XHR.
Использование eval() для оценки модулей — плохая практика:
- Разработчикам объяснили, что eval() — это плохо.
- В некоторых средах eval() запрещен.
- Отладка сложнее. Firebug и инспектор WebKit имеют соглашение //@ sourceURL=, которое помогает дать имя выполняемому тексту, но эта поддержка не является универсальной для всех браузеров.
- Контекст eval() различается в разных браузерах. Вы можете использовать execScript в IE, чтобы помочь в этом, но это означает больше движущихся частей.
Использование тегов script с заданным текстом тела — плохая практика:
- Во время отладки номер строки ошибки не соответствует исходному файлу.
XHR также имеет проблемы с междоменными запросами. Некоторые браузеры теперь поддерживают междоменные XHR, но это не универсально, а IE решил создать отдельный объект API для междоменных вызовов — XDomainRequest. Больше движущихся частей и больше вещей, которые могут пойти не так. В частности, необходимо убедиться, что не отправляются никакие нестандартные HTTP-заголовки, в противном случае может быть выполнен ещё один запрос «предварительной проверки», чтобы убедиться, что доступ к междоменным ресурсам разрешен.
Dojo использовал XHR-базовый загрузчик с eval(), и, хотя он работает, он стал источником разочарования для разработчиков. Dojo имеет загрузчик xdomain, но он требует модификации модулей с помощью шага сборки, чтобы использовать оберточную функцию, так что теги script src="" могут быть использованы для загрузки модулей. Существует много крайних случаев и движущихся частей, которые создают нагрузку на разработчика.
Если мы создаём новый загрузчик скриптов, мы можем сделать лучше.
Загрузка скриптов: Веб-рабочие потоки
Веб-рабочие потоки могут быть ещё одним способом загрузки скриптов, но:
- Он не имеет сильной кросс-браузерной поддержки
- Это API обмена сообщениями, и скрипты, вероятно, захотят взаимодействовать с DOM, поэтому это означает просто использование рабочего потока для извлечения текста скрипта, но передача текста обратно в основное окно, а затем использование eval/script с текстовым телом для выполнения скрипта. Это имеет все те же проблемы, что и XHR, упомянутые выше.
Загрузка скриптов: document.write()
document.write() можно использовать для загрузки скриптов — он может загружать скрипты с других доменов, и он соответствует тому, как браузеры обычно потребляют скрипты, поэтому он позволяет легко отлаживать.
Однако в примере Асинхронность против синхронности мы не можем просто выполнить этот скрипт напрямую. В идеале мы могли бы узнать зависимости require() до выполнения скрипта и убедиться, что эти зависимости загружены первыми. Но мы не имеем доступа к скрипту до его выполнения.
Кроме того, document.write() не работает после загрузки страницы. Отличный способ повысить видимую производительность вашего сайта — это загружать код по требованию, когда пользователь его нуждается для следующего действия.
Наконец, скрипты, загруженные с помощью document.write(), будут блокировать отображение страницы. При поиске наилучшей производительности вашего сайта это нежелательно.
Загрузка скриптов: head.appendChild(script)
Мы можем создавать скрипты по требованию и добавлять их в head:
var head = document.getElementsByTagName('head')[0],
script = document.createElement('script');
script.src = url;
head.appendChild(script);
Существует немного больше деталей, чем просто приведенный выше фрагмент, но это основная идея. Этот подход имеет преимущество перед document.write(), так как он не будет блокировать отображение страницы и работает после загрузки страницы.
Однако он по-прежнему имеет проблему Асинхронность против синхронности: в идеале мы могли бы узнать зависимости require() до выполнения скрипта и убедиться, что эти зависимости загружены первыми.
Обертывание функций
Таким образом, нам нужно знать зависимости и убедиться, что мы их загружаем перед выполнением нашего скрипта. Лучший способ сделать это — создать наш API загрузки модулей с функциями-обёртками. Вот так:
define(
//The name of this module
"types/Manager",
//The array of dependencies
["types/Employee"],
//The function to execute when all dependencies have loaded. The
//arguments to this function are the array of dependencies mentioned
//above.
function (Employee) {
function Manager () {
this.reports = [];
}
//This will now work
Manager.prototype = new Employee();
//return the Manager constructor function so it can be used by
//other modules.
return Manager;
}
);
И это синтаксис, используемый RequireJS. Также есть упрощённый синтаксис, если вы хотите просто загрузить некоторые обычные файлы JavaScript, которые не определяют модули:
require(["some/script.js"], function() {
//This function is called after some/script.js has loaded.
});
Этот тип синтаксиса был выбран, потому что он лаконичен и позволяет загрузчику использовать загрузку типа head.appendChild(script).
Он отличается от обычного синтаксиса CommonJS из-за необходимости работы в браузере. Были предложения использовать обычный синтаксис CommonJS с загрузкой типа head.appendChild(script), если процесс сервера преобразует модули в формат передачи с функцией-обёрткой.
Я считаю, что важно не принуждать к использованию процесса сервера во время выполнения для преобразования кода:
- Это делает отладку странной, номера строк будут отличаться от исходного файла, поскольку сервер вставляет функцию-обёртку.
- Это требует большего количества компонентов. Разработка фронтенда должна быть возможна с помощью статических файлов.
Более подробную информацию о причинах проектирования и вариантах использования этого формата обертывания функций, называемого Asynchronous Module Definition (AMD), можно найти на странице Почему AMD?
© jQuery Foundation and other contributors
Licensed under the MIT License.
http://requirejs.org/docs/why.html