1. sinpaout

    Posted

    sinpaout
Changes in title
+フロントエンドAPIモック導入でビルド時間が爆速になった
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,276 @@
+フロントエンド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が走り、フロントのビルドは1分ちょい。
+
+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にビルド結果を展開が可能