AngularJS + Webpack = lazyLoad

Вступление


При написании Single Page Application разработчики в большинстве случаев сталкиваются с одной очень распространенной проблемой, а именно — создание lazyLoad модулей и их последующая загрузка на сторону клиента. Т.е. по какому-то действию или по переходу по URL (в большинстве случаев) мы должны загрузить определенный набор зависимостей — JavaScript, CSS, HTML и т.д. В реалиях современной Front-End разработки это будет большущий JavaScript файл. В этой статье я хочу поделиться своим опытом и показать как реализовать lazyLoad модули для AngularJS и тем самым уменьшить общий объем кода при первой загрузке приложения.

Почему AngularJS 1.x


Наверное, у тебя, уважаемый читатель, появился резонный вопрос: «Стоп, какой AngularJS 1.x, ведь совсем недавно был релиз Angular v5.2». Вопрос более чем уместен. Все просто, довольно много проектов, которые используют AngularJS 1.x и при этом себя хорошо чувствуют. Есть целый ряд отраслей для которых переход на новую версию очень затратный как в человеко-часах, так и в денежном эквиваленте. AngularJS 1.x еще очень востребован на рынке.

Велосипеды, которые уже есть


Так уж повелось, что в мире Front-End разработки велосипедов инструментов/подходов, чтобы решить один и тот же вопрос — вагон и маленькая тележка. Каждый выбирает под свои нужды, умения и знания. И здесь нет какого-то одного выверенного решения, которое подойдет в 99,99% случаев. Это и не хорошо и не плохо. Просто нужно это принять и жить дальше разрабатывать дальше. Кто-то выбирает RequireJS, кто-то curl.js, кто-то Browserify, кто-то <вставить свое>. Мы рассмотрим как реализовать lazyLoad загрузку модулей с использованием Webpack, UI-Router и ocLazyLoad. Всю закулисную магию будет проделывать ocLazyLoad. Если мы попробуем сделать lazyLoad модуль при помощи того же require.ensure и без использования ocLazyLoad, то получим примерно такую ошибку:

require.ensure error

Структура проекта


Итак, приступим. Оригинальный код проекта, для которого мне нужно было реализовать lazyLoad модули, я не могу предоставлять в общий доступ. Поэтому я сделал небольшое приложение. Я старался сделать так, чтобы приложение не выглядело как звездолет в котором непонятно, что откуда берется и вообще как это все работает. Основное назначение — это сделать рабочий прототип, который будет доступен онлайн (не только исходники). Ниже вы можете видеть структуру проекта для веток require-ensure и system-import. Для ветки import-es6 мы внесем небольшое дополнение.

project-root
+-- src
¦   +-- core
¦   ¦   +-- bootstrap.js
¦   +-- pages
¦   ¦   +-- home
¦   ¦   ¦   +-- about
¦   ¦   ¦   ¦   +-- about.module.js
¦   ¦   ¦   ¦   +-- about.view.html
¦   ¦   ¦   +-- index
¦   ¦   ¦   ¦   +-- index.module.js
¦   ¦   ¦   ¦   +-- index.view.html
¦   ¦   ¦   +-- home.module.js
¦   ¦   ¦   +-- home.module.routing.js
¦   ¦   ¦   +-- home.module.states.js
+-- app.js
+-- index.html

Прежде, чем мы приступим к работе с кодом, давайте обсудим два важных момента, которые влияют на то, будет ли наш модуль lazyLoad или нет:

  1. Чтобы модуль стал lazyLoad его не нужно указывать как зависимость для других модулей.
  2. Модуль не должен быть импортирован нигде, кроме того роута для которого вы хотите сделать этот модуль lazyLoad.

Если не очень понятно, не волнуйтесь — на практике сразу станет ясно, что к чему.

require.ensure() + $ocLazyLoad


require.ensure был предложен командой webpack еще в его первых версиях. Этот метод позволяет разработчикам динамически создавать отдельные файлы (чанки в контексте терминологии webpack) с какой-то частью кода, которые будут впоследствии загружены по требованию на стороне клиента. Данный подход является менее предпочтительным для создания динамически загружаемых модулей, но если сильно хочеться, то ничего страшного в этом нет. Этот метод подойдет прекрасно тем, кто хочет сделать lazyLoad модули без больших затрат на рефакторинг. Ниже вы можете видеть пример использования require.ensure для загрузки index.module.js:

Для about.module.js код будет идентичным, за исключением путей к модулю и некоторых других параметров. Ниже вы можете видеть пример использования require.ensure для загрузки about.module.js:

Как вы могли заметить из кода вся магия происходит вот в этой сроке:

$ocLazyLoad.load(module.HOME_ABOUT_MODULE);

Есть другой вариант указания модуля — через объект:

$ocLazyLoad.load({
  name: "home.module"
});

Но в таком случае, мы себя ограничиваем в свободе действий. Если мы захотим переименовать модуль, то нужно будет менять код в нескольких местах. Также есть большая вероятность допустить ошибку при написании имени модуля. Я вам настоятельно рекомендую не использовать этот подход.

Хотел бы обратить ваше внимание на один важный нюанс в отношении about.module.js и последующей загрузки на сторону клиента. Посмотрите на скрин ниже:


При переходе по Home/About ссылке подгружаются сразу два файла: index.module.chunk.js и about.module.chunk.js. Так происходит потому что home.about URL является дочерним по отношению к home URL. Об этом стоит помнить. Забегая немного наперед, в последнем разделе мы добавим еще один модуль с новым URL, и увидим, что для него будет грузиться только один файл и больше ничего.

System.import + $ocLazyLoad


Я долго думал писать про этот подход или нет. Считаю, что нужно о нем поговорить.
System.import это другая конструкция от команды webpack, которую впоследствии запретили, но этот подход продолжают предлагать как вариант реализации. Более того, эта конструкция продолжает работать и в новых версиях webpack. Подозреваю, что это сделали из соображений совместимости. Если вы используете эту конструкцию в своем проекте, то у меня плохие новости — она в статусе deprecated. Кода в этом разделе не будет. Идем дальше.

Dynamic imports + $ocLazyLoad


Возможно, вы уже слышали, что Chrome 63 и Safari Technology Preview 24 выкатили обновления и теперь разработчикам доступны динамические импорты. Да, да, те самые динамические импорты, которые были предложены в спецификации. Еще в далеком 2016 команда webpack внедрила поддержку динамических импортов.

В этом разделе мы добавим еще один модуль в корень директории pages, чтобы убедиться в правильной работе lazyLoad. Структура для ветки import-es6 представлена ниже:

project-root
+-- src
¦   +-- core
¦   ¦   +-- bootstrap.js
¦   +-- pages
¦   ¦   +-- blog
¦   ¦   ¦   +-- blog.module.js
¦   ¦   ¦   +-- blog.service.js
¦   ¦   ¦   +-- blog.view.html
¦   ¦   +-- home
¦   ¦   ¦   +-- about
¦   ¦   ¦   ¦   +-- about.module.js
¦   ¦   ¦   ¦   +-- about.view.html
¦   ¦   ¦   +-- index
¦   ¦   ¦   ¦   +-- index.module.js
¦   ¦   ¦   ¦   +-- index.view.html
¦   ¦   ¦   +-- home.module.js
¦   ¦   ¦   +-- home.module.routing.js
¦   ¦   ¦   +-- home.module.states.js
+-- app.js
+-- app.routing
+-- app.states.js
+-- index.html

Если вы не используете у себя в проекте Babel или TypeScript, то все заведется с коробки без лишних танцев с бубном. Но и вы и я знаем, что в реалиях современного Front-End очень сложно писать код без Babel или TypeScript. Поговорим про Babel. Для начала нам нужно установить дополнительный плагин для Babel, который понимает синтаксис динамических импортов: syntax-dynamic-import. Иначе мы получим ошибку:

Babel Syntax Error

Далее нам нужно добавить .babelrc с настройками:

Теперь вторая неприятная ошибка, которую вы можете видеть ниже:

ESLint Syntax Error

Да, вы не ошиблись, ESLint тоже не понимает динамических импортов. Чтобы это поправить нужно установить специальный парсер для ESLint babel-eslint и тогда все заработает как по маслу. Добавим .eslintrc с настройками:

Что ж, пришло время попробовать динамические импорты на деле. Протестируем их на новом модуле:

Как вы могли заметить из кода, команда webpack добавила несколько приятных фишек к динамическим импортам. Есть возможность указать название итогового чанка, который создаст webpack, а также мы можем указать способ загрузки. Подробнее про это читайте здесь. На видео ниже вы можете наблюдать работу динамических импортов:


Component vs. Template


Для загрузки модулей использовалось свойство component. Можно использовать вместо свойства component свойство под именем template. Работают они практически одинаково, с одним лишь нюансом. Если вы опечатались в названии компоненты или по каким-то другим причинам компонента недоступна, то вы получите ошибку в консоли. С template вы такой ошибки не получите. И можно очень долго искать в чем проблема.

Полезные ссылки


  1. How to route to AngularJS 1.5 components
  2. Lazy loading in UI-Router
  3. Исходный код на GitHub
  4. Проект на Heroku

Вместо заключения


lazyLoad модули в контексте AngularJS приложения — это отличная возможность сделать ваше приложение более легковесистым, более отзывчивым и более распределенным. Времена меняются, требования к приложениям растут, а с ними растут и объемы кода, которые мы доставляем на клиент. Если раньше было достаточно собрать весь код в единый файл, отдать его конечному пользователю и все было круто, то сейчас это непозволительная роскошь. Наблюдается тенденция разделения приложения в зависимости от URL с выделением общего кода.

На этом все. Спасибо за внимание. Кто дочитал до конца, отдельное спасибо.

P.S. Если у вас был подобный опыт в реализации lazyLoad модулей для AngularJS приложения — поделитесь им в комментариях.

Комментарии (6)


  1. PaulMaly
    11.01.2018 19:24

    Так с чем связана ошибка при простом использовании require.ensure с Angular? Много раз его использовал с другими фреймворками и ни разу не сталкивался.


    1. var_bin Автор
      11.01.2018 19:48

      Если в кратце, то вся проблема в сервисе $injector. Учитывая как webpack обрабатывает require.ensure, $injector в свою очередь не находит сам модуль для которого мы применяем require.ensure. ocLazyLoad берет решение этой проблемы на себя.


      1. PaulMaly
        11.01.2018 21:05
        +1

        Понятно, я так и понял, что это специфичная для Angular проблема. require.ensure не всегда удобен, так как можно легко сломать статический анализатор Webpack, когда его используешь. Спасибо за разъяснение.


  1. Focushift
    11.01.2018 19:47

    Можно ли таким образом завернуть сервисы?


    1. var_bin Автор
      11.01.2018 19:50

      В принципе, данный подход можно использовать и для сервисов и для контроллеров. В документации к ocLazyLoad есть примеры


  1. var_bin Автор
    11.01.2018 19:48

    .