避免所有 component 都在同一個 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


lazy012

原本只有一個 AppModule:

  • 按下 Login 會載入 LoginComponent
  • 按下 Post 會載入 PostComponent
  • 按下 Home 會載入 AppComponent

Task


拆成多個 module,並採用 lazy loading。

Architecture


lazy013

除了一定要有的 AppModule 外

  • 將 LoginComponent 獨立成 LoginModule
  • 將 PostComponent 獨立成 PostModule

並且對 LoginModule 與 PostModule 使用 lazy loading。

Implementation


建立新 Module

建立 LoginModule

1
2
~/ MyProject $ cd src/app
~/ MyProject/src/app $ ng g m Login --routing

原本 LoginComponent 是建立在 src/app/login 目錄下,目前想將 LoginComponent 變成 module,由於 Angular CLI 建立 module 時,會建立子目錄,因此先將目錄切到 src/app ,建立 LoginModule 在 src/app/login 目錄下。

  • g : generate 的縮寫
  • m : module 的縮寫
  • —-routing : 建立 module 時,順便建立 RoutingModule

lazy000

  1. 將目錄切到 src/app 下,使用 ng g m [module name] —-routing 建立 module 與 routing module
  2. Angular CLI 將會建立 login.module.ts 與 login-routing.module.ts 兩個檔案
  3. 將原本在 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

lazy001

  1. 將目錄切到 src/app 下,使用 ng g m [module name] —-routing 建立 module 與 routing module
  2. Angular CLI 將會建立 post.module.ts 與 post-routing.module.ts 兩個檔案
  3. 將原本在 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
2
3
4
5
6
7
8
9
10
11
12
13
import { LoginComponent } from './login/login.component';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
{ path: '', component: LoginComponent }
];

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

第 5 行

1
{ path: '', component: LoginComponent },

將原本在 AppRoutingModule 的 route

1
{ path: 'login', component: LoginComponent },

重構到 LoginRoutingModule 內,因為已經在 LoginRoutingModule 內,所以 path 為 '' 即可。

lazy002

  1. 編輯 login-routing.module.ts
  2. 將原本在 app-routing.module.ts 的 login route 重構到 login-routing.module.ts

login.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoginComponent } from './login/login.component';
import { LoginRoutingModule } from './login-routing.module';

@NgModule({
imports: [
CommonModule,
LoginRoutingModule
],
declarations: [
LoginComponent
]
})
export class LoginModule { }

11 行

1
2
3
declarations: [
LoginComponent
]

因為 LoginRoutingModule 已經使用到 LoginComponent,因此在 LoginModule 需要加以宣告。

lazy004

  1. 編輯 login.module.ts
  2. 在 declarations 加上 LoginComponent

post-routing.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { PostComponent } from './post/post.component';

const routes: Routes = [
{ path: '', component: PostComponent },
];

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

第 5 行

1
{ path: '', component: PostComponent },

將原本在 AppRoutingModule 的 route

1
{ path: 'post', component: PostComponent },

重構到 PostRoutingModule 內,因為已經在 PostRoutingModule 內,所以 path 為 '' 即可。

lazy003

  1. 編輯 post-routing.module.ts
  2. 將原本在 app-routing.module.ts 的 post route 重構到 post-routing.module.ts

post.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { 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
2
3
declarations: [
PostComponent
]

因為 PostRoutingModule 已經使用到 PostComponent,因此在 PostModule 需要加以宣告。

lazy005

  1. 編輯 post.module.ts
  2. 在 declarations 加上 PostComponent

設定 AppModule

app-routing.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { 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 名稱。

lazy006

app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { 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
2
3
declarations: [
AppComponent
],

若只有 AppModule,則 declarations 必須包含全部 component,但因為目前已經切出 LoginModule 與 PostModule,而 LoginComponent 與 PostComponent 已經在 LoginModule 與 PostModule 宣告過,所以 AppModule 只需宣告 AppComponent 即可。

lazy007

Experiment


理論上使用了 lazy loading module,應該會看到兩個效果 :

  1. 出現 0.xxx.chunck.js、1.xxx.chunk.js
  2. 當 route 執行到才會下載 0.xxx.chunck.js、1.xxx.chunk.js

觀察 ng build —prod

lazy008

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

lazy009

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

觀察 Chrome

lazy010

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

lazy011

點了 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 上找到

Reference


Angular, Lazy Loading Feature Modules

2018-02-15

▲