Angular7 で俺式 MEAN スタック Webアプリを構築する、ほぼ全手順（1）

## 概要
Angular7 で俺式 MEAN スタックを作るための備忘録。
今回は「クライアント側のベース生成」を行う。

### 前提
2019年1月1日時点の情報です。また、以下の環境になっている前提です。

- Angular CLI： 7.0.6
- Node.js： 10.15.0
- npm： 6.4.1

また、 「ルート（root）」「ルート（route）」「ディレクトリ」「コンポーネント」「ビルド」「スクリプト」などの言葉の概念が何となくわかることが前提です。
知らない場合は、事前に軽く言葉の意味を検索しておいてください。
（わからなくても、記載の通りやっていけばアプリはできます）

### 事前準備

「[npm コマンドで Angular CLI のインストール、バージョンアップする方法（まとめ）](https://qiita.com/tomy0610/items/731a41c8b6b6e5fa25ed)」などを参考に環境は整えておいてください。
`ng` コマンドや `npm` コマンドができる前提で書いていきます。

## Angular アプリ初期生成

フロント側のベースを構築する。

### Angular CLI でアプリ自動生成

まず、以下のコマンドを実行。
スタイルは SCSS で生成する（嫌だったらオプション抜けば選択できる）

```bash
# ng new 任意のアプリ名
ng new demo-app --routing=true --style=scss
```

※ 以降、各コマンドで「アプリ名」を使用するので必ず、コマンド実行時に統一して読み替えていくこと。
※ 生成されたルートディレクトリの名前は変更しても大丈夫
※ もし、Git クローンした空のプロジェクト内で、今回の生成コマンドを行う場合は、Angular CLI で生成されたプロジェクト名のディレクトリ配下のファイルを、Git クローンディレクトリ直下に、`node_modules` と `README.md` 以外は全て移動/コピーする。その後に、要らなくなったディレクトリを消し、もう一度 `npm install` を実行すること。

### 脆弱性が指摘された場合は都度直す

```bash:package.jsonがあるディレクトリ階層
# 脆弱性に対する対応コマンド
npm audit fix
```

このコマンドは、npm インストールする度に、ほぼ毎回やる事になるので、都度求められると思っておいてください。

### PWA対応にする

以下の PWA 対応コマンドで、ServiceWorker が入る。

```bash
# ng add @angular/pwa --project=アプリ名
ng add @angular/pwa --project=demo-app
```
ngsw-config.json, manifest.json 等が増える。
PWA 対応にすると ServiceWorker の機能でブラウザに色々とキャッシュしちゃうので、それが、煩わしい場合はアプリ起動する前に PWA 機能の関連箇所（`src/app/app.module.ts` など）をコメントアウトしても良い。
（※ なんでキャッシュしちゃうのかは ServiceWorker について調べてください）

### Universal にする

```bash
# ng g universal --client-project=アプリ名
ng g universal --client-project=demo-app
```

angular.json に `server` というプロパティが増える。

### AppShell 追加

```bash
# ng g app-shell --client-project=アプリ名 --universal-project=アプリ名
ng g app-shell --client-project=demo-app --universal-project=demo-app
```

アプリシェルのソースコードが生成される。
（※ アプリシェルって何なのかは調べてください）

### Angular Material 導入 （省略可）

[Angular 用のマテリアルデザイン](https://material.angular.io) を導入。
Web アプリをマテリアルデザインにしない場合は、この手順はスキップして良い。

実際の導入手順は「[こちらのQiita記事](https://qiita.com/tomy0610/items/cb12347a44015a443105)」を参照して実施してください。

## クライアントサイドのベース作成

### ディレクトリを作成

以下の通りに、新規ディレクトリを src/app 配下に作成しておく

```bash
［ルートディレクトリ］
  ├─ src
  │  └─ app
  │      ├─ app-shell # 既存
  :      ├─ components # 新規作成： コンポーネント用
  :      :

```

### 適当なコンポーネントを新規作成

適当な画面レベルのコンポーネントを作っておく。
今回は Home というホーム画面用コンポーネントを用意。

```bash
# src/app/components ディレクトリに移動
cd src/app/components

# コンポーネント生成 (モジュールが２つあるのでスキップオプションを付けないとエラーになるかも)
ng generate c home --skip-import
```

終わったら、 `cd ../../..` で元のディレクトリ位置に戻っておく。

### ホーム画面コンポーネント修正

出来上がったコンポーネントを以下のとおりに修正。
画面にテスト用ボタンと押下処理を追加する。

#### まず TypeScript を修正

```ts:src/app/components/home/home.component.ts
import { Component, OnInit } from '@angular/core';

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

  id: number;
  name: string;
  age: number;

  // エラーメッセージ
  message: string;

  sampleList: { id: number, name: string, age: number }[] = [];

  constructor() { }

  ngOnInit() {
  }

  test() {
    // エラーメッセージ初期化
    this.message = '';

    // 入力判定 および 表示処理
    if (this.id && this.name && this.age) {
      this.sampleList.push({ id: this.id, name: this.name, age: this.age });
    } else {
      this.message = '未入力の項目があります。必ず全て入力してください';
    }
  }

  // 表示リストをリセット
  resetList() {
    this.sampleList = [];
  }

}

```

#### 次にHTMLを修正

```html:src/app/components/home/home.component.html
<div class="home">
  <h2>ホーム画面</h2>

  <!--他のコンポーネントに遷移するときに使用  -->
  <!-- <a [routerLink]="'/home2'">go to home2</a> -->

  <!-- エラーメッセージ -->
  <p *ngIf="message" class="error-message">{{ message }}</p>

  <!-- 入力エリア -->
  <div class="condition">
    <label>ユーザID</label>
    <input type="number" [(ngModel)]="id">

    <label>名前</label>
    <input type="text" [(ngModel)]="name">

    <label>年齢</label>
    <input type="number" [(ngModel)]="age">

    <button (click)="test()">表示追加</button>
    <button (click)="resetList()">リセット</button>
  </div>

  <!-- 一覧表示エリア -->
  <table>
    <thead>
      <tr>
        <th>ID</th><th>名前</th><th>年齢</th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let element of sampleList">
        <td>{{ element.id }}</td><td>{{ element.name }}</td><td>{{ element.age }}歳</td>
      </tr>
    </tbody>
  </table>
</div>

```

#### そして CSS に追記

```scss:src/app/components/home/home.component.scss
.home { padding: 1rem }
.error-message { color: red }
.condition { margin: 2rem }
.condition input { margin-right: 1rem }
.condition button { margin-left: 1rem }

table {
  width: 100%;
  margin: 3rem 0;
}
thead {
  color: white;
  background-color: #3f51b5;
}

```

### モジュールに追加

ホーム画面のコンポーネントを有効化するためモジュールに登録する。
Angular でフォームを扱うために必要なモジュールも同時にインポートする。

```ts:src/app/app.module.ts
:
import { FormsModule } from '@angular/forms'; // 追加

import { HomeComponent } from './components/home/home.component'; // 追加

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent, // 追加
  ],
  imports: [
    :
    FormsModule, // 追加
  ],
:
})
export class AppModule { }

```

### 画面ルーティング追加

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

import { HomeComponent } from './components/home/home.component';

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' }, // 追加
  { path: 'home', component: HomeComponent }, // 追加
];

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

### Universal 用のモジュールをインポートしておく

module-map-ngfactory-loader の型定義ファイルを念の為入れておく。

```bash
npm i -D @nguniversal/module-map-ngfactory-loader
```

モジュールに module-map-ngfactory-loader を追加

```ts:src/app/app.server.module.ts
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader'; // 追加
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { Routes, RouterModule } from '@angular/router';
import { AppShellComponent } from './app-shell/app-shell.component';

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

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    ModuleMapLoaderModule, // 追加
    RouterModule.forRoot(routes),
    MatProgressSpinnerModule
  ],
  bootstrap: [AppComponent],
  declarations: [AppShellComponent],
})
export class AppServerModule {}
```

### アプリのビルド/起動コマンドを修正

package.json のスクリプト部分を以下の通りにする

```json-doc:package.json
...

  "scripts": {
    "ng": "ng",
    "dev": "ng serve",
    "start": "npx node-static ./dist/demo-app --spa --port=3000",
    "build": "npm run build:client",
    "build:client": "ng run demo-app:app-shell:production", // ng run アプリ名:app-shell:production
    "build:clientsub": "ng build && ng run demo-app:server",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },

...
```

### 受付ポート変更

`ng serve` 実行時に受け付けるポートを念のため指定して合わせておく。

##### angular.json
ポート設定を追記する。データ構造的には、 `projects.プロジェクト名.architect.serve` にのオプションにポート設定を追加する

```json-doc:angular.json
{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "demo-app": {
      ：
      "architect": {
        ：
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "demo-app:build", // カンマだけ追加
            "port": 3000 // １行追加
          },
          "configurations": {
            "production": {
              "browserTarget": "demo-app:build:production"
            }
          }
        },
        ：
      }
    },
    ：
```

##### protractor.js
テスト時のURLのポート番号を変更する

```js:e2e/protractor.js
...
  baseUrl: 'http://localhost:3000', // 4200 から修正
...
```

### いったん確認

```bash
# ビルド
npm run build

# ビルド終わったら、アプリ起動
npm run start
```

起動したら Web ブラウザから `http://localhost:3000` にアクセス。表示確認できれば成功（停止は `Ctrl` + `C`）

アプリシェルが効いていれば、初期画面表示が遅い場合のみ、画面表示時に一瞬だけ AppShellComponent の内容が表示される。

### 実際に表示される画面例

ブラウザでは初期表示でこんな画面になるはず。
※ 画像では Angular Material でマテリアルデザインをヘッダ部分に導入している

<img width="1552" alt="スクリーンショット 2019-02-09 19.38.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/199602/e2713d3c-786b-73d9-cc99-6f8abe6745f3.png">

何も入力せずに「表示追加」ボタンを押したらこんな感じになるはず

<img width="1552" alt="スクリーンショット 2019-02-09 19.39.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/199602/7a8b6841-c645-08b2-18e2-364ccbe389c3.png">

試しに入力欄を埋めて「表示追加」ボタンを押してみると、一覧に追加されるはず。（連打すればたくさん入る）

<img width="1552" alt="スクリーンショット 2019-02-09 19.39.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/199602/eac40050-e495-c9a8-d011-11f32436ec4a.png">


あとは、「リセット」ボタンを押したら全部消えるはず。

## バックエンド導入準備

サーバサイドを作る前提で、クライアント側の関連ディレクトリ名をわかりやすい表記に変更しておく。
今回は「browser」というディレクトリに変更する。

### src ディレクトリ名修正
クライアントのディレクトリ名を `src` → `browser` に変更する

### angular.json 修正
angular.json ファイル内で `src` → `browser` に全置換する

### 動作確認

```bash
# ビルド
npm run build

# ビルド終わったら、アプリ起動
npm run start
```

問題なく動いていればOK。

👉 [次の開発手順はこちら](https://qiita.com/tomy0610/items/ae9092f933c399ec1bfa)