こまぶろ

技術のこととか仕事のこととか。

Angularで「ネストされたルート」を実装する(シンプルな実装 / モジュール切り出し / 遅延ロード)

この記事は write-blog-every-week Advent Calendar 2018 11日目の記事です。

前日の記事は、nitt-san(@nitt_san)さんの「インプットからアウトプットへ -2017年〜2018年の自分を振り返る-」でした。

nitt-san.hatenablog.com

Angular のルーティング機能を試したい

以前、Vue.jsを勉強していた際に、Vue Routerの「ネストされたルート」を扱う方法を試した。

ky-yk-d.hatenablog.com

Vue.jsにおける「ネストされたルート」は、Vue Routerの公式ガイドにも記載されている名称だ。

実際のアプリケーションの UI では通常複数のレベルの階層的にネストしたコンポーネントで構成されます。ネストされたコンポーネントの特定の構造に対して URL のセグメントを対応させることはよくあります。

今回は、Angularでこれを実現してみる*1。具体的には、

1.ルートモジュール単体で実装する
2.フィーチャーモジュールとそのルーティングモジュールを切り出す(通常ロード)
3.URLアクセス時に遅延ロードするように修正する

の三段階で実装していく。1の段階で既に上の記事で書いた「ネストされたルート」自体は実現できているのだが、その先に一歩進んで、フィーチャーモジュールの切り出しと遅延ロードも実装する。フィーチャーモジュールの切り出しはアプリの規模が大きくなると不可欠だし、遅延ロードもそれと関連した重要な機能だ。公式ドキュメントでも、「フィーチャーモジュールの遅延ロード」という項目が立っている。

サンプルコードは下記リポジトリを参照。各段階の事後の状態をぞれぞれ、

1.beforeブランチ
2.normal-loadブランチ
3.lazy-loadブランチ

として切ってある。

https://github.com/ky-yk-d/router-samplegithub.com

単一モジュールでの実装

今回は、最もシンプルなネスト構造を持つアプリケーションとして、下に示すようなSPAを実装する。最初に、これを単純にルートモジュールとルートのルーティングモジュールに記述してこのアプリを実装する(第1段階)。その後、このアプリケーションを機能上は変更せずに、リファクタリング(第2段階)および遅延ロード化(第3段階)をおこなっていく。

f:id:ky_yk_d:20181210231309g:plain

プロジェクトの作成〜コンポーネントの作成

Angular CLIを利用してプロジェクトの雛形とコンポーネントを作成する。

ng new router-sample
# ルーティングを利用するか聞かれるので"y"を選択
# スタイルシートの形式は何を選んでもよい
cd router-sample
ng generate component home # AppComponentの直下(1)
ng generate component feature # AppComponentの直下(2)
ng generate component feature/main # FeatureComponentの子要素(1)
ng generate component feature/sub #FeatureComponentの子要素(2)

以上を実行すると、/src/app配下は下記のように構成になる。

.
├── app-routing.module.ts
├── app.component.css
├── app.component.html
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── feature
│   ├── feature.component.css
│   ├── feature.component.html
│   ├── feature.component.spec.ts
│   ├── feature.component.ts
│   ├── main
│   │   ├── main.component.css
│   │   ├── main.component.html
│   │   ├── main.component.spec.ts
│   │   └── main.component.ts
│   └── sub
│       ├── sub.component.css
│       ├── sub.component.html
│       ├── sub.component.spec.ts
│       └── sub.component.ts
└── home
    ├── home.component.css
    ├── home.component.html
    ├── home.component.spec.ts
    └── home.component.ts

以上で、今回のアプリケーションに必要なコンポーネントは揃っている*2。今回のサンプルアプリでは、ネストの構造がわかりやすいように少しだけHTMLを編集しているが、本質的ではないので説明は省略する。

AppRoutingModuleを修正する

次に、AppRoutingModule(/src/app/app-routing.module.ts)を修正していく。このファイルは、ng newコマンド実行時に「ルーティングを使用する」を選択すると自動で作成されるが、自分で作っても良い。

/* import文省略 */
const routes: Routes = [
/* 追加する部分はここから */
  {path: '', component: HomeComponent}, 
  {path: 'feature', component: FeatureComponent,
    children: [
      {path: '', component: MainComponent},
      {path: 'sub', component: SubComponent}
    ]
  }
/* ここまで */
];

@NgModule({
  imports: [RouterModule.forRoot(routes)], // forRoot() を利用していることに注目
  exports: [RouterModule]
})
export class AppRoutingModule { }

forRoot()に渡しているroutes変数に、複数の要素を付加していく。path属性で指定したURLにアクセスすると、component属性で指定したコンポーネントが表示される。そして、ネストされたルートはchildren要素で指定すればよい。ここでは、FeatureComponentの内側のrouter-outletタグに、/featureにアクセスした場合にはMainComponentが、/feature/subにアクセスした場合にはSubComponentが表示されるように設定している。

ここまでで、実装ができたはずだ。改めて、このブランチのURLを貼っておく。

フィーチャーモジュールの切り出し(通常のロード)

ここまでで、機能としては実現できている。しかし、ルートモジュールであるAppModuleが個々のコンポーネントを全て知っている状態になっているのが気になる。今回のサンプルコード程度のコンポーネント数であれば問題はないが、長期的には管理しづらくなってしまう。そこで、Angularのモジュールの機能を用いる。モジュールとは、コンポーネントやサービスなどのファイルを束ねるためのものだ。

ちなみに、AppModuleも名前が示しているようにモジュールであり、これを/src/main.tsで下記のように指定することで、Angularアプリケーションが立ち上がった際に読み込まれるようになっている。

/* 略 */
platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

原則として、AppModuleで読み込んでいるモジュールがそのアプリケーションで使えるモジュールとなる*3。逆に言えば、AppModuleは下位にあるフィーチャーモジュールを読み込むことさえすれば、あとの管理は(ルーティングを含めて)そのフィーチャーモジュールに委譲することができるというわけだ。

Angular CLI を用いたモジュールの作成

さて、モジュールを作成していく。ここでもAngular CLIを利用する。

ng generate module feature --routing

上記のコマンドにより、Angular CLI で feature のモジュールを作成する。--routing オプションを付与することで、下記の2つのモジュールが生成される。

  • /feature/feature.module.ts
  • /feature/feature-routing.module.ts

github.com

FeatureRoutingModuleへのルーティング記述の移植

AppRoutingModuleに記載していたルーティングの記述を、FeatureRoutingModule(/feature/feature-routing.module.ts)に移植していく。ここはカット&ペーストしてしまえばよい。

/* import文省略 */
const routes: Routes = [
/* 移植したのはここから */
  {path: 'feature', component: FeatureComponent,
  children: [
    {path: '', component: MainComponent},
    {path: 'sub', component: SubComponent}
  ]}
/* ここまで */
];
 @NgModule({
  imports: [RouterModule.forChild(routes)], // AppRoutingModuleではforRoot()だったのがforChild()であることに注目
  exports: [RouterModule]
})
export class FeatureRoutingModule { }

FeatureModuleにコンポーネントを移植する

ルーティングの設定は以上で完了だ。しかし、このままではアプリケーションは機能しない。AppModuleでimportされ、declarationsに加えられていたフィーチャーコンポーネント(Feature/Main/Sub)を、フィーチャーモジュールに移植してやらないといけない。また、せっかく作ったFeatureRoutingModuleをFeatureModuleでimportしていることも確認しておく。

/src/app/feature/feature.module.ts

/* import文省略 */
@NgModule({
  declarations: [
    FeatureComponent, // AppModuleから移植
    MainComponent, // AppModuleから移植
    SubComponent // AppModuleから移植
  ],
  imports: [
    CommonModule,
    FeatureRoutingModule, // ルーティングモジュールをimportしている
  ]
})
export class FeatureModule { }

これで、フィーチャーのコンポーネントについての情報はルートのルーティングモジュールからは削除できる。その代わりに、FeatureModuleをAppModuleでimportすることで、間接的にFeatureRoutingModuleを利用できるようにする。

/* import文省略 */
@NgModule({
  declarations: [
    AppComponent,
    HomeComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule, 
    FeatureModule // これを追加する
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

これで、ルートであるAppModuleとAppRoutingModuleからはフィーチャーのコンポーネントやルーティングについての知識が消え、FeatureModuleに委譲することができた。フィーチャーのコンポーネントを読み込むのも、それを特定のURLに割り当てるのも、FeatureModuleの役割ということになる。

遅延ロードへの変更

さて、最後に、遅延ロードへの変更を行う。遅延ロードとは、読んで字のごとく、フィーチャーのコンポーネント(やサービスなど、モジュールに含まれるもの)の読み込みを必要になったタイミングまで遅らせることだ。先に読み凍んでおくことでスムーズな画面の切り替えが可能になるのがSPAの良さだが、あまり使われないのに重い画面などがある場合には却って初回のロードが長くなってしまう。遅延ロードを利用することで、読み込みのタイミングをずらすことができる。

AppRoutingModuleにルーティングを再び追加する

遅延ロードは、フィーチャーモジュールの読み込みを、URLへのアクセスのタイミングまで遅らせることによって実現する。そうであるとすれば、第二段階でしたように、/featureというパス自体の情報をFeatureModule(より正確に言えば、それが読み込んでいるFeatureRoutingModule)に留めておくわけにはいかない。

app-routing.module.tsに再度パスを追加し、Componentは記載せずにloadChildrenを記載する。読み込みの対象となるフィーチャーモジュールを、[ファイル名(拡張子不要)]#[クラス名]という指定する。ファイルの冒頭でimportする必要はない。

const routes: Routes = [
  {path: '', component: HomeComponent},
  {path: 'feature', loadChildren: './feature/feature.module#FeatureModule'} // パスを再び記載する
];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

FeatureRoutingModuleからURLの情報を削除する

また、/featureというURLの情報はAppRoutingModule側に移してしまったので、FeatureRoutingModuleにはその情報は余計だ。削除する。

const routes: Routes = [
  {path: '', component: FeatureComponent,  // path: 'feature' だったところを path : '' に修正
  children: [
    {path: '', component: MainComponent},
    {path: 'sub', component: SubComponent}
  ]}
];
@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class FeatureRoutingModule { }

FeatureModuleのAppModuleでの読み込みを削除する

最後に、AppModuleでFeatureModuleを読み込むのをやめる。すでに、AppRoutingModuleで遅延してロードするモジュールとしてloadChildren: './feature/feature.module#FeatureModule'と指定しているのだから、AppModuleから先に読み込んでしまってはいけない。

@NgModule({
  declarations: [
     AppComponent,
     HomeComponent
  ],
  imports: [
    BrowserModule,
// ここでFeatureModuleをimportしていた
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]

これで、遅延ロードに変更することができた。遅延ロードができているかどうかを確認したい方は、フィーチャーモジュールのコードのどこかにconsole.log()か何かを仕込んでみるだけでも確認ができる。

まとめ

  • Angular で「ネストされたルート」を実装した
  • ルートモジュールからフィーチャーモジュールを切り出した
  • フィーチャーモジュールを遅延ロードするように変更した

ルーティングを思い通りに実装できると、SPAの骨格を作ることができると思う。まず骨格を作った上で、個々のコンポーネントやサービスを充実させていくのがひとつの道筋になるのではないだろうか。Angularはまだ勉強し始めたばかりなのだが、これから他の機能も試していきたい。


write-blog-every-week Advent Calendar 2018 、明日の記事の担当は、KIDANI Akito@kdnakt さんです!*4

kdnakt.hatenablog.com

*1:筆者はAngularの入門者であることをあらかじめご了承願いたい。もし記述に誤りを見つけた方がいれば、コメントやTwitter、GitHubでご連絡をいただけるとありがたい。

*2:ng generateコマンドでコンポーネントを作成すると、自動的にapp.module.tsが更新され、新規作成したコンポーネントをimport文で読み込み、declarationsに追加してくれる。手動でコンポーネントを作る場合は、モジュールに追加するのを忘れてはならない。

*3:遅延ロードはその例外という位置付けになる。

*4:公開されたのでリンクを貼った。