LoginSignup
95
91

More than 3 years have passed since last update.

AngularのNgModuleを使って、アプリの構成を管理する

Last updated at Posted at 2017-06-20

この記事はAngular+Firebaseでチャットアプリを作るのエントリーです。
前記事:Angular+FirebaseRTDBでCRUD(CREATE, READ, UPDATE, DELETE)を実装する
次記事:Angularのルーティング設定(基礎編)

この記事で行うこと

ここまでの記事では、ルーティング(ページ遷移)のない単一ページのWEBアプリを構築してきました。
ですが、これから認証機能を実装していくにあたって、ログインページなど他ページへの遷移や、ヘッダーなどサイト全体に共有するテンプレートが必要になってきます。
本稿ではそうした中規模以上のWEBアプリ構築をするため、モジュール構成の整理とヘッダーのテンプレート作成を行います。

Angularのモジュラリティシステム

以前の記事でも少し触れましたが、AngularはNgModuleというモジュラリティシステムを導入しています。
モジュールは、モジュール内部で扱うコンポーネント、サービス、外部ライブラリなどを定義しており、独立して動作するものになっています。これらのモジュールはLazy Loading(遅延読み込み)の対象とすることができ、一度に読み込ませる範囲の指定にもなります。

Lazy Loadingの例

検索にかかる機能を「サーチモジュール」とした場合、それにかかるページに遷移、ないしはその機能を使用しない限り読み込みが実行されない→ローディング時間が短くなる。

Angularのガイドではモジュールの種類を、最初に読み込まれるルートモジュール、アプリ全体に適用させるコアモジュール、機能単位で切り出された機能モジュール、共通部品を格納する共有モジュールの4つに分類しています。
下記ではコアモジュールルートモジュールと共有モジュールの実装方法を紹介します。

コアモジュールの扱いについて
Angular7の公式ドキュメントから、コアモジュールについての記述が削除されました。
これはサービスがシングルトンに対応することになったことに伴い、ルートモジュールとコアモジュールの機能をあえてわける必要がなくなったためのようです。(参考:https://github.com/angular/angular/issues/29848)
コアモジュールが使えなくなったわけではないので、ここでの記述も残そうかと思いましたが、チュートリアルという性質上修正するのが妥当と思い、以下の記述を修正しています。


(2018/1追記)記述を現時点で最新のものに差し替えました。
(2018/9追記)記述を現時点で最新のものに差し替えました。
(2020/6追記)記述を現時点で最新のものに差し替えました。


実装内容

モジュールを整理する

ここまでで作成した単一ページのAngularプロジェクト(/src/app以下)は次のようになっています。

 app
  ├── class
  │    ├── chat.spec.ts
  │    └── chat.ts
  ├── pipe
  │    ├── chat-date.pipe.spec.ts
  │    └── chat-date.pipe.ts
  ├── app.component.css
  ├── app.component.html
  ├── app.component.spec.ts
  ├── app.component.ts
  ├── app.module.ts
  └── app-routing.module.ts

この構成にAngular CLIを使ってモジュールとコンポーネントを追加します。

ng g module shared
ng g component chat

app.module.tsルートモジュール、新しく作成されたshared.module.ts共有モジュールとして扱っていきます。

まずはルートモジュールと共有モジュールの内容を見ていきます。

子コンポーネントを表示する

ユーザーがアプリを起動後、最初に実行されるのがルートモジュールです。
ルートモジュールであるapp.module.tsを見てみると、@NgModuleというデコレータのbootstrapというプロパティにAppComponentが指定されています。
これは最初に読み込むコンポーネントをAppComponentに指定したという意味になります。

指定されたAppComponentapp.component.tsに書かれているコンポーネントで、app.component.htmlをhtmlテンプレートとして指定しています。今回はこのテンプレートに上記で作成したChatComponentを表示させます。

chat.component.ts
@Component({
  selector: 'app-chat',
  templateUrl: './chat.component.html',
  styleUrls: ['./chat.component.css']
})

chat.component.tsを開くと、@Componentデコレータの中にselector: 'app-chat'という記述があります。これがこのコンポーネントを指定するカスタムタグ名となります。

では、app-chatをカスタムタグとしてapp.component.htmlに挿入します。

app.component.html
    <div class="card-header">
      NgChat <app-chat></app-chat>
    </div>

実行結果

NgChat (8).png

ChatComponentのテンプレート内容がapp.component.htmlに表示されました。このとき、ChatComponentAppComponentの「子コンポーネント」である、ということができます。
ルートのコンポーネントであるAppComponentには全ページに共通するものを残していくので、AppComponentの内容をChatComponentに移していきます。

app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-chat></app-chat>`, // templateに変更
  // styleUrlsを削除
})

export class AppComponent {

  constructor() {
  }

}
app.component.html
<!--内容をchat.component.htmlに移動して削除-->
app.component.css
/* 内容をchat.component.cssに移動 */
chat.component.ts
import { Component, OnInit } from '@angular/core';
import { Comment, User } from '../class/chat';
import { AngularFirestore } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

const CURRENT_USER: User = new User(1, 'Tanaka Jiro');
const ANOTHER_USER: User = new User(2, 'Suzuki Taro');

@Component({
  selector: 'app-chat',
  templateUrl: './chat.component.html',
  styleUrls: ['./chat.component.css']
})
export class ChatComponent implements OnInit {

  public content = '';
  public comments: Observable<Comment[]>;
  public currentUser = CURRENT_USER;

  // DI(依存性注入する機能を指定)
  constructor(private db: AngularFirestore) {
  }

  ngOnInit(): void {
    this.comments = this.db
      .collection<Comment>('comments', ref => {
        return ref.orderBy('date', 'asc');
      })
      .snapshotChanges()
      .pipe(
        map(actions => actions.map(action => {
          // 日付をセットしたコメントを返す
          const data = action.payload.doc.data() as Comment;
          const key = action.payload.doc.id;
          const commentData = new Comment(data.user, data.content);
          commentData.setData(data.date, key);
          return commentData;
        })));
  }

  // 新しいコメントを追加
  addComment(e: Event, comment: string) {
    if (comment) {
      this.db
        .collection('comments')
        .add(new Comment(this.currentUser, comment).deserialize());
      this.content = '';
    }
  }

  // 編集フィールドの切り替え
  toggleEditComment(comment: Comment) {
    comment.editFlag = (!comment.editFlag);
  }

  // コメントを更新する
  saveEditComment(comment: Comment) {
    this.db
      .collection('comments')
      .doc(comment.key)
      .update({
        content: comment.content,
        date: comment.date
      })
      .then(() => {
        alert('コメントを更新しました');
        comment.editFlag = false;
      });
  }

  // コメントをリセットする
  resetEditComment(comment: Comment) {
    comment.content = '';
  }

  // コメントを削除する
  deleteComment(key: string) {
    this.db
      .collection('comments')
      .doc(key)
      .delete()
      .then(() => {
        alert('コメントを削除しました');
      });
  }
}

これでAppComponentにかかる記述がかなりすっきりしました。内容を移すにあたり、変更を加えた点を列記しておきます。

templateに変更

app.component.tsではこれまで外部ファイルをhtmlテンプレートに指定するtemplateUrlというプロパティを使っていましたが、これをtemplateに変更しています。
templateはデコレータ内に直接テンプレートを書き込むためのプロパティです。テンプレートの記述内容が少ない時などに使用します。

constructor()の記述をngOnInit()に移す

ngOnInit()はコンポーネントが読み込まれる際に最初に実行される関数です。役割的にはconstructor()と同じなのですが、データのライフサイクルをわかりやすくしておくため、最初に読み込まれるものは極力ngOnInit()に記述するようにしていきます。

Angularのライフサイクル。機会があったら取り上げてみます。

共有モジュールを読み込む

さて、次は共有モジュールを作成していきます。上記でも触れましたが、共有モジュールはアプリ内で使用する共通部品を管理するモジュールです。これまでに作成したものだとChatDatePipeが共通部品として使えそうです。
ngIFやngForを扱うCommonModule、ngModelなどを扱うFormsModuleと合わせて共有モジュールに登録します。

まず/src/app/shared配下に/src/app/pipeを移動し、SharedModuleChatDatePipeCommonModuleFormsModuleを追加します。

src/app/shared/shared.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from "@angular/forms"; // 追加

import { ChatDatePipe } from "./pipe/chat-date.pipe"; // 追加

@NgModule({
  imports: [
    CommonModule,
    FormsModule, // 追加
  ],
  exports: [  // 追加
    CommonModule,
    FormsModule,
    ChatDatePipe
  ],
  declarations: [ // 追加
    ChatDatePipe
  ]
})
export class SharedModule { }

注意すべき点として、SharedModuleの外部で使う部品についてはexportsにも登録する必要があります。これを忘れるとDIをする時に「宣言されていません」というエラーが出るので、必ず登録しておくようにします。

次にAppModuleを更新します。先ほどSharedModuleに追加したものを削除し、代わりにSharedModuleをインポートします。
ここではAppModuleに登録を行っていますが、SharedModuleは必ずしもルートモジュールに登録する必要はありません。
SharedModuleを必要とするモジュールがあればその都度登録を行い、そのモジュールが読み込まれる際にSharedModuleも合わせて読み込まれます。

src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
// FormsModuleを削除
import { environment } from '../environments/environment';
import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { AngularFireAuthModule } from '@angular/fire/auth';

import { SharedModule } from './shared/shared.module'; // 追加
import { AppComponent } from './app.component';
import { ChatComponent } from './chat/chat.component'; // 追加
// ChatDatePipeを削除


@NgModule({
  declarations: [
    AppComponent,
    ChatComponent
  ],
  imports: [
    NgbModule.forRoot(),
    // FormsModuleを削除
    BrowserModule,
    SharedModule, // 追加
    AngularFireModule.initializeApp(environment.firebase),
    AngularFirestoreModule,
    AngularFireAuthModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

これで共有モジュールの読み込みができるようになりました。
この時点で/src/app配下は次のようになっています。

 app
  ├── chart
  │    ├── chat.component.css
  │    ├── chat.component.html
  │    ├── chat.component.spec.ts
  │    └── chat.component.ts
  ├── class
  │    ├── chat.spec.ts
  │    └── chat.ts
  ├── shared
  │    ├── pipe
  │    │   ├── chat-date.pipe.spec.ts
  │    │   └── chat-date.pipe.ts
  │    └── shared.module.ts
  ├── app.component.spec.ts
  ├── app.component.ts
  ├── app.module.ts
  └── app-routing.module.ts

ヘッダーテンプレートを作る

次はルートモジュールにヘッダーを作っていきます。
Angular CLIでルートモジュール内にヘッダーコンポーネントを作成します。

ng g component header

上記コマンドを叩くと自動的にコンポーネントが作成されますが、合わせてルートモジュールも更新がかかります。Angular CLIはディレクトリの階層構造をみて、該当するモジュールに自動でコンポーネントを登録する機能を備えています。慣れるとかなり楽チンです。

これでヘッダーコンポーネントのルートモジュールへの登録が完了しました。
ルートコンポーネントでヘッダーのカスタムタグが使えるようになったので、テンプレートに追加します。

src/app/app.component.ts
@Component({
  selector: 'app-root',
  template: ` 
        <app-header></app-header> <!--追加-->
        <app-chat></app-chat>
  `, 
  styleUrls: ['./app.component.css']
})

あとはヘッダーのテンプレートをモリモリ更新するだけです。
ヘッダー固定のレイアウトにするので、chat.component.cssの調整もしておきます。

src/app/header/header.component.html
<nav class="navbar fixed-top navbar-dark bg-primary">
    <a class="navbar-brand" href="#">NgChat</a>
    <span class="navbar-text">
        <a href="#">Login</a>
    </span>
</nav>
src/app/chat/chat.component.css
.page section {
  margin: 65px 10px 30px; /*margin-topを65pxに*/
}

実行結果

localhost_4200_ (1).png

これで中規模アプリを構築していく準備が整いました。
次はログインページへのルーティングを扱います。

ソースコード

この時点でのソースコード
※firebaseのapiKeyは削除しているので、試すときは自身で作成したapiKeyを入れてください。

95
91
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
95
91