避免所有 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

並且對 LoginModulePostModule 使用 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 ,建立 LoginModulesrc/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.tslogin-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/loginLoginComponent 重構到 src/app/login/login,目前只是因為 LoginModuleLoginComponent 同名,所以看起來很怪

建立 PostModule

lazy001

  1. 將目錄切到 src/app 下,使用 ng g m [module name] —-routing 建立 module 與 routing module
  2. Angular CLI 將會建立 post.module.tspost-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/postPostComponent 重構到 src/app/post/post,目前只是因為 PostModulePostComponent 同名,所以看起來很怪

設定新 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,但因為目前已經切出 LoginModulePostModule,而 LoginComponentPostComponent 已經在 LoginModulePostModule 宣告過,所以 AppModule 只需宣告 AppComponent 即可。

lazy007

Experiment


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

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

觀察 ng build —prod

lazy008

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

lazy009

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

觀察 Chrome

lazy010

當沒有使用 lazy loading module 時,儘管點了 LoginPost,但都沒有任何 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