如何在 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 上找到