フロントエンドAPIモック導入でビルド時間が爆速になった

フロントエンドAPIモック導入したことでビルド時間が爆速になった
=====

おはようございます、[モチベーションクラウド](https://www.motivation-cloud.com)の開発に参画している[@sinpaout](https://github.com/sinpaout)です。

> TL; DR
Docker + Rails + Mysql + webpackerで起動するのが時間かかりすぎるので
全てを捨ててnodeのみ（webpack-dev-server）で生きていくことに。。。

## 環境

Rails + Mysql + webpacker（vue.jsビルド）がDockerイメージとして管理され、
コマンド一発で開発環境を起動できる。

![Docker and Rails](https://user-images.githubusercontent.com/40750609/49211198-5cde8900-f402-11e8-8226-1b703b49c232.png)

## 問題
起動に`db:setup` や `db:migrate` などDBの初期化が走り大体3〜5分前後かかる。
更に画面が起動後にWebpackerが走り、フロントのビルドは１分ちょい。

DBの処理化なしでマイグレーションのみでもRailsが立上がるまで2,3分かかってしまう。
普段はJSのビルド時間も入れると5分はかかってしまう。

立上がったあとは毎回ログインして目的の画面に進むが途中で何かしらエラーに直面して進めなくなることはよくある。
また、 `Seeds` データが全てのパターンきちんと用意さていない事が多く
目的の画面まで到達するのにかなりの時間と労力がかかってしまう。
ときにはDockerが壊れて丸一日をクジラのお世話に費やされてしまうエンジニアもいた。

Seedsデータを用意できても更新系やデータのありなしなどの
パターンはDBを直接触る必要が出てき来たりするので手間がかかるので
APIモックシステムの構築を検討することになった。

## APIモックとは

> APIをJSONファイルとしてwebpack-dev-serverなど簡易Webサーバで提供する仕組み
Railsなどのバックエンドを起動しないため高速に開発環境を起動可能

**バックエンドの関係者たちを退場させる：**

![webpack-dev-server](https://user-images.githubusercontent.com/40750609/49211195-5b14c580-f402-11e8-8d4e-a812b1038fd5.png)

Nodeは今どきnvmなど入れとけばバージョン管理も楽なのでDockerも退場。
（みんな今まで頑張ってくれてありがとう。。。）

アプリのAPIパスとJSONファイルのマッピングはyamlファイルで定義し
axiosのintercepeterでアドレスを変換する

## 使ってみる

**マッピングの設定：**

```yml
# js/mocks/apiMapper.yml
default:
  desc: デフォルトのモック
  api:
    /users: mocks/users.json
    /users/1/: mocks/users/detail.json
```

`users.json` の中身

```json
{
  "users": [{
    "id": "",
    "name": "",
  }]
}
```
上記の例は
`/users` のAPIを`mocks/users.json` に
`/users/1/` のAPIを `mocks/users/detail.json`
に置き変える。

> ※ パスのidの部分は全て1として解釈するようにする。

## APIをJSONファイルと関連付けてくれる人

`API mocker` 役割
![apiMocker](https://user-images.githubusercontent.com/40750609/49212819-49cdb800-f406-11e8-92e8-9c50741354cf.png)

### API Mockerの詳細

```js
// js/mocks/apiMocker.js
import urlParse from 'url-parse'
import apiMapper from './apiMapper.yml'

const defaultApi = apiMappers.default.api

global.apiMockIntercepter = (config) => {
  const originalUrl = config.url
  const parsedUrl = urlParse(config.url)
  let apiPath = parsedUrl.pathname.replace(new RegExp(`^${config.baseURL}`), '')

  // idをすべて1に置き換える
  apiPath = apiPath.replace(/\/([0-9]+)\//ig, '/1/')
  const mockApiPath = defaultApi[apiPath]

  if (mockApiPath) {
    // 強制的にGETに
    config.method = 'get'
    config.url = config.baseURL + mockApiPath
    // 元情報を書き出す
    console.info('api mocked', originalUrl, mockApiPath)
  }

  return config
}
```

※ 環境に合わせてパスを調整する必要がある。

### Webpackの設定

普段は `index` のみバンドルするが、実行環境が`local`の時のみ `apiMocker`を挿入する。
`apiMocker` が `index` より前に挿入する必要がある。

**CopyWebpackPlugin:**
モックのJSONファイルをoutputパスにコピーさせる

```js
// webpack.local.js
if (process.env.DEV_ENV === 'local') {
  ...

  // Inject api mocker
  webpackConfig.entry.index = [
    `${dir.mocks}/apiMocker.js`,
    `${dir.js}/index.js`
  ]

  webpackConfig.plugins.push(new CopyWebpackPlugin([{
    from: `${dir.mocks}/api/`,
    to: `${output.path}/api/`
  }]))

  ...
}
```

### axiosの設定

実行環境が`local`の時かつ `apiMockIntercepter` が存在したら使うようにする

```js
axios.interceptors.request.use((config) => {
  if (process.env.DEV_ENV === 'local' && global.apiMockIntercepter) {
    return global.apiMockInterceptor(config)
  }
  return config
})
```

### モックマッピングの拡張

パターンごとに切り替えられるようにする。

```yml
# js/mocks/apiMapper.yml
default: &default
  desc:
  api: &api
    /users: /users.json
    /users/1/: /users/detail.json
    ...

noData:
  <<: *default
  api:
    <<: *api
    /users: /users_no_data.json
```

`noData` のパターンでは `/users` をデータなしに置き換える

`users_no_data.json`

```json
{
  "users": []
}
```

## 一捻り

### Devtoolのネットワークでデバッグ
元のAPIやPOST場合は中身がリクエストの中身がわからなくなるので
`console` で元の情報を表示するように `apiMocker` を更新

```js
// js/mocks/apiMocker.js
global.apiMockIntercepter = (config) => {
  const originalUrl = config.url

  ...

  if (mockApiPath) {

    ...

    // 元情報を書き出す
    console.info('api mocked', originalUrl, mockApiPath)
  }

  return config
}
```

### UIからの置き換え

開発時に直接モックを切替変えられると、より効率上がるので
画面の右上あたりにパターン一覧を置き換えるポップアップ的なものを実装。

![API pattern changing UI](https://user-images.githubusercontent.com/40750609/49349107-74608f00-f6ec-11e8-80c8-97231a0b71ac.gif)

ブラウザーのリロード後もモックの設定を有効にしたいのでsessionStorageに突っ込む。
cookieを使わない理由はhttpOnlyなどを考慮したため。

### E2E用

Cypressなどからパターンを変えられるように `apiMocker` を更新

```js
// js/mocks/apiMocker.js

const apiMocker = {
  currentPattern: sessionStorage.getItem('apiMockerPattern') || 'default'
}

// E2Eようの外部モジュールから参照できるようにグローバル変数にしておく
window.apiMocker = apiMocker

...

// モックパターンをセットする関数を用意
apiMocker.setCurrentMock = function (patternName) {
  apiMocker.currentPattern = patternName
  sessionStorage.setItem('apiMockerPattern', apiMocker.currentPattern)
}

global.apiMockIntercepter = (config) => {
  ...
  const apiMap = apiMappers[apiMocker.currentPattern].api
  const mockApiPath = apiMap[apiPath]
  ...
}
```

Cypressから使う

```js
cy.window().then((win) => {
  win.apiMocker.setCurrentMock('noData')
  cy.visit('localhost:8081/company/1/users')
})
```

## 結果

- 開発環境の立ち上げ5分 → 1分ちょい
- パターンごとにのデータの用意がjsonファイルのみで完結（ストレス激減）
  - Dockerの死亡やSeed不足の悩みから開放
- E2Eからのパターンが切替えられるようになるためUIテストが書きやすい
- DirやPOなどのエンジニア以外への画面共有が楽

## 今後の追加機能

- Webpackerのビルド廃止（Railsと完全に縁を切る）
  - Railsは嫌いではない（むしろ好き）がやりすぎると制御しづらくなるの要注意。
- POSTなどの更新系API対応
- 4xx、5xx系のエラー対応
- webpackのバージョンアップやチューニング
- プルリクエスト単位でのレビュー環境の用意
  - 静的なファイルで再現できるためS3にビルド結果を展開が可能