LABjs и RequireJS: загрузка ресурсов JavaScript интересным способом
Кайл Симпсон (Kyle Simpson), Джеймс Берк (James Burke) | 18 августа 2010 г. С загрузкой ресурсов JavaScript связано много проблем, зависящих от реализованного подхода. Растущее количество веб-сайтов и приложений использует сложные системы ресурсов JavaScript, и многие разработчики надеются на более простые, быстрые и удобные в обслуживании схемы управления зависимостями. Авторы двух проектов с открытым кодом, LABjs и RequireJS, обсуждают различные перспективы достижения этих целей. Может показаться, что разговор о загрузке ресурсов JavaScript не может представлять ничего интересного. Тег Не существует единого решения, удовлетворяющего всем целям и потребностям. Следует учесть не только различные возможности синтаксиса, но и различные компромиссы, зависящие от того, какие случаи использования являются первоочередными. LABjs и RequireJS воплощают многие из этих компромиссов. Эта статья должна позволить вам глубже понять проблемы загрузки скриптов, одновременно вооружая решениями, лучше всего подходящими для конкретных сред и потребностей. Эта статья в основном фокусируется на загрузке ресурсов в среды на основе браузеров, но, как будет показано позднее, появление JavaScript на стороне сервера и необходимость "загружать" модули на стороне сервера оказывают определенное влияние на подход к загрузке браузера. Загрузка скриптов Сначала давайте убедимся, что все мы находимся на одной "странице". Темой нашего разговора является проблема необходимости загружать файлы JavaScript (либо "локально" в том же домене, либо "удаленно", то есть междоменно) на веб-страницу. Как вам, конечно, известно, обычно это выполняется с помощью старых добрых тегов Тем не менее существуют определенные ограничения использования тегов Во-вторых, до браузеров последнего поколения тег Поэтому, если в строке используется 10 тегов Даже в свежайших версиях браузеров новейшего поколения мы обнаружили только частичное улучшение этих последовательных загрузок и выполнений. Большинство новых браузеров сегодня достаточно разумны, чтобы загружать сценарии параллельно, но они все равно будут ждать, пока загруженные скрипты не выполнятся по одному, последовательно. Кроме того, так как браузеру нужно учесть, что в файле может встретиться что-то наподобие
К счастью, браузер позволяет выполнять ряд вещей асинхронно и по запросу, поэтому многие из этих проблем производительности можно решить с помощью динамической загрузки нужных файлов JavaScript. Существуют десятки различных “загрузчиков JavaScript” различной сложности и с различными возможностями. Многие из них являются частью специальных пакетов средств, например загрузчики, входящие в Dojo и YUI. В центре внимания этой статьи — два цельных решения, LABjs и RequireJS, каждое из которых полностью независимо от какого-либо пакета и позволяет решать некоторые очень сложные проблемы загрузки ресурсов. Хотя LABjs и RequireJS используют различные подходы, существует ряд преимуществ, которыми оба этих решения обладают по сравнению с базовыми тегами 1. Динамически создаваемые элементы script, позволяющие избежать блокировки визуализации браузером Благодаря добавлению скриптов на страницу с помощью динамически создаваемых элементов script браузер больше не будет блокировать визуализацию, ожидая загрузки скрипта, как показано в примере кода 1.
Пример кода 1. Упрощенный пример функции, динамически создающей тег script и добавляющей его в DOM. Вставка элементов script на лету обладает двусторонним эффектом: страница будет "разблокирована", поэтому она может продолжить загружать свои ресурсы параллельно со скриптами, но это также полностью отделит загрузку скриптов как от событий готовности DOM (DOMContentLoaded), так и от событий Если вы привыкли привязывать поведение к событиям готовности DOM/загрузки, предполагая, что это гарантирует загрузку всех скриптов (как это происходит с обычными тегами Кроме того, так как скрипт может выполняться после загрузки страницы, в скрипте нельзя использовать Хотя у динамически созданных элементов есть ряд побочных эффектов script, выигрыш в быстродействии и гибкие возможности загрузки стоят того. 2. Скрипты можно загрузить в любое время Так как элементы script добавляются на страницу динамически, для скриптов может использоваться “ленивая загрузка” в любой точке жизненного цикла страниц, например, после временной задержки, или когда пользователь нажимает кнопку. Это может помочь распределить затраты на загрузку кода по времени жизни страницы. 3. Задайте зависимости скриптов Загрузчики скриптов позволяют задать зависимости между скриптами. Синтаксис и подход к обработке зависимостей — это одно из основных различий между загрузчиками скриптов, но все они предлагают более надежное управление зависимостями по сравнению с обычными тегами Почему бы просто не объединить файлы? Прежде чем погрузиться в конкретные загрузчики, важно ответить на один конкретный вопрос, часто возникающий при обсуждении загрузчиков скриптов: “Зачем же нужен загрузчик скриптов, если я могу просто объединить все свои файлы вместе и загрузить один файл? Разве это будет плохо работать?” Это сложный вопрос, но давайте коротко на нем остановимся. Если нужно более подробное обсуждение, Кайл рекомендует в качестве полного сообщения блога "Почему бы просто не объединить файлы?".
Вывод: правильным ответом не всегда является один файл. Загрузка меньшего числа файлов обычно лучше загрузки большего числа файлов. Для многих веб-сайтов оптимальной будет загрузка от 1 до 3 скриптов. Все веб-сайты/приложения будут слегка отличаться друг от друга, поэтому нужно просто поэкспериментировать и посмотреть на результаты. Вполне вероятно, что при внимательном анализе вы выберете несколько скриптов. Даже если действительно используется только один скрипт, использование загрузчика скриптов все равно выгодно, так как загрузка одного большого слоноподобного файла может выполняться параллельно с другими ресурсами страницы. Только оно это может заметно увеличить быстродействие веб-сайта. Попробуйте, и все получится! Кроме того, помогите мне выполнить некоторое исследование, используя этот базовый тест, сравнивающий параллельную загрузку с частичным и полным объединением. Было бы замечательно, если бы вы нажали три кнопки по порядку и повторили этот процесс несколько раз. Результаты регистрируются автоматически. LABjs Средство LABjs (Loading And Blocking JavaScript, загрузка и блокировка JavaScript) с самого начала создавалось как удобный загрузчик скриптов. Под “удобством” я подразумеваю, что основной целью является максимальная простота использования при выполнении поставленных функциональных целей. Я стремился сделать LABjs доступным большинству веб-сайтов — то есть средством, с помощью которого даже владельцы веб-сайтов с минимальными навыками смогут осуществить загрузку скриптов LABjs, обладая очень малыми знаниями JavaScript. На ранних стадиях разработки LABjs практически все, кто проверял и испытывал это решение, замечали, что оно резко улучшало быстродействие Интернета (то есть оно уменьшало время загрузки страниц, ускоряя загрузку ресурсов JavaScript). Я быстро понял, что главной функциональной целью LABjs была и должна быть следующая: добиться максимально возможной оптимизации в загрузке скриптов, выполняемой единообразно для всех браузеров. Поэтому LABjs — это повышающий быстродействие загрузчик скриптов, по замыслу и по функционалу. При дальнейшем продвижении важно понимать это стремление. Выше мы рассмотрели, как элементы тегов Проблема заключается в том, что при попытке динамической загрузки двух и более скриптов одновременно многие браузеры не гарантируют нужный порядок их выполнения. Это означает, что при наличии зависимостей между двумя скриптами тут же возникает состояние конкуренции, из-за которой скрипты могут выполняться не в правильном/планируемом порядке. Поэтому моя цель в работе над LABjs состояла в создании замены тега Такой набор требований очень трудно сбалансировать в одном интерфейсе API, но интерфейс API LABjs представляет собой мою лучшую попытку решить эту задачу. Типичный пример использования нескольких тегов script приведен в примере кода 2, а эквивалентная загрузка, выполняемая с помощью синтаксиса LABjs, показана в примере кода 3.
Пример кода 2. Обычные теги <script>
Пример кода 3. Синтаксис LABjs для загрузки скриптов Как можно видеть, синтаксис LABjs пытается быть максимально близким к заменяемой разметке, позволяя веб-разработчикам преобразовать существующую разметку в загрузку скриптов, оптимизированную для быстродействия. На рис. 3 показано повышение быстродействия, достигнутое благодаря использованию LABjs, по сравнению с загрузкой при использовании обычных тегов
Примечания к интерфейсу API LABjs
По умолчанию при использовании LABjs все скрипты загружаются параллельно (включая “предварительные загрузки”, если в цепочке есть вызовы
Пример кода 4. Задание настроек по умолчанию $LAB, глобально и для цепочки Список параметров настройки и их назначение см. в документации на интерфейс API. Дополнительные возможности LABjs Мне часто задают вопрос: “Что, если мне понадобится определить, какие скрипты будут загружаться, во время выполнения в зависимости от некоторого условия?” Этот и похожие на него вопросы убедили меня задокументировать здесь (в очень общем виде) способ, который можно использовать для выжимания дополнительных возможностей из интерфейса API цепочек, подобного LABjs. Во-первых, Важно понимать, что, когда функция в цепочке вызывается в зависимости от значения, возвращенного предыдущей функцией, это просто синтаксический прием, а не требование. Возвращенное промежуточное значение, которое в цепочке всегда остается невидимым и используется косвенно, можно сохранить во временной переменной, а затем использовать так, как если бы это был вызов в конце цепочке. Это означает, что можно программно создать массив элементов, концептуально представляющий отдельные вызовы цепочки
Пример кода 5. Имитация цепочки $LAB с помощью цикла for-loop Развитие LABjs В настоящее время (и в основном) LABjs представляет собой средство загрузки скриптов, оптимизирующее производительность на стороне клиента. В нем используется знание автором дерева зависимостей этих скриптов, позволяющего создать правильную цепочку Хотя подобная среда не является целевой областью использования 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 фокусируется на предоставлении браузеру дружественного к тегам В примере кода 6 показан основной формат загрузки скриптов с помощью RequireJS.
Пример кода 6. Синтаксис RequireJS для первого примера LABjs В то время как LABjs поддерживает принудительный порядок выполнения, вставляя вызовы В RequireJS используется архитектура подключаемых модулей, позволяющая добавлять новые типы загрузки или зависимостей. Существуют подключаемые модули для пакетов строк i18n, загружающие и текстовые файлы (которые могут быть встроены с помощью средства оптимизации), рассматривающие службу JSONP как зависимость и обеспечивающие порядок выполнения зависимостей. Подключаемый модуль order использует те же приемы, что и LABjs: тип=”script/cache” имитирует тип MIME параллельной загрузки скриптов, но позволяет вручную выполнять их в нужном порядке. Нужно отметить, что этот прием нужен/эффективен только в IE, Chrome и Safari. Для Firefox/Gecko и Opera этот прием не работает, но, к счастью, этого и не требуется, так как эти браузеры автоматически гарантируют правильный порядок выполнения даже для динамически загруженных элементов script. Подключаемые модули отличаются от имени ресурса восклицательным знаком, как показано в примере кода 7.
Пример кода 7. Использование подключаемого модуля order для обеспечения порядка выполнения скриптов Модули RequireJS RequireJS поощряет использование модулей. Модули в RequireJS определяются с помощью вызова функции
Пример кода 8. Определение модуля с помощью require.def() Обратите внимание, что имя модуля и его зависимости не содержат суффикс “.js”. URL-адреса .js можно использовать напрямую для загрузки скриптов, не участвующих в определении модуля с четкой областью применения. Но с элементами, определяющими модули с помощью Модули RequireJS могут не экспортировать значения в глобальное пространство имен, и, так как для ссылок на все зависимости применяются строковые значения, на странице можно использовать несколько версий модуля. Формат модулей RequireJS аналогичен формату модулей CommonJS, и в RequireJS предусмотрен запускаемый из командной строки скрипт, который можно использовать для преобразования модулей CommonJS в синтаксис RequireJS, что позволяет использовать модули CommonJS в среде на основе браузера. При наличии серверного процесса, который может упаковать модули CommonJS в формат CommonJS Transport D, такого как Transporter Криса Зипа (Kris Zyp), RequireJS позволяет использовать этот код напрямую с помощью адаптера RequireJS Transport D. С использованием модулей сценарии в нашем основном примере могут быть написаны следующим образом:
Пример кода 9. Примеры скриптов как модулей RequireJS Средство оптимизации RequireJS Средство оптимизации RequireJS упрощает объединение скриптов, и RequireJS можно настроить для заблаговременной приоритетной загрузки этих объединенных слоев скриптов, таким образом, предоставляя максимальное быстродействие и одновременно сохраняя небольшие размеры и модульность источников JavaScript. Самое главное, так как это средство оптимизации является средством командной строки, для его работы не требуется специальных настроек на стороне сервера, и результаты его работы можно будет развертывать в системах со статическими файлами и CDN. Если с помощью средства версии script2-a.js иscript2-b.js, определяемые с помощью
Пример кода 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. Независимо от конкретных потребностей, использование любого загрузчика вместо обычных тегов |