如何在 Angular 建立 Lazy Loading Module ?
隨著 app 開發越來越大,若將所有的 route 都寫在 AppModule,除了難以維護外,還必須在一開始就載入全部 component,使得 Angular 載入時間變久;比較好的方式是將 component 切成 module,並有自己的 route,當 user 點入該 route 時,才去下載該 module,這就是 lazy loading。
Version
macOS High Sierra 10.13.3
Node.js 8.9.4
Angular CLI 1.6.7
Angular 5.2.4
User Story

原本只有一個 AppModule:
- 按下
Login會載入LoginComponent - 按下
Post會載入PostComponent - 按下
Home會載入AppComponent
Task
拆成多個 module,並採用 lazy loading。
Architecture
除了一定要有的 AppModule 外
- 將
LoginComponent獨立成LoginModule - 將
PostComponent獨立成PostModule
並且對 LoginModule 與 PostModule 使用 lazy loading。
Implementation
建立新 Module
建立 LoginModule
1 | ~/ MyProject $ cd src/app |
原本 LoginComponent 是建立在 src/app/login 目錄下,目前想將 LoginComponent 變成 module,由於 Angular CLI 建立 module 時,會建立子目錄,因此先將目錄切到 src/app ,建立 LoginModule 在 src/app/login 目錄下。
g: generate 的縮寫m: module 的縮寫—-routing: 建立 module 時,順便建立RoutingModule

- 將目錄切到
src/app下,使用ng g m [module name] —-routing建立 module 與 routing module - Angular CLI 將會建立
login.module.ts與login-routing.module.ts兩個檔案 - 將原本在
src/app/login目錄下的LoginComponent重構到src/app/login/login目錄下
目前
src/app/login/login看起來很彆扭,事實上src/app/login為 module 目錄,而src/app/login/login為 component 目錄,將來會有更多 component,因此特別將原本在src/app/login的LoginComponent重構到src/app/login/login,目前只是因為LoginModule與LoginComponent同名,所以看起來很怪
建立 PostModule

- 將目錄切到
src/app下,使用ng g m [module name] —-routing建立 module 與 routing module - Angular CLI 將會建立
post.module.ts與post-routing.module.ts兩個檔案 - 將原本在
src/app/login目錄下的PostComponent重構到src/app/post/post目錄下
目前
src/app/post/post看起來很彆扭,事實上src/app/post為 module 目錄,而src/app/post/post為 component 目錄,將來會有更多 component,因此特別將原本在src/app/post的PostComponent重構到src/app/post/post,目前只是因為PostModule與PostComponent同名,所以看起來很怪
設定新 Module
login-routing.module.ts
1 | import { LoginComponent } from './login/login.component'; |
第 5 行
1 | { path: '', component: LoginComponent }, |
將原本在 AppRoutingModule 的 route
1 | { path: 'login', component: LoginComponent }, |
重構到 LoginRoutingModule 內,因為已經在 LoginRoutingModule 內,所以 path 為 '' 即可。

- 編輯
login-routing.module.ts - 將原本在
app-routing.module.ts的 login route 重構到login-routing.module.ts
login.module.ts
1 | import { NgModule } from '@angular/core'; |
11 行
1 | declarations: [ |
因為 LoginRoutingModule 已經使用到 LoginComponent,因此在 LoginModule 需要加以宣告。

- 編輯
login.module.ts - 在
declarations加上LoginComponent
post-routing.module.ts
1 | import { NgModule } from '@angular/core'; |
第 5 行
1 | { path: '', component: PostComponent }, |
將原本在 AppRoutingModule 的 route
1 | { path: 'post', component: PostComponent }, |
重構到 PostRoutingModule 內,因為已經在 PostRoutingModule 內,所以 path 為 '' 即可。

- 編輯
post-routing.module.ts - 將原本在
app-routing.module.ts的 post route 重構到post-routing.module.ts
post.module.ts1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PostComponent } from './post/post.component';
import { PostRoutingModule } from './post-routing.module';
@NgModule({
imports: [
CommonModule,
PostRoutingModule
],
declarations: [
PostComponent
]
})
export class PostModule { }
11 行
1 | declarations: [ |
因為 PostRoutingModule 已經使用到 PostComponent,因此在 PostModule 需要加以宣告。

- 編輯
post.module.ts - 在
declarations加上PostComponent
設定 AppModule
app-routing.module.ts1
2
3
4
5
6
7
8
9
10
11
12
13
14import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{ path: 'login', loadChildren: 'app/login/login.module#LoginModule' },
{ path: 'post', loadChildren: 'app/post/post.module#PostModule' },
{ path: '', redirectTo: '', pathMatch: 'full'}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
第 5 行
1 | { path: 'login', loadChildren: 'app/login/login.module#LoginModule' }, |
若要使用 lazy loading module,必須從 component 改成 loadChildren,後間接的是字串,為 login.module.ts 的路徑,但不用加上 .ts。
最後以 # 加上 LoginModule,為 module 名稱。

app.module.ts1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
第 7 行
1 | declarations: [ |
若只有 AppModule,則 declarations 必須包含全部 component,但因為目前已經切出 LoginModule 與 PostModule,而 LoginComponent 與 PostComponent 已經在 LoginModule 與 PostModule 宣告過,所以 AppModule 只需宣告 AppComponent 即可。

Experiment
理論上使用了 lazy loading module,應該會看到兩個效果 :
- 出現
0.xxx.chunck.js、1.xxx.chunk.js - 當 route 執行到才會下載
0.xxx.chunck.js、1.xxx.chunk.js
觀察 ng build —prod

當沒有使用 lazy loading module 時,全部的 JavaScript 都在 main.xxx.bundle.js。

當使用 lazy loading modules 後,多出了 0.xxx.chunk.js 與 1.xxx.chunk.js,這就是 LoginModule 與 PostModule 編譯之後的 chunk。
觀察 Chrome

當沒有使用 lazy loading module 時,儘管點了 Login 與 Post,但都沒有任何 request,因為全部都在 main.xxx.bundle.js 中了。

點了 Login 才會下載 login.module.chunk.js;點了 Post 才會下載 post.module.chunk.js,證明 lazy loading module 是有作用的。
Conclusion
- 在這個小小範例中,我們可能看不到 lazy loading module 的威力,但若頁面夠複雜,component 夠多時,若不拆 module,則
main.xxx.bundle.js可能會好幾 MB,此時就必須將 component 拆成 module,配合 lazy loading,只有在 route 被執行時,才會載入該 chunk 的 JavaScript,這樣使用者體驗才會好。
Sample Code
完整的範例可以在 GitHub 上找到