Angular Lazy LoadingでつまずいちゃうNgModuleのコンポーネント登録

AngularではLazyloadモジュールという遅延読み込みを行うことができる機能が提供されており、ページ遷移した時に必要なモジュールだけ読み込むように設定することができます。

しかしLazy Loading(遅延読み込み)をいざ実装するとなるとNgModuleのコンポーネント登録でつまづいた点もあってサンプルアプリケーションを例になるべくわかりやすく説明します

# はじめに
■前提条件
既に[Angular公式のLazy Loadingの説明](https://angular.jp/guide/lazy-loading-ngmodules)等でLazy Loadingについて基本的な概念や実装方法はなんとなく理解していればOKです


■進め方
どんな問題につまづくか、いくつかのパターンがあります。
各パターンごとに問題につまづく**前**と問題につまづき修正した**後**の完成版二つのサンプルコードをStackBlitzで用意しています。
アプリケーションの内容についてどのパターンもベースは同じですが、記事を読み進めるにつれてコードを追加ししより発展する形になります。

まずは記事に沿って問題につまづく**前**のサンプルコードから編集し、実際にLazy Loadingでどんなとこにはまってどのように解決すれば良いのかを体験してください。
もし編集してみたんだけど解決できないという場合は完成版と比較してみましょう。
※どんなエラーが発生しているかを知るためにコンソールは開いておいてください。

サンプルアプリケーションの最初はヘッダーがあるレイアウトで既にいくつかのフィーチャーモジュールをLazy Loadingしている場面からスタートします。
https://stackblitz.com/edit/angular8-lazy-loading-sample-pattern1

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71113/a76b3643-1c3b-286d-18b6-0156f83b092b.png)

```app-routing.module.ts
import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: 'admin', loadChildren: () => import('./modules/admin/admin.module').then(m => m.AdminModule)},
  { path: 'home', loadChildren: () => import('./modules/home/home.module').then(m => m.HomeModule)},

];

@NgModule({
  imports: [ RouterModule.forRoot(routes) ],
  exports: [ RouterModule ]
})
export class AppRoutingModule {}
```

フィーチャーモジュールでは同じ階層にあるルーティングモジュールをインポートし、ルーティングのデフォルトとしてページコンポーネントを指定しています。
AdminModuleのフィーチャーモジュールであればAdminRoutingModuleをインポートし、`/admin`でアクセスされた時にAdminComponentを表示します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71113/ae9eca99-e85c-bf8e-5a6f-c99e5d4383d1.png)


# つまずき集
## つまずき1. 親モジュールが使用しているコンポーネントを子モジュールでも使用する
### つまづき
本記事ではわかりやすくする為に親モジュールと子モジュールという言葉を使います。

- 親モジュール：AppModuleもしくはAppModuleから読み込むモジュール
- 子モジュール：Lazy Loadingしているフィーチャーモジュール

まずはこちらで用意した[サンプルコード](https://stackblitz.com/edit/angular8-lazy-loading-sample-pattern1)を開いてください。

ヘッダーを表示するheader.component.htmlでは他のページへ遷移するリンクとして`app-link`というコンポーネントを表示しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71113/f2e82666-8087-c665-8e54-96664f7e6263.png)

これをLazy Loadingしているコンポーネントでも使用したいとします。
試しにhome.component.htmlで`app-link`を追加します。
すると内容が表示されずエラーが発生します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71113/50e97bc6-e4a5-ed33-3215-46224ff50f60.png)

```
Error: Template parse errors:
'app-link' is not a known element:
1. If 'app-link' is an Angular component, then verify that it is part of this module.
2. If 'app-link' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("
	home works!
</p>
```

`app-link`は未定義なHTMLタグとして認識されているようですね。

LinkComponentはAppModuleで登録されているだけなので、HomeModuleでも登録してみます。

```home.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HomeComponent } from '../../components/pages/home/home.component';
import { HomeRoutingModule } from './home-routing.module'
import { LinkComponent } from '../../components/parts/link/link.component';

@NgModule({
  imports: [
    CommonModule,
    HomeRoutingModule
  ],
  declarations: [
    HomeComponent, 
    LinkComponent // 追加
  ]
})
export class HomeModule { }
```

しかし、今度は別のエラーが発生します。

```
ERROR Error: Uncaught (in promise): Error: Type LinkComponent is part of the declarations of 2 modules: AppModule and HomeModule! Please consider moving LinkComponent to a higher module that imports AppModule and HomeModule. You can also create a new NgModule that exports and includes LinkComponent then import that NgModule in AppModule and HomeModule.
Error: Type LinkComponent is part of the declarations of 2 modules: AppModule and HomeModule! Please consider moving LinkComponent to a higher module that imports AppModule and HomeModule. You can also create a new NgModule that exports and includes LinkComponent then import that NgModule in AppModule and HomeModule.
```

LinkComponentはAppModuleとHomeModuleで重複登録されているよ！と怒られます。。
「いったいどうしろと。。」という状態になったところで解決編に移りましょう。

### 解決
まずLazy Loadingを行う場合は特にそうですが、NgModuleのコンポーネント登録には重要な**大原則**があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71113/965fc852-e0af-92ca-3a1e-fdfb3a829820.png)

この大原則を知らない、もしくは忘れて開発を進めるとえらい目にあいかねないので是非覚えてください。

実際に起こったエラーを解決していきます。
まずLinkComponentを一度だけ登録する共通の新しいモジュールをmodulesディレクトリ内に作成します。

StackBlitzではngコマンドをGUIで代わりに実行するための機能が提供されています。
moduleディレクトリで右クリックすると操作メニューが表示されるのでその中で`Angular Generator`を選択してください。
そうするとその中から何を新規作成するかを選べるので`Module`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71113/18a31790-61d6-8f65-0960-57a16a951b3c.png)

そうするとファイル名入力欄が表示されるので`shared`と入力してEnterを押すとmodulesディレクトリの中にsharedディレクトリが作成されて、さらにその中に`shared.module.ts`が作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71113/6c3ec5a7-d74f-4788-d213-30229df3c078.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71113/ec2c53ea-085b-dcbb-3dbd-1596f98f5dac.png)

次に新しく作成したShareModuleにLinkComponentの登録を行います。

```shared.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LinkComponent } from '../../components/parts/link/link.component';
import { RouterModule } from '@angular/router';

@NgModule({
  imports: [
    RouterModule
  ],
  declarations: [LinkComponent],
  exports: []
})
export class SharedModule {}
```

そしてLinkComponentを使えるようAppModuleとHomeModuleの両方でSharedModuleをインポートします。

```ts
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { HeaderComponent } from "./components/parts/header/header.component";
import { SharedModule } from "./modules/shared/shared.module";

@NgModule({
  imports: [BrowserModule, FormsModule, AppRoutingModule, SharedModule],
  declarations: [AppComponent, HeaderComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

```

```home.module.ts
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { HomeComponent } from "../../components/pages/home/home.component";
import { HomeRoutingModule } from "./home-routing.module";
import { SharedModule } from "../shared/shared.module";

@NgModule({
  imports: [CommonModule, HomeRoutingModule, SharedModule],
  declarations: [HomeComponent]
})
export class HomeModule {}
```

さあ、これで動くか・・！？と思ったら安心してください、まだ動きません！泣
NgModuleとつきあっていくには根気と忍耐力が必要なのです(遠い目)

実際のエラーは以下が出ます。始めのエラーに逆戻りしましたね。

```
Error: Template parse errors:
'app-link' is not a known element:
1. If 'app-link' is an Angular component, then verify that it is part of this module.
2. If 'app-link' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("
```

実はもう一つ大事な修正があります。 それがNgModuleの**exports**です。
つまりただ上位のモジュールにコンポーネント登録を移してインポートしてもちゃんとexportsが指定されていなければコンポーネントが提供されないのです。

なのでSharedModuleのexportsにLinkComponentを指定してください。

```
  declarations: [LinkComponent],
+  exports: [LinkComponent]
```

無事に動くようになりましたね! これで完成です！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71113/fe81d316-09d1-ec43-1813-47678ceedef8.png)


[サンプルコード完成版](https://stackblitz.com/edit/angular8-lazy-loading-sample-pattern1-fixed)

## つまずき2. 複数の子モジュールで同じコンポーネントを使用する
### つまづき
この[サンプルコード](https://stackblitz.com/edit/angular8-lazy-loading-sample-pattern2)からスタートしてください。

サンプルコードはつまずき1の完成版からボタンのコンポーネントを追加してhome.component.htmlで表示するよう修正しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71113/0d74a6a6-3083-05de-529a-e26c03e64494.png)

またコンポーネント登録はHomeModuleで行っています。

```home.module.ts
declarations: [HomeComponent, ButtonComponent]
```

さて`app-buttton`のコンポーネントをもう一つの`/admin`ページでも使用したい場合はどうすれば良いでしょうか？

試しに`admin.component.html`と`admin.module.ts`を以下のように修正してください。

```admin.component.html
<p>
	admin works!
</p>
<app-button></app-button>
```

```admin.module.ts
declarations: [
  AdminComponent, 
  ButtonComponent // 追加
] 
```

もうお気づきだと思いますが、つまづき1と同様にコンポーネント重複登録エラーが発生します:eyes:

```
Error: Type ButtonComponent is part of the declarations of 2 modules: HomeModule and AdminModule! Please consider moving ButtonComponent to a higher module that imports HomeModule and AdminModule.
```

### 解決

解決はつまずき1と同じく原則に従います。
**「コンポーネントを複数のモジュールで使用したい場合は、より上位のモジュールで登録して、そのモジュールを読み込むこと！」**

AdminModuleとHomeModuleからButtonComponentの登録を削除し、代わりにSharedModuleで登録を行います(exportsも忘れずに!)

```shared.module.ts
import { ButtonComponent } from "../../components/parts/button/button.component";

@NgModule({
  imports: [
    RouterModule
  ],
  declarations: [LinkComponent, ButtonComponent],
  exports: [LinkComponent, ButtonComponent]
})

```

仕上げにAdminModuleでSharedModuleをインポートします。

```admin.module.ts
import { SharedModule } from "../shared/shared.module";

@NgModule({
  imports: [CommonModule, AdminRoutingModule, SharedModule],
  declarations: [AdminComponent]
})
```

エラーが消えて動くようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71113/bb224632-37a1-0c73-6214-4ddc614e066f.png)

[サンプルコード完成版](https://stackblitz.com/edit/angular8-lazy-loading-sample-pattern2-fixed)


# まとめ
本記事の例はシンプルなものですが、より複雑なコンポーネント階層になった時にはどのモジュールにコンポーネントを登録するかで同じようにつまづく場合があるかと思います。

そんな時は以下の原則を思い出してください。

- **どんなコンポーネントもNgModuleに登録できるのは一度だけ！**
- **コンポーネントを複数のモジュールで使用したい場合は、より上位のモジュールで登録して、そのモジュールを読み込むこと！** 
- **exportsを忘れるな!**

以上、ありがとうございました！