От автора: давайте поговорим о разделении кода в Angular, ленивой загрузке, а также немного затронем Webpack. Разделение кода позволяет разбить весь код на маленькие куски и использовать их по необходимости, а это мы уже называем «ленивой загрузкой». Давайте узнаем, как это делать, а также разберемся в концепциях и терминологии за этим понятием.
Нужен код? Найдете его на GitHub или смотрите живое демо.
Gif-изображение выше демонстрирует ленивую загрузку. Обратите внимание, как скачиваются 0-chunk.js и 1-chunk.js при переходе на эти роуты. Запись сверху скомпилирована в AoT.
Терминология
Для лучшего понимания давайте разберемся в терминологии.
Разделение кода
Разделение кода – процесс, как очевидно, разбития нашего кода. Но что, как и где разбивать? Мы это поймем по мере прочтения статьи. Разделение кода позволяет взять все наше приложение и нарезать его на разные куски. В этом весь смысл разделения кода, и с помощью Webpack это можно делать очень легко с загрузчиком для Angular. Если кратко, то ваше приложение превращается во множество маленьких приложений, которые обычно называют кусками. Эти куски можно загружать по необходимости.
Ленивая загрузка
Главное здесь – «по необходимости». Ленивая загрузка – процесс загрузки уже разбитых кусков кода по требованию. В Angular ленивая загрузка осуществляется через роутер. Загрузка «ленивая», потому что она не «жадная» (файлы не загружаются наперед раньше необходимого). Ленивая загрузка увеличивает производительность, так как мы загружаем лишь часть нашего приложения, а не всю сборку. Мы можем разделить код на @NgModules в Angular и подавать их лениво через роутер. Роутер Angular загрузит модуль кода только, когда будет запрошен определенный роут.
Настройка Webpack
Настройка Webpack – довольно тривиальная задача. Можете посмотреть весь config, чтобы понять, как все работает. Однако нам понадобится лишь парочка настроек.
Выбор загрузчика роутов
Для включения ленивой загрузки можно воспользоваться angular-router-loader или ng-router-loader. Я возьму первый angular-router-loader – с ним довольно просто работать. Оба инструмента покрывают базовый набор функций, которые нам понадобятся для ленивой загрузки.
Вот так я добавил его в свой Webpack конфиг:
1 2 3 4 5 6 7 8 |
{ test: /\.ts$/, loaders: [ 'awesome-typescript-loader', 'angular-router-loader', 'angular2-template-loader' ] } |
Я подключаю angular-router-loader в массив загрузчиков файлов TypeScript. Это позволит нам использовать замечательный загрузчик для ленивой загрузки! Далее необходимо настроить свойство output в конфиге Webpack:
1 2 3 4 5 6 |
output: { filename: '[name].js', chunkFilename: '[name]-chunk.js', publicPath: '/build/', path: path.resolve(__dirname, 'build') } |
Здесь можно задать имена для наших кусков, которые обычно динамические и заканчиваются примерно так:
1 2 3 4 |
0-chunk.js 1-chunk.js 2-chunk.js 3-chunk.js |
Если необходимо, можете посмотреть весь config, чтобы связать его со своими настройками.
Ленивые @NgModules
Чтобы проиллюстрировать настройки, как в демо и gif, у нас есть три функциональных модуля, которые полностью одинаковы за исключением имен модулей и компонентов.
Функциональные модули
Функциональные модули или дочерние модули – модули, которые можно лениво загружать через роутер. Ниже представлено три дочерних модуля:
1 2 3 |
DashboardModule SettingsModule ReportsModule |
И родительский модуль app:
1 |
AppModule |
AppModule каким-то образом импортирует другие модули. Это можно сделать несколькими способами, асинхронно и синхронно.
Ленивая загрузка async-модуля
Для ленивой загрузки нам нужен роутер, и для этого нам нужно лишь магическое свойство loadChildren при объявлении роутов.
Файл ReportsModule:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// reports.module.ts import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; // containers import { ReportsComponent } from './reports.component'; // routes export const ROUTES: Routes = [ { path: '', component: ReportsComponent } ]; @NgModule({ imports: [ RouterModule.forChild(ROUTES) ], declarations: [ ReportsComponent ] }) export class ReportsModule {} |
Обратите внимание на то, как мы используем пустой path:
1 2 3 4 |
// reports.module.ts export const ROUTES: Routes = [ { path: '', component: ReportsComponent } ]; |
Этот модуль можно использовать вместе с loadChildren и path в родительском модуле, позволяя AppModule указывать URL. Это создает гибкую модульную структуру, в которой функциональные модули «не знают» свои абсолютные пути. У них есть относительный путь, который меняется в зависимости от путей AppModule.
Т.е. внутри app.module можно сделать следующее:
1 2 3 4 |
// app.module.ts export const ROUTES: Routes = [ { path: 'reports', loadChildren: '../reports/reports.module#ReportsModule' } ]; |
Этот код говорит Angular «когда мы переходим на /reports, загрузи пожалуйста этот модуль». Обратите внимание на то, что объявление роута внутри ReportsModule пустое. Также и другие определения роутов пустые:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// reports.module.ts export const ROUTES: Routes = [ { path: '', component: ReportsComponent } ]; // settings.module.ts export const ROUTES: Routes = [ { path: '', component: SettingsComponent } ]; // dashboard.module.ts export const ROUTES: Routes = [ { path: '', component: DashboardComponent } ]; |
Полная картина определений роутов в AppModule:
1 2 3 4 5 6 |
export const ROUTES: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'dashboard' }, { path: 'dashboard', loadChildren: '../dashboard/dashboard.module#DashboardModule' }, { path: 'settings', loadChildren: '../settings/settings.module#SettingsModule' }, { path: 'reports', loadChildren: '../reports/reports.module#ReportsModule' } ]; |
Таким образом, в любой момент времени мы можем «передвинуть» целый модуль под новый путь роута, и все будет работать, как и должно. Круто!
Обратите внимание, как в записи ниже *-chunk.js файлы загружаются при переходе по определенным роутам.
Ленивая загрузка вызывается в момент, когда мы асинхронно вызываем кусок кода. Если использовать loadChildren и строковое значение, указывающее на модуль, то куски будут загружаться асинхронно, если не использовать загрузчик, в котором прописана синхронная загрузка.
Загрузка sync-модуля
Если, как в моем приложении, ваш базовый путь редиректит на другой роут:
1 |
{ path: '', pathMatch: 'full', redirectTo: 'dashboard' }, |
Вы можете указать, чтобы один модуль загружался синхронно. Т.е. он будет встроен в ваш app.js (в моем случае, у других может отличаться в зависимости от глубины загружаемых с помощью ленивой загрузки функциональных модулей). Так как я делаю редирект прямо на DashboardModule, есть ли мне смысл разбивать его? Да и нет.
Да: если пользователь сначала заходит на /settings (страница обновляется), нам не нужно загружать дополнительный код, т.е. мы экономим на загрузке.
Нет: этот модуль может использовать крайне часто, возможно, его лучше загружать заранее в жадном режиме.
Да и нет зависят от вашего сценария. Вот так можно синхронно загружать наш DashboardModule с помощью import и стрелочной функции:
1 2 3 4 5 6 7 8 |
import { DashboardModule } from '../dashboard/dashboard.module'; export const ROUTES: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'dashboard' }, { path: 'dashboard', loadChildren: () => DashboardModule }, { path: 'settings', loadChildren: '../settings/settings.module#SettingsModule' }, { path: 'reports', loadChildren: '../reports/reports.module#ReportsModule' } ]; |
Я предпочитаю этот способ, так как он менее явный. Сейчас DashboardModule встроен с AppModule и хранится в app.js. Можете попробовать и запустить проект локально.
Проект angular-router-loader имеет хорошую функцию, о которой стоит сказать. Это кастомный синтаксис, который указывает, какие модули необходимо загружать синхронно при добавлении к строке ?sync=true:
1 |
loadChildren: '../dashboard/dashboard.module#DashboardModule?sync=true' |
Эффект такой же, как от использования стрелочной функции.
Производительность
В простом приложении, как у меня в демо, вы не заметите прироста производительности, однако в большом приложении с большим кодом вы сильно выиграете от разделения кода и ленивой загрузки!
Модули ленивой загрузки
Представим, что у нас есть следующие файлы:
1 2 |
vendor.js [200kb] // angular, rxjs, etc. app.js [400kb] // our main app bundle |
Теперь предположим, что мы разбили код:
1 2 3 4 5 |
vendor.js [200kb] // angular, rxjs, etc. app.js [250kb] // our main app bundle 0-chunk.js [50kb] 1-chunk.js [50kb] 2-chunk.js [50kb] |
В больших масштабах прирост производительности будет огромным для вещей типа PWA, первичных запросов сети. Первичная загрузка сильно сократится.
Предварительная загрузка ленивых модулей
Есть и другой вариант — PreloadAllModules в Angular. После загрузки он вытягивает все оставшиеся куски модулей с сервера. Это можно делать частично и по выбору жадно загружать модули кусков кода. Такой подход ускорит навигацию между разными модулями. Модули, в свою очередь, будут загружаться асинхронно после их добавления в корневой роутинг. Пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import { RouterModule, Routes, PreloadAllModules } from @angular/router; export const ROUTES: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'dashboard' }, { path: 'dashboard', loadChildren: '../dashboard/dashboard.module#DashboardModule' }, { path: 'settings', loadChildren: '../settings/settings.module#SettingsModule' }, { path: 'reports', loadChildren: '../reports/reports.module#ReportsModule' } ]; @NgModule({ // ... imports: [ RouteModule.forRoot(ROUTES, { preloadingStrategy: PreloadAllModules }) ], // ... }) export class AppModule {} |
В моем демо Angular сначала загрузит приложение, а затем загрузит оставшиеся куски.
Весь код ищите на GitHub или в демо! Крайне рекомендую попробовать эти подходы, посмотрите разные сценарии и нарисуйте свою картину производительности.
Автор: Todd Motto
Источник: //toddmotto.com/
Редакция: Команда webformyself.