JavaScript
docker
フロントエンド
webpack
モック

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

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

おはようございます、モチベーションクラウドの開発に参画している@sinpaoutです。

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

環境

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

Docker and Rails

問題

起動にdb:setupdb: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

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

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

使ってみる

マッピングの設定:

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

users.json の中身

{
  "users": [{
    "id": "",
    "name": "",
  }]
}

上記の例は
/users のAPIをmocks/users.json
/users/1/ のAPIを mocks/users/detail.json
に置き変える。

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

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

API mocker 役割
apiMocker

API Mockerの詳細

// 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を挿入する。
apiMockerindex より前に挿入する必要がある。

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

// 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 が存在したら使うようにする

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

モックマッピングの拡張

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

# 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

{
  "users": []
}

一捻り

Devtoolのネットワークでデバッグ

元のAPIやPOST場合は中身がリクエストの中身がわからなくなるので
console で元の情報を表示するように apiMocker を更新

// 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

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

E2E用

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

// 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から使う

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にビルド結果を展開が可能