Лінива завантаження: поділ коду NgModules з Webpack

324

Від автора: давайте поговоримо про поділ коду в Angular, ледачою завантаженні, а також трохи торкнемося Webpack. Поділ коду дозволяє розбити увесь код на маленькі шматки і використовувати їх за потребою, а це ми вже називаємо «ледачою завантаженням». Давайте дізнаємося, як це робити, а також розберемося в концепціях і термінології за цим поняттям.

Потрібен код? Знайдете його на GitHub або дивіться живе демо.

Лінива завантаження: поділ коду NgModules з Webpack

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 конфіг:

{
test: /\.ts$/,
loaders: [
‘awesome-typescript-loader’,
‘angular-router-loader’,
‘angular2-template-loader’
]
}

Я підключаю angular-router-loader в масив завантажувачів файлів TypeScript. Це дозволить нам використовувати чудовий завантажувач для ледачої завантаження! Далі необхідно налаштувати властивість output в конфіги Webpack:

output: {
filename: ‘[name].js’,
chunkFilename: ‘[name]-chunk.js’,
publicPath: ‘/build/’,
path: path.resolve(__dirname, ‘build’)
}

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

0-chunk.js
1-chunk.js
2-chunk.js
3-chunk.js

Якщо необхідно, можете подивитися весь config, щоб пов’язати його зі своїми настройками.

Ледачі @NgModules

Щоб проілюструвати налаштування, як в демо і gif, у нас є три функціональні модулі, які повністю однакові, за винятком імен модулів і компонентів.

Функціональні модулі

Функціональні модулі або дочірні модулі – модулі, які можна ліниво завантажувати через роутер. Нижче представлено три дочірніх модуля:

DashboardModule
SettingsModule
ReportsModule

І батьківський модуль app:

AppModule

AppModule якимось чином імпортує інші модулі. Це можна зробити декількома способами, асинхронно і синхронно.

Лінива завантаження async-модуля

Для ледачої завантаження нам потрібен роутер, і для цього нам потрібно лише магічне властивість loadChildren при оголошенні роутов.

Файл ReportsModule:

// 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:

// reports.module.ts
export const ROUTES: Routes = [
{ path: “, component: ReportsComponent }
];

Цей модуль можна використовувати разом з loadChildren і path в батьківському модулі, дозволяючи AppModule вказувати URL. Це створює гнучку модульну структуру, в якій функціональні модулі «не знають» свої абсолютні шляхи. У них є відносний шлях, який змінюється в залежності від шляхів AppModule.

Тобто всередині app.module можна зробити наступне:

// app.module.ts
export const ROUTES: Routes = [
{ path: ‘reports’, loadChildren: ‘../reports/reports.module#ReportsModule’ }
];

Цей код каже Angular «коли ми переходимо на /reports, завантаж будь ласка цей модуль». Зверніть увагу на те, що оголошення роута всередині ReportsModule порожнє. Також і інші визначення роутов порожні:

// 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:

export const ROUTES: Routes = [
{ path: “, pathMatch: ‘full’, redirectTo: ‘dashboard’ },
{ path: ‘dashboard’, loadChildren: ‘../інструментів/інструментів.module#DashboardModule’ },
{ path: ‘settings’, loadChildren: ‘../settings/settings.module#SettingsModule’ },
{ path: ‘reports’, loadChildren: ‘../reports/reports.module#ReportsModule’ }
];

Таким чином, в будь-який момент часу ми можемо «пересунути» цілий модуль під новий шлях роута, і все буде працювати, як і повинно. Круто!

Зверніть увагу, як у запису нижче *-chunk.js файли завантажуються при переході з певним роутам.

Лінива завантаження: поділ коду NgModules з Webpack

Лінива завантаження викликається в момент, коли ми асинхронно викликаємо шматок коду. Якщо використовувати loadChildren і рядкове значення, що вказує на модуль, то шматки будуть завантажуватися асинхронно, якщо не використовувати завантажувач, в якому прописана синхронна завантаження.

Завантаження sync-модуля

Якщо, як у моєму додатку, ваш базовий шлях редиректит на інший рауса:

{ path: “, pathMatch: ‘full’, redirectTo: ‘dashboard’ },

Ви можете вказати, щоб один модуль завантажувався синхронно. Тобто він буде вбудований в ваш app.js (у моєму випадку, у інших може відрізнятися в залежності від глибини завантажуються за допомогою ледачою завантаження функціональних модулів). Так як я роблю редирект прямо на DashboardModule, чи є мені сенс розбивати його? Та й немає.

Так: якщо користувач спочатку заходить на /settings (сторінка оновлюється), нам не потрібно завантажувати додатковий код, тобто ми економимо на завантаженні.

Немає: цей модуль може використовувати вкрай часто, можливо, його краще заздалегідь завантажувати в жадібну режимі.

Та й немає залежать від вашого сценарію. Ось так можна синхронно завантажувати наш DashboardModule з допомогою import і стрілочної функції:

import { DashboardModule } from ‘../інструментів/інструментів.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:

loadChildren: ‘../інструментів/інструментів.module#DashboardModule?sync=true’

Ефект такий же, як від використання стрілочної функції.

Продуктивність

У простому додатку, як у мене демо ви не помітите приросту продуктивності, проте у великому додатку з великим кодом ви сильно виграєте від поділу коду і ледачою завантаження!

Модулі ледачою завантаження

Уявімо, що у нас є такі файли:

vendor.js [200kb] // angular, rxjs, etc.
app.js [400kb] // our main app bundle

Тепер припустимо, що ми розбили код:

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. Після завантаження вона витягує всі залишилися шматки модулів з сервера. Це можна робити і частково за вибором жадібно завантажувати модулі шматків коду. Такий підхід прискорить навігацію між різними модулями. Модулі, у свою чергу, будуть завантажуватися асинхронно після додавання до кореневої роутинг. Приклад:

import { RouterModule, Routes, PreloadAllModules } from @angular/router;
export const ROUTES: Routes = [
{ path: “, pathMatch: ‘full’, redirectTo: ‘dashboard’ },
{ path: ‘dashboard’, loadChildren: ‘../інструментів/інструментів.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 або в демо! Вкрай рекомендую спробувати ці підходи, подивіться різні сценарії і намалюйте свою картину продуктивності.