BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

Docker 環境から webpack-dev-server に繋いで HMR する

BASE Advent Calendar 2021 9日目の記事です。

f:id:stbc:20211208112042p:plain

フリーランスのフロントエンドエンジニア 坪内です。
BASE のお手伝いをさせていただくようになって 1ヶ月が経ち、色々見えるようになってきた中で最も気になっていた点の 1つが、

HMR されていない

でした。

BASE の Web フロントエンドは webpack でビルドされているのですが、 ローカルの開発環境が Docker 上で動いている事もあってか、残念ながら HMR はされていない状況でした。

「Docker 環境だから無理?」
「“webpack HMR docker” とかでググると色々出てくるし出来そうな?」

さっそく取り掛かってみました。

HMR とは?

一応、HMR の説明をしておきます。

https://webpack.js.org/guides/hot-module-replacement/
Hot Module Replacement の略です。
js モジュールの変更を、ブラウザをリロードすること無しに動的に差し替えて反映する webpack の機能の事を指します。 歴史は割と古く、webpack 1系の頃から試験的に導入されていました。

似たものとして、ファイルの変更を検知してブラウザを自動でリロードしてくれる liveReload があります。

仕組み

HMR はおおよそ以下の流れで実現されています。

HMR の仕組み

  1. webpack-dev-server が HMR 用のコードを含めて js を返す
  2. WebSocket に接続
  3. webpack がファイルの変更を検知したら差分ビルド
  4. 変更があったことを WebScoket で通知
  5. 変更差分情報を取得
  6. 変更のあったモジュールを取得して動的に置き換え
    • 置き換えができない場合は liveReload される

構成

一般的な SPA の場合、起動した webpack-dev-server にブラウザからアクセスし、API などへのアクセスは必要に応じてリバースプロキシするという構成を取ります。

一般的な SPA の場合の構成図

  1. 起動した webpack-dev-server (http://localhost:3000 など)にブラウザでアクセス
  2. webpack でビルドされた html を返却
  3. webpack でビルドされた js を返却
  4. webpack で管理していないリソースは backend へリバースプロキシ

当初

BASE のサービスは CakePHP を使用しているので、上記と同様に前段に webpack-dev-server を配置して CakePHP へリバースプロキシする必要があると思っていました。

また、ローカル Docker 環境は架空のドメイン(ここでは仮に *.base.test)上で動くようになっているので、webpack-dev-server へもその架空ドメインとしてアクセス出来るようにしてあげる必要があると考え、Docker 環境内に webpack-dev-server を建てました。

当初の構成図

これはこれで動くので間違いではないのですが、

  • Docker上で動かすので単純に重くなる
  • Docker上で webpack-dev-server を使うかどうかを切り替えるのが面倒そう
  • node-gyp によるネイティブパッケージに依存していると、Docker上で yarn install する必要がある
  • 内部のライブラリへの yarn link が厳しそう

などの問題があり、色々と調整や解決が大変そうでした。

助言

そんな中、ある助言をもらいました。

アセットだけホスト側の dev-server に繋げないんだろうか

❗❗❗
リバースプロキシしなければならないと思い込んでしまっていましたが、 CDN から js や css を取得して動かせるわけなので、webpack-dev-server を CDN のように扱うこともできるのではないか?

助言後

助言後の構成図

  • webpack-dev-server はホスト側(http://localhost:3081/)で起動する
  • CakePHP で js をロードしている箇所の URL を http://localhost:3081/ に向ける

以上。
js の向き先を変えるだけで良くなったので、docker-compose には影響を及ぼさず、とてもシンプルな仕組みになりました!

webpack の設定

最終的な webpack の devServer 設定は以下です。

devServer: {
  allowedHosts: ['.base.test'],
  client: {
    overlay: true,
    webSocketURL: 'ws://localhost:3081/ws',
  },
  headers: {
    'Access-Control-Allow-Origin': '*',
  },
  port: 3081,
  proxy: {
    '*': 'http://localhost:8081/',
  },
  static: false,
  watchFiles: ['app/**/*'],
},

考慮した点

CORS を解決する

Docker 環境 https://*.base.test から見ると、 http://localhost:3081 はクロスオリジンなアクセスになるので、devServer.headers として CORS ヘッダーを返す必要があります。
また、webpack-dev-server へのアクセスを許可するドメインを devServer.allowedHosts に追加する必要がありました。

devServer: {
  allowedHosts: ['.base.test'],
  headers: {
    'Access-Control-Allow-Origin': '*',
  },
},

HMR の WebSocket 接続先や差分取得先などを webpack-dev-server に向ける

html は https://*.base.test/ から返されており、そのままだと HMR 用の WebSocket などもそちらを向いてしまうので、devServer.client.webSocketURLws://localhost:3081/ws へ向けます。

devServer: {
  client: {
    webSocketURL: 'ws://localhost:3081/ws',
  },
}

また、webpack-manifest-plugin を使用している関係で、output.publicPath を指定していたのですが、

output: {
  publicPath: '/',
},

これが差分の取得先に使われているので、webpack-dev-server で起動している場合は localhost:3081 へ向くようにする必要がありました。

output: {
  publicPath: env.WEBPACK_SERVE ? 'http://localhost:3081/' : '/',
},

webpack 管理外の js への対処

webpack でビルドされていない古い js が残っており、そのままだとそれらへのアクセスが 404 になってしまうので、それ用に devServer.proxy で CakePHP へリバースプロキシしています。

devServer: {
  proxy: {
    '*': 'http://localhost:8081/',
  },
},

また、それらを含め CakePHP 側に変更があった際に liveReload されるよう、devServer.watchFiles を設定しています。

devServer: {
  watchFiles: ['app/**/*'],
},

CakePHP で js の向き先を変える

HtmlHelper を使用して、

<?= $this->Html->script('foo') ?>

のように出力していれば、 App.jsBaseUrl を設定する事で向き先を変えることができます。

<?php
Configure::write('App.jsBaseUrl', 'http://localhost:3081/js/');

これをローカルでのみ有効になる Config ファイルに記述して、必要に応じて切り替えるようにしています。

おわりに

Docker 環境でもシンプルな形で webpack-dev-server で HMR する事ができました!
この形で HMR できるとなると、ProxymanCharles などの proxy と組み合わせてみたりなど色々応用もできそうですね。

明日は @budougumi0617 さんです!