title: Angular 延遲載入
date: 2017-09-05 10:00
categories: Training
keywords:


延遲載入 (Lazy loading)

一般來說我們會將 JavaScript 直接撰寫在網頁內或是另外儲存成 js 檔再透過連結方式引用,但是隨著功能的增加檔案也會逐漸變大,因而導致使用者需要等待載入的時間也逐漸變長。

延遲載入 便是透過將 JavaScript 切割成多個檔案再逐一下載方式來縮短等待時間,看似簡單但是實作上卻很有難度,若單存只是將 JavaScript 切割成數個檔案然後都在主頁面連結,這樣其實總檔案大小並沒有改變,我們需要的應該是在下載完前面的 js 檔後網頁畫面應該就要呈現,而且使用者也可以開始操作,後續 js 檔下在後再動態載入目前的網頁程式內,因此出現一些問題需要克服:

Angular 模組化 我們發現 元件(Component) 可以很容易的移動到任意的 模組(NgModule) 內,不論是一開始的先規劃好或是事後的調整,其實沒有很困難,因此如果我們可以以 模組 當作切割單位,每個模組可以單獨為一個檔案,當使用者有需要用到該功能時再下載,這應該對開發人員來說應該會是很容易規劃的 延遲載入 方式,接下來我們就來看看如何利用 Angular 來實作 延遲載入

安裝 Angular Augury

利用 Chrome 瀏覽器安裝 Augury 擴充功能。
img
執行專案,並透過 Chrome 的開發者工具查看,可以發現 Augury 下面的 RouterTree 完整的呈現專案的路由樹狀圖。
img

建立 employee 的路由模組

比較一下自己手動建立的路由規則的定義檔(src\app\employee\employee.routing.ts)與透過 --routing 參數建立的路由模組(src\app\home\home-routing.module.ts),可以發現最大差別就是 --routing 參數所建立的檔案會將路由規則再包覆到一個模組內。

{% codeblock employee.routing.ts lang:ts %}
import { Routes } from ‘@angular/router’;
import { AddressBookComponent } from ‘./address-book/address-book.component’;
import { CalendarComponent } from ‘./calendar/calendar.component’;
import { FileComponent } from ‘./file/file.component’;
import { LeaveComponent } from ‘./leave/leave.component’;
import { LogbookComponent } from ‘./logbook/logbook.component’;
import { ReimburseComponent } from ‘./reimburse/reimburse.component’;
import { ToDoListComponent } from ‘./to-do-list/to-do-list.component’;

const routes: Routes = [
{ path: ‘’, redirectTo: ‘to-do-list’, pathMatch: ‘full’ },
{ path: ‘address-book’, component: AddressBookComponent },
{ path: ‘calendar’, component: CalendarComponent },
{ path: ‘file’, component: FileComponent },
{ path: ‘leave’, component: LeaveComponent },
{ path: ‘logbook’, component: LogbookComponent },
{ path: ‘reimburse’, component: ReimburseComponent },
{ path: ‘to-do-list’, component: ToDoListComponent }
];

export const employeeRouter = routes;

{% endcodeblock %}

{% codeblock home-routing.module.ts lang:ts %}
import { NgModule } from ‘@angular/core’;
import { Routes, RouterModule } from ‘@angular/router’;
import { HomeComponent } from ‘./home.component’;
import { employeeRouter } from ‘../employee/employee.routing’;

const routes: Routes = [
{
path: ‘home’, component: HomeComponent, children: employeeRouter
}
];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HomeRoutingModule { }

{% endcodeblock %}

建立路由模組檔 src\app\employee\employee-routing.module.ts,輸入 a-m 就會列出相關指令清單,選擇 a-module-routing 系統就會產生路由模組的程式框架。
img
img
src\app\employee\employee.routing.ts 的路由規則加進來。

{% codeblock home-routing.module.ts lang:ts %}
import { NgModule } from ‘@angular/core’;
import { Routes, RouterModule } from ‘@angular/router’;

import { AddressBookComponent } from ‘./address-book/address-book.component’;
import { CalendarComponent } from ‘./calendar/calendar.component’;
import { FileComponent } from ‘./file/file.component’;
import { LeaveComponent } from ‘./leave/leave.component’;
import { LogbookComponent } from ‘./logbook/logbook.component’;
import { ReimburseComponent } from ‘./reimburse/reimburse.component’;
import { ToDoListComponent } from ‘./to-do-list/to-do-list.component’;

const routes: Routes = [
{ path: ‘’, redirectTo: ‘to-do-list’, pathMatch: ‘full’ },
{ path: ‘address-book’, component: AddressBookComponent },
{ path: ‘calendar’, component: CalendarComponent },
{ path: ‘file’, component: FileComponent },
{ path: ‘leave’, component: LeaveComponent },
{ path: ‘logbook’, component: LogbookComponent },
{ path: ‘reimburse’, component: ReimburseComponent },
{ path: ‘to-do-list’, component: ToDoListComponent }
];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class EmployeeRoutingModule { }

{% endcodeblock %}

再將 EmployeeRoutingModule 註冊到 src\app\employee\employee.module.ts 內。

{% codeblock employee.module.ts lang:ts %}
import { NgModule } from ‘@angular/core’;
import { CommonModule } from ‘@angular/common’;
import { CalendarComponent } from ‘./calendar/calendar.component’;
import { AddressBookComponent } from ‘./address-book/address-book.component’;
import { LogbookComponent } from ‘./logbook/logbook.component’;
import { ToDoListComponent } from ‘./to-do-list/to-do-list.component’;
import { FileComponent } from ‘./file/file.component’;
import { LeaveComponent } from ‘./leave/leave.component’;
import { ReimburseComponent } from ‘./reimburse/reimburse.component’;
import { EmployeeRoutingModule } from ‘./employee-routing.module’;

@NgModule({
imports: [
CommonModule,
EmployeeRoutingModule
],
declarations: [
CalendarComponent,
AddressBookComponent,
LogbookComponent,
ToDoListComponent,
FileComponent,
LeaveComponent,
ReimburseComponent
]
})
export class EmployeeModule { }

{% endcodeblock %}

loadChildren

上面的步驟只是將路由規則以 CLI 預設模式重新封裝,其實我們可以在建立模組時都先帶入 --routing 參數,最後確定不需要時在移除就好,接下來回到正題,將 EmployeeModule 改成以延遲載入方式啟動。
開啟 src\app\home\home-routing.module.tschildren 屬性替換成 loadChildren 屬性,屬性質的規則是 模組路徑 + # + 模組名稱

{% codeblock home-routing.module.ts lang:ts %}
import { NgModule } from ‘@angular/core’;
import { Routes, RouterModule } from ‘@angular/router’;
import { HomeComponent } from ‘./home.component’;
import { employeeRouter } from ‘../employee/employee.routing’;

const routes: Routes = [
{
path: ‘home’, component: HomeComponent,
loadChildren: ‘./../employee/employee.module#EmployeeModule’
}
];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HomeRoutingModule { }

{% endcodeblock %}

開啟專案,並透過開發工具檢視路由樹,可以發現 EmployeeModule 內的元件好像被載入2次。
img
這是因為 HomeModule(src\app\home\home.module.ts) 內有註冊 EmployeeModule,因此我們將它移除在重新測試。

{% codeblock home.module.ts lang:ts %}
import { NgModule } from ‘@angular/core’;
import { CommonModule } from ‘@angular/common’;

import { HomeRoutingModule } from ‘./home-routing.module’;
import { HomeComponent } from ‘./home.component’;
import { HeaderComponent } from ‘./header/header.component’;
import { AsideComponent } from ‘./aside/aside.component’;
import { CustomMaterialModule } from ‘../custom-material.module’;
// import { EmployeeModule } from ‘../employee/employee.module’;

@NgModule({
imports: [
CommonModule,
HomeRoutingModule,
CustomMaterialModule,
// EmployeeModule
],
declarations: [
HomeComponent,
HeaderComponent,
AsideComponent
]
})
export class HomeModule { }

{% endcodeblock %}

img
看起來似乎是正常的,但是到底有沒有延遲載入卻很難看出來。
嘗試將 home-routing.module.tscomponent: HomeComponent 移除看看。

{% codeblock home-routing.module.ts lang:ts %}
import { NgModule } from ‘@angular/core’;
import { Routes, RouterModule } from ‘@angular/router’;
import { HomeComponent } from ‘./home.component’;
import { employeeRouter } from ‘../employee/employee.routing’;

const routes: Routes = [
{
path: ‘home’,
loadChildren: ‘./../employee/employee.module#EmployeeModule’
}
];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HomeRoutingModule { }

{% endcodeblock %}

img
可想而知畫面就會少掉 HomeComponent,但是有趣的是路由樹內卻出現 [lazy] 的字樣,這樣就表示在 home 後面分支都是透過延遲載入方式加進來的。
img
看起來似乎當有使用 component 屬性時就不會啟用延遲載入,變通的方式是將在延遲載入的模組拉到 children 屬性內,等於在往下延伸一層路由。

{% codeblock home-routing.module.ts lang:ts %}
import { NgModule } from ‘@angular/core’;
import { Routes, RouterModule } from ‘@angular/router’;
import { HomeComponent } from ‘./home.component’;
import { employeeRouter } from ‘../employee/employee.routing’;

const routes: Routes = [
{
path: ‘home’, component: HomeComponent, children: [
{ path: ‘’, loadChildren: ‘./../employee/employee.module#EmployeeModule’ }
]
}
];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HomeRoutingModule { }

{% endcodeblock %}

啟動專案後可以看到畫面變正常,路由樹也有標註 [lazy]
img
img

PreloadAllModules

目前的 延遲載入 模式是當使用者點選時才會載入相關元件,但是 Angular 提供另一個更自動化的參數-PreloadAllModules,透過它的設定下載載入模式會變成自動下載,也就是說使用者在瀏覽器操作同時它會在背景繼續下載剩餘的檔案,當使用者點選到相關功能時就可以直接切換,不必再等待下載,開啟 src\app\app-routing.module.tsRouterModule.forRoot(routes) 加入 preloadingStrategy: PreloadAllModules 參數。

{% codeblock home-routing.module.ts lang:ts %}
import { NgModule } from ‘@angular/core’;
import { PreloadAllModules, Routes, RouterModule } from ‘@angular/router’;
import { Page404Component } from ‘./page404/page404.component’;

const routes: Routes = [
{ path: ‘’, redirectTo: ‘home’, pathMatch: ‘full’ },
{ path: ‘404’, component: Page404Component }
];

@NgModule({
imports: [RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})],
exports: [RouterModule]
})
export class AppRoutingModule { }

{% endcodeblock %}

preloadingStrategy:預設值為 NoPreloading,也就是需要時再下載,當然我們可以以自己客製下載規則,讓常用功能自動下載,其他功能需要時再下載。

{% img /images/download.png 36 %}first-app_2017-09-05.zip