フロントエンド初心者がMEANスタック(MongoDB+Express+Angular+Node.js)でアプリを作ってみて躓いたこと

[FUJITSU Advent Calendar 2017](https://qiita.com/advent-calendar/2017/fujitsu) 12日目の記事です。

リッチでイマドキなデザインのアプリが作りたくて、
ここ４ヶ月ほど**MEAN**スタック(**M**ongoDB+**E**xpress+**A**ngular+**N**ode.js)でブログアプリを作っています。
知識ゼロからのスタートでしたが、多くの方々がブログやStackOverFlowに情報を載せてくれているので、躓きながらもなんとかアプリ開発を進めることができました。

この記事では、フロントエンド初心者の自分がMEANスタックでアプリを作る時に躓いたことや、こういう機能を実現するにはどうすればいいか?などをまとめています。
これからをAngularを学ぼうとしている方、ExpressやMongoDBなどサーバーサイドJavaScriptを学ぼうとしている方の参考になればうれしいです。

## アプリの紹介
本線から脱線しますが、イントラでの使用を想定したQiitaのようなブログアプリです。デモ環境もあるのでよければ触ってみてください(モバイルには未対応ですが。。。)

* [Github](https://github.com/Takumon/mean-blog)
* [Dockerhub](https://hub.docker.com/r/takumon/mean-blog_auto/)
* [デモ環境](https://material-blog-demo.herokuapp.com/) (ユーザID/パスは DemoUser / DemoUser1234# です)

アプリキャプチャ その１ (記事詳細)
![appdemo_detail.png](https://qiita-image-store.s3.amazonaws.com/0/49915/b8133317-fac2-bb9b-8f2d-13db896becf5.png)

アプリキャプチャ その２ (記事一覧)
![アプリ_スクリーンキャプチャ_記事一覧.png](https://qiita-image-store.s3.amazonaws.com/0/49915/950d1062-1ba4-63f6-fe53-cfe6d46430a4.png)

アプリキャプチャ その３ (プロフィール)
![アプリ_スクリーンキャプチャ_ ユーザ画面.png](https://qiita-image-store.s3.amazonaws.com/0/49915/325c7948-ce16-9c08-0b66-d3892e046aed.png)

# 1. フロント側Angularまわり
## Angularについて調べる時に古い情報を除外したい
1系は`AngularJS`、2系以降は`Angular`と呼ばれており、1系と2系以降では大きく仕様が異なります。
そのため検索する時は`Angular2`などバージョンを指定したり、1系を除外するため`--AngularJS`をつけたりすると検索しやすいです。

## HTMLのDOM要素を、別のDOM要素またはComponentから扱いたい
要素に`#xxxxx`のように`#`始まりの名前をつけると、別のDOM要素から参照できます

```html:HTML
<input #phone placeholder="電話番号"/>
<!--  他のDOM要素からphonという変数名でDOM要素を参照できるようになる -->
<button >(click)="callPhone(phone.value)">
```

Componentから参照する場合は`@ViewChild`を使います

```typescript:Component
  // ViewChildの引数に名前を文字列で指定します
  @ViewChild('phone') phoneElement: phoneElement;

  showPhoneValue() {
    console.log(this.phoneElement.value);
    )
  }
```

**参考サイト**

* [Angular公式サイト](https://angular.io/guide/template-syntax#ref-vars)
* [StackOverFlow](https://stackoverflow.com/questions/32693061/how-can-i-select-an-element-in-a-component-template)



## Routing時の認証を非同期で行いたい
URLごとの認証は`CanActivate`インターフェースを実装すればできますが、
サーバから認証情報を取得して非同期で認証したい場合もあると思います。
そのような時は、**`CanActivate#canActivate`で**booleanの代わりに**Observable<boolean>を戻り値に指定する**ことで実現できます。


```typescript:SampleAuthGuard
import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Rx';

import { AuthenticationService } from './authentication.service';

@Injectable()
export class SampleAuthGuard implements CanActivate {

  constructor(
    private auth: AuthenticationService,
  ) { }

  // booleanではなくObservable<boolean>を戻り値で返す
  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
    return this.auth.checkState()
      .map(res => true)
      .catch(err => Observable.of(false))
  }
}

```

**参考サイト**

* [StackOverFlow](https://stackoverflow.com/questions/38425461/angular2-canactivate-calling-async-function)

## FormArrayの値を初期化したい
FormControlはpatchValueで初期値を設定できますが、
FormArrayの場合patchValueで配列の値を設定しようとしても設定できません。
こういう場合は、**配列1つ１つの値をもとにFormControlを生成してFormArrayにpush**します。

```typescript:ダメな例
ngOnInit() {
  // Form生成
  this.form = this.formBuilder.group({
    schoolName: '',
    students: this.formBuilder.array([])
  });

  // Formに初期値を設定
  this.form.pathValue({
    schooleName: 'SampleSchoolName' // FormControlの値の初期化はpatchValueで可能
    students: ['taro', 'jiro', 'saburo']; // FormArrayに要素を追加する場合patchValueでは不可能
  });
}
```

```typescript:良い例
ngOnInit() {
  // Formを生成
  this.form = this.formBuilder.group({
    schoolName: '',
    students: this.formBuilder.array([])
  });

  // Formに初期値を設定
  this.form.pathValue({
    schooleName: 'SampleSchoolName'
  });

  // データからFormControlを生成し１件１件FormArrayにpushする
  ['taro', 'jiro', 'saburo'].forEach(student -> {
    this.form.controls['students'].push(new FormControll(student));
  });
}
```

**参考サイト**

* [StackOvereFlow](https://stackoverflow.com/questions/40916084/angular2-patchvalue-push-value-into-array)

## 「ERROR Error: No provider for TemplateRef!」というエラー
最初このエラーが出た時は、何が原因なのかわからずに困りました。。。
大抵の場合は、**`*ngIf`や`*ngForm`の`*`が抜けてることが原因**です(要するにただのタイポです。。。)
`*`が抜けると、Angularは`ngIf`をディレクティブとして解釈しようとしますが、
そんなディレクティブは存在しないので`No provider for TemplateRef!`と言われてしまうそうです。

**参考サイト**

* [StackOverFlow](https://stackoverflow.com/questions/35932074/angular2-no-provider-for-templateref-ngif-templateref)


## textareaにおいてTabキーでインデントしたい
文書を入力するようなテキストエリアの場合に、Tabキーでのインデントしたい場合は、
kyedownイベント発生時にテキストエリアの値とキャレットの位置を操作することで実現可能です。

```html:HTML
<textarea #sampletextarea
  (keydown)="indent($event, sampletextarea)" ></textarea>
```

```typescript:Component
  indent($event, sampleTextAreaElement) {
    // Tabキー押下時
    if ($event.keyCode === 9) {
      // 次の要素にフォーカスが移らないようにする
      $event.preventDefault();

      // 現在のキャレット位置を取得
      const caretStart = textareaElement.selectionStart;
      const caretEnd = textareaElement.selectionEnd;

      // テキストエリアの値を取得し、キャレット位置にTabを挿入
      const TAB = '¥t';
      sampleTextAreaElement.value = sampleTextAreaElement.value.substring(0, caretStart)
                     + TAB + sampleTextAreaElement.value.substring(caretStart, value.length);

      // キャレット位置をTab分ずらす
      sampleTextAreaElement.focus();
      sampleTextAreaElement.setSelectionRange(caretStart + TAB.length, caretEnd + TAB.length);

      return;
    }
  }

```



## Markdownプレビューを表示したい、ソースコードはシンタックスハイライトさせたい
[marked](https://github.com/chjj/marked)と[highlight.js](https://github.com/isagalaev/highlight.js)を組み合わせて使います。
[marked](https://github.com/chjj/marked)のReadmeを見ればなんとなくわかりますが、Angularの仕組みに乗せる必要があるのでPipeやらServiceやらを作ります。

```typescript:markdown-parse.service.ts
import { Injectable } from '@angular/core';
import marked from 'marked';
import hljs from 'highlight.js';


@Injectable()
export class MarkdownParseService {

  constructor() {
    marked.setOptions({
      highlight: function (code) {
        return hljs.highlightAuto(code).value;
      }
    });
  }

  parse(rawText: string) {
    return marked(rawText);
  }
}
```


```typescript:markdown.pipe.ts
import marked from 'marked';
import { Pipe, PipeTransform } from '@angular/core';
import { MarkdownParseService } from './markdown-parse.service';

@Pipe({ name: 'toMarkdown' })
export class MarkdownParsePipe implements PipeTransform {
  constructor(markdownParseService: MarkdownParseService) {}

  transform(value: string): any {
    return this.markdownParseService.parse(value);
  }
}
```

HTMLで下記のように指定します。`{{}}`だとサニタイズされてしまうので`innerHTML`属性を指定します。

```html
<div [innerHTML]="md | toMarkdown"></div>
```


**参考サイト**

* [Qiita AngularでMarkdownをHTMLとして表示するカスタムpipeを作成する](https://qiita.com/daikiojm/items/e02e2aeb0231b3620a0b)
* Github [chjj/marked](https://github.com/chjj/marked)




## 絞り込み条件付きリストにおいて、リストの要素が変更、追加、削除された時に絞り込み結果をリフレッシュしたい
リストの絞り込みはPipeで実現しますが、通常Pipeはリストの要素が変更されても再び絞り込みが実施されることはありません。
このような場合はPipeアノテーションにて**pureオプションをfalseに設定**ましょう。

```html:HTML
<input type="text" #searchUserName>
<ul>
  <li *ngFor="let user of (userList | searchUserFilter: searchUserName.value);" >{{user.name}}</li>
<ul>
```

```typescript:search-user.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { User } from './user';

@Pipe({
  name: 'searchUserFilter',
  pure: false // pureをfalseにすることでユーザが変更、追加、削除された時にリフレッシュできる
})
export class SearchFilterPipe implements PipeTransform {
  transform(items: Array<User>, searchUserName: string): any[] {
    if (!searchUserName) return items;

    searchUserName = searchUserName.toLowerCase();
    return items.filter( item => item.user.userId.toLowerCase().includes(searchUserName));
  }
}
```

**参考サイト**

* [Angular公式サイト](https://angular.io/guide/pipes#pure-and-impure-pipes)




## グローバル定数を定義したい
いろんなクラスで使う定数を共通化する時は、単純に**クラスを作ってstaticなメンバとして定数を定義**します。

```typescript:app-settings.ts
export class AppSettings {
   public static API_ENDPOINT='http://127.0.0.1:6666/api/';
}
```

```typescript:SampleService
import {Injectable} from 'angular2/core';
import {AppSettings} from './app-settings';

@Injectable()
export class SampleService {
    sampleMethod() {
      console.log(AppSettings.API_ENDPOINT);
    }
}
```

**参考**

* [StackOverFlow](https://stackoverflow.com/questions/34986922/define-global-constants-in-angular-2)


## 画像が多い画面の初期表示を早くしたい
[ng-lazyload-image](https://github.com/tjoskar/ng-lazyload-image)を使えば画像の遅延ロードを実現できます。
使い方もとても簡単でimgタグにディレクティブを指定するだけです。

```html:HTML
 <img
  [defaultImage]="https://images.sample.com/photo/defaultimage" 
  [lazyLoad]="https://images.sample.com/photo/sampleimage"
  [offset]="30"
 >
```

*defaultImage*
　即時ロードされる画像のURL、遅延ロードする画像を読み込む間、表示される
*lazyLoad*
　遅延ロードする画像のURL
*offset*
　スクロールが発生する場合に画面下部の何ピクセル下に来た時にロードを開始するか
*errorImage*
　遅延ロード失敗時に表示する画像URL

**参考サイト**

* [ng-lazyload-image](https://github.com/tjoskar/ng-lazyload-image)


## Angular Cliの`ng serve`コマンドでdistフォルダを一旦削除したくない
`ng serve`コマンドは`dist`フォルダを削除してからtsファイルをトランスコンパイルします。
それを防ぐためには、**delete-output-pathオプションをfalse**に指定します。

```json:package.json　ビルドスクリプト
"script": {
  "build": "cp ./resource/* dist && ng serve --delete-output-path=false"
}
```

**参考サイト**

* [angular-cli github issue #4366](https://github.com/angular/angular-cli/issues/4366)


## AOTコンパイルが遅いのでなんとかしたい
なんとかできませんでした。。。(もしかしたら方法があるのかもしれません。誰か教えてください!!!)
AOTコンパイルはJITコンパイルが検出してくれないHTMLのエラーを警告してくれますが、その反面遅いです。特に[Angular Material](https://material.angular.io/)を使う場合は顕著です。
そのため、自分の場合は基本的にJITビルドを使い、issueのプルリクをする前など区切りのいいタイミングでAOTビルドしてエラーがないか確認するというように使い分けてました。


# 2. フロント側Angularでのテスト周り
## CI環境などでテストが終わらずにタイムアウトしてしまう
CirleCiなどでテストを実行する場合`ng test`コマンドだとプロセスが終了しないためタイムアウトで失敗してしまいます。
このような場合は**watchオプションをfalseに設定**します。

```
ng test --wtach=false
```

**参考サイト**
[Github isssue](https://github.com/angular/angular-cli/issues/362)

## テスト時に「Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.」や「Failed to execute 'send' on 'XMLHttpRequest'」のエラー
このような場合は、一時的に`ng test`コマンドのオプションに**`-sm=false`を追加**してテストし直すと根本原因エラーメッセージで出力されるようになります。
大抵の場合は自作したモッククラスに必要なメソッドがないことが原因です。

**参考サイト**

* [stackOverFlow](https://stackoverflow.com/questions/45399079/angular-tests-failing-with-failed-to-execute-send-on-xmlhttprequest)


## テスト用に子コンポーネントをモック化したい
意外と簡単で、TestBed#configureTestingModuleで
declarationsに自作したモックの子コンポーネントを追加するだけです。
input,outputがあれば必要に応じてメンバ定義します。


```typescript
// ※import文は省略

// モックの子コンポーネントを定義
@Component({
  selector: 'app-child', // 子コンポーネントと同じものを定義
  template: '<p>Mock Child Component</p>'
})
class MockClildComponent {
  @Input() childInput: string;
  @Output() childOutput = new EventEmitter();
}

// ...

beforeEach(() => {
  TestBed.configureTestingModule({
    declarations: [
      // テスト時のdeclarationsにモックの定義を追加
      MockProductSettingsComponent,
      // ...
    ],
    // ...
  });
  // ...
});
```

**参考サイト**

* [stackOverFlow](https://stackoverflow.com/questions/41240163/mocking-child-components-angular-2)


## テスト結果にAngular Materilaのスタイルが反映されない
UIフレームワークで[Angular Material](https://material.angular.io/)を使っている場合、
`karma.config.js`でAngular Materilaのcssを直接読み込んであげる必要あります。

```javascript:karma.conf.js
files: [
      ・・・
      // Angular Materialのスタイルをテスト開始時に読み込んでおく
      {pattern: './node_modules/@angular/material/prebuilt-themes/indigo-pink.css', included: true, watched: false},
],
```

**参考サイト**

* [stackoverflow](https://github.com/angular/material2/issues/4056)


## テスト結果にstyles.scss(アプリ共通のスタイル定義)のスタイルが反映されない
アプリ共通スタイルをSASS形式にしている場合
開発用ライブライに`karma-scss-preprocessor`と`node-sass`を追加して`karma.conf.js`を下記のように設定します。

```javascript:karma.conf.js
plugins: [
  ・・・
　 // プラグインに`karma-scss-preprocessor`を追加
  require('karma-scss-preprocessor')
],


files: [
  ・・・
  // filesにアプリ共通スタイルを追加
  { pattern: './src/styles.scss', watched: false,  included: true, served: true }
],

// preprocessorsを追加
preprocessors: {
 './src/test.ts': ['@angular/cli'],
 './src/styles.scss': ['scss']
},
```

**参考サイト**:

* [stackOverFlow](https://stackoverflow.com/questions/44460733/how-to-configure-karma-to-include-global-scss-files-for-an-angular-cli-project/44498689#44498689)
* [Github karma/issue/2700](https://github.com/karma-runner/karma/issues/2700)




# 3. バックエンド側 Express、MongoDB周り
※MongDBをNode.jsで扱う場合は[mongoose](https://github.com/Automattic/mongoose)という便利なライブラリがあるのでそれを使う前提のお話です。

## mongooseのvirtualメソッドを使う
例えば`記事`,`コメント`,`リプライ`などのモデルを定義する場合、
３つのモデルのライフサイクルは、記事追加 => 記事に対するコメント追加 => コメントに対するリプライ追加　のようになります。
このような場合は、コメントが記事の参照を持ち、リプライがコメントの参照を持つモデル構造が望ましいです。コメントやリプライ追加時に１つのモデルの更新だけですむからです。
![db構造_良.png](https://qiita-image-store.s3.amazonaws.com/0/49915/f93ac0e6-317e-1cdf-bfba-13531e2455ba.png)

ただ記事の検索は少し工夫が必要で、**mongooseのvirtualを使います**。これによって記事からコメントへの参照、コメントからリプライへの参照を擬似的に定義できるため、
検索時に擬似要素をpopulateするだけで、記事に紐付くコメントとリプライを取得できるようになります。
![mongoose virtualのイメージ.png](https://qiita-image-store.s3.amazonaws.com/0/49915/4aab7f38-1f19-e00b-b35d-23babf36a75b.png)

具体的なソースコードを示します。

```typescript:article.model.ts
import * as mongoose from 'mongoose';

const ArticleSchema = new mongoose.Schema({
  content: String
}, { toJSON: { virtuals: true } });


// 記事に紐付くコメントモデルの配列をcommentsという擬似要素で定義する
ArticleSchema.virtual('comments', {
  ref: 'Comment',
  localField: '_id',
  foreignField: 'articleId',
  justOne: false,
});

const Article = mongoose.model('Article', ArticleSchema);

export { Article };
```


```typescript:comment.model.ts
import * as mongoose from 'mongoose';

const CommentSchema = new mongoose.Schema({
  articleId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Article',
  },
  comment: String;
}, { toJSON: { virtuals: true } });

// コメントに紐付くリプライモデルの配列をrepliesという擬似要素で定義する
CommentSchema.virtual('replies', {
  ref: 'Reply',
  localField: '_id',
  foreignField: 'commentId',
  justOne: false,
});

const Comment = mongoose.model('Comment', CommentSchema);

export { Comment };
```

```typescript:reply.model.ts
import * as mongoose from 'mongoose';

const ReplySchema = new mongoose.Schema({
  commentId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Comment'
  },
  reply: String;
});

const Reply = mongoose.model('Reply', ReplySchema);

export { Reply };
```

```typescript:検索処理
Article.find()
.populate({
  path: 'comments', // Articleモデルで定義した擬似要素commentsをpopulateする
  populate: [{
    path: 'replies', // Commentモデルで定義した擬似要素repliesをpopulateする
  }],
});
```


## リクエスト、レスポンスのログ出力したい
Expressのuseにて実現します。
レスポンスオブジェクトのfinishイベントを監視することで、レスポンス時にログを出力しています。


```javascript:リクエストとレスポンスのログ出力　※ここでは簡単のためコンソールに出力しています
const express = express();
express.use(function accessLogHandler (req, res, next) {
  const start = new Date();
  // リクエスト時のログ　
  console.log([
    'start',
    req.headers['x-forwarded-for'] || req.connection.remoteAddress,
    req.method,
    req.url,
    '-',
    req.headers.referer || '-',
    req.headers['user-agent'] || '-',
    '--ms--'
  ].join(',\t'));

  res.once('finish', function() {
    // レスポンス時のログ
    accessLogger.info([
      'end',
      req.headers['x-forwarded-for'] || req.connection.remoteAddress,
      req.method,
      req.url,
      res.statusCode,
      req.headers.referer || '-',
      req.headers['user-agent'] || '-',
      '--' + (new Date().getMilliseconds() - start.getMilliseconds()) + 'ms--'
    ].join(',\t'));
  });

  next();
});
```

実際のログはこんな感じで出力されます。

```py3
start,  ::ffff:127.0.0.1, GET, /api/authenticate/check-state,   -, http://localhost:4200/, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36, --ms--
end,    ::ffff:127.0.0.1, GET, /check-state,                  403, http://localhost:4200/, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36,  --47ms--
```



## ログ出力でオブジェクトの全プロパティを出力したい
オブジェクトの中身をログで確認する時はNode.jsの`util.inspect()`を使います。


```javascript
const util = require('util');
const myObject = {
   "a":"a",
   "b":{
      "c":"c",
      "d":{
         "e":"e",
         "f":{
            "g":"g",
            "h":{
               "i":"i"
            }
         }
      }
   }
}; 

console.log(util.inspect(myObject, false, null));
```

このようなログが出力されます

```
{ a: 'a', b: { c: 'c', d: { e: 'e', f: [Object] } } }
```

**参考サイト**

* [StackOverFlow](https://stackoverflow.com/questions/10729276/how-can-i-get-the-full-object-in-node-jss-console-log-rather-than-object)


## 環境変数で上書きできる定数を定義したい
例えばサーバのポートなど環境個別に設定したくなるようなものは
環境変数で上書きできる定数にしておくと便利です。
Node.jsではprocess.env.変数名で環境変数が参照できるので下記のようにします。

```typescript:定数定義
export const SERVER_PORT: string = process.env.SERVER_PORT || '3000';　// 環境変数SERVER_PORTが未指定の場合は3000となる
export const SERVER_HOST: string = process.env.SERVER_HOST || 'localhost'; // 環境変数SERVER_HOSTが未指定の場合はlocalhostとなる
```


## DB初回アクセスに失敗した場合にリトライされない
mongooseを使っていると、DB初回接続時にエラーが起きた時に、なぜか再接続処理してくれません。
Dockerでアプリを配布する場合、アプリとMongoDBをdocker-composeで同時に立ち上げることがあると思いますが、まだMongoDBが起動しきってない状態でアプリからDBに接続しようとすると、アプリが異常終了してしまいます。これを防ぐには自力で再接続処理を実装する必要があります。

```javascript
function createConnection (dbURL, options) {
    var db = mongoose.createConnection(dbURL, options);

    db.on('error', function (err) {
        if (err.message && err.message.match(/failed to connect to server .* on first connect/)) {
            console.log(new Date(), String(err));

            setTimeout(function () {
                console.log("Retrying first connect...");
                db.openUri(dbURL).catch(() => {});
            // 20秒後に再接続する
            }, 20 * 1000);
        } else {
            console.error(new Date(), String(err));
        }
    });

    db.once('open', function () {
        console.log("Connection to db established.");
    });

    return db;
}
```

**参考サイト**

* [Github mongooseのissue5169](https://github.com/Automattic/mongoose/issues/5169)