Rails+React環境下における、Hot Module Replacementの導入

webpack and rails

はじめに

はじめまして。株式会社スタメンでエンジニアをしております、永井(@0906koki)です。

以前の記事では、筋トレを週5でしていると書いていましたが、今は週2に減らして体をメンテナンスしています。

今回の記事ではRailsとWebpack、そしてReactを使って、webpack_dev_serverによるHot Module Replacement(以下 HMR)を実装する方法について書きたいと思います。

軽くwebpack_dev_serverとHMRの説明をすると、webpack_dev_serverとはWebpackを利用した開発環境向けWebサーバーで、Webpack管理内の静的アセットを配信することができます。また、HMRとはWebpackの提供する仕組みで、ブラウザのリロードをせずにJavascriptの変更内容を画面に反映するツールです。

弊社のプロダクトであるTUNAGではサーバーサイドをRails、フロントエンドをReactとTypeScriptで実装しており、フロントエンドのビルドファイルを、Railsのsprocketsでコンパイルしてerbで読み込ませていました。 プロダクトの成長に比例してWebpackのbundleサイズも肥大化していき、それに起因してsprocketsのアセットコンパイルに掛かる時間も増加し、フロントエンド開発環境化においてスピード感を持って開発することが難しくなってきました。

このままでは、プロジェクトの進行に大きな悪影響を及ぼすことが目に見えてきたので、問題を解消するために、

  • sprocketsによる無駄なコンパイルをなくし、webpack_dev_serverによるコンパイルのみにする
  • HMRを導入し、リロードせずとも変更内容が反映されるようにする

この2つを軸として、フロントエンド開発環境改善プロジェクトをスタートしました。

※ この改善プロジェクトの内、webpack_dev_serverとRailsの連携部分に関しては、スタディストさんの「フロントエンド原理主義者が目論んだ脱webpacker」が非常に参考になりました。

実装の手順

webpack_dev_serverを導入して、HMRを適用する手順は以下の通りです。

  1. webpack_dev_serverのインストール
  2. webpack_dev_serverとmanifestPluginの設定
  3. webpack_dev_serverのビルドファイルを読み込むヘルパーメソッドの実装
  4. Railsのプロキシ設定
  5. react-hot-loaderの導入と実装

※ TUNAGではRailsのWebpackerを使わず純粋なWebpackを元々使用していたため、Webpackで実装する前提で話を進めます。

webpack_dev_serverのインストール

webpack_dev_serverに必要なpackageを追加します。

$ yarn add -D webpack_dev_server webpack-manifest-plugin

※ webpack-manifest-pluginは、生成したビルドファイルパスの管理ファイルとして使用します。

webpack_dev_serverとmanifestPluginの設定

設定は以下のようにしています。(loader等の設定は省略しているので、適宜追加してください)

const path = require('path');
const WebpackManifestPlugin = require('webpack-manifest-plugin')

const outputPath = path.resolve('../../public/packs')

module.exports = (env, argv) => {
  return ({
    entry: {
      bundle: [
        'webpack-dev-server/client?http://localhost:8080',
        './src/index.tsx'
      ]
    },

    output: {
      path: outputPath,
      publicPath: 'http://localhost:8080/packs',
      filename: '[name].js',
    },

    plugins: [
      new WebpackManifestPlugin({
        fileName: 'manifest.json',
        publicPath: '/packs/'
      })
    ],

    devServer: {
      contentBase: 'http://localhost:8080/packs',
      port: 8080,
      hot: true,
      headers: {
        'Access-Control-Allow-Origin': '*',
      }
    },
  });
};

ここでは、ビルドファイルの出力先をpublicディレクトリ配下のpacksディレクトリに指定しています。そしてwebpack_dev_serverのcontentBaseに/packsを指定することで、http://localhost:8080/packsで出力先されたビルドファイルを取得することができます。

また、WebpackのプラグインであるManifestPluginを使用して、ファイル名と実際に配置されるファイルパスが記述されたマニフェストファイルを生成します。

devServerとmanifestPluginの各プロパティの説明は以下の通りです。

devServer

  • contentBase: 静的ファイルを配置するパスの指定
  • port: ポート番号の指定(rails serverが3000を使用するので、8080に)
  • hot: HMRの利用
  • headers: webpack_dev_serverからのレスポンスに任意のヘッダー情報を含める

manifestPlugin

  • filename: 生成されるマニフェストファイル名の指定
  • publicPath: valueにprefixを付与する

これでwebpack_dev_serverからアセットを配信する設定が完了したので、早速webpack_dev_serverを立ち上げてみたいと思います。

立ち上げ方は、以下のコマンドを実行するだけです。(package.jsonのscriptsに設定しておくことをオススメします)

$ webpack-dev-server --progress --color

これで、http://localhost:8080/packs/◯◯.jsにアクセスすると、出力先されたビルドファイルを取得することができます。

ちなみに、http://localhost:8080/packs/manifest.jsonで、以下のようなマニフェストファイルも取得できると思います。

{
  "bundle.js": "/packs/bundle.js",
}

webpack_dev_serverのビルドファイルを読み込むヘルパーメソッドの実装

次に行いたいことは、上記で配信されたアセットをRails側で読み込むヘルパーメソッドの実装です。

コードは以下のようになります。(jsファイルのみを読み込む設定になっていますが、cssファイルも読み込みたい場合は、専用のメソッドを追加してください)

module WebpackBundleHelper
  class BundleNotFound < StandardError; end

  def javascript_bundle_tag(entry, **options)
    return javascript_include_tag entry unless Rails.env.development?

    path = asset_bundle_path("#{entry}.js")

    options = {
      src: path,
      defer: true
    }.merge(options)
    
    options.delete(:defer) if options[:async]

    javascript_include_tag '', **options
  end

  private

  def asset_host
    Rails.application.config.asset_host || ''
  end

  def dev_server_host
    "http://localhost:8080"
  end

  def dev_manifest
    # webpack-dev-serverから直接取得する
    OpenURI.open_uri("#{dev_server_host}/manifest.json").read
  end

  def manifest
    @manifest ||= JSON.parse(dev_manifest)
  end

  def valid_entry?(entry)
    return true if manifest.key?(entry)
    raise BundleNotFound, "Could not find bundle with name #{entry}"
  end

  def asset_bundle_path(entry, **options)
    valid_entry?(entry)
    asset_path(asset_host + manifest.fetch(entry), **options)
  end
end

javascript_bundle_tagでは、webpack_dev_serverから配信されているmanifest.jsonを取得し、manifest.jsonの中で引数に合致するファイルパスを取得します。 例えば、javascript_bundle_tagの引数にbundleを指定すると、manifest.jsonでbundleに合致するkeyを見つけて、そのvalue(/packs/bundle.js)を取得します。そして、localhost:3000/packs/bundle.jsへリクエストを送る流れです。

しかし、localhost:3000ではなくlocalhost:8080でwebpack_dev_serverを立ち上げているので、当然のことながら、この段階ではアセットを取得できません。 なので、プロキシをしてRails側がwebpack_dev_serverからアセットを取得できるように設定してあげます。

Railsのプロキシ設定

プロキシの処理は、rack-proxyというGemをRailsに追加して実装しました。

require 'rack/proxy'
class DevServerProxy < Rack::Proxy
  def perform_request(env)
    if env['PATH_INFO'].start_with?('/packs/')
      env['HTTP_HOST'] = dev_server_host
      env['HTTP_X_FORWARDED_HOST'] = dev_server_host
      env['HTTP_X_FORWARDED_SERVER'] = dev_server_host
      super
    else
      @app.call(env)
     end
   end

  private

  def dev_server_host
    "localhost:8080"
  end
end

ここで行っていることは単純にlocalhost:3000/packs/で来たリクエストをlocalhost:8080/packs/へプロキシしているだけとなっています。 開発環境下のみでプロキシを行いたいので、developmentのconfigファイルに以下の設定を追加します。

config.middleware.use DevServerProxy, ssl_verify_none: true

これでRails側からフロントエンドのアセットを取得することができるようになったので、http://localhost:3000/packs/manifest.jsonにアクセスすると、webpack_dev_serverから配信されているマニフェストファイルを取得することができるはずです。

react-hot-loaderの導入と実装

ここまでで、Rails側がwebpack_dev_serverから配信されるアセットを取得できるようになったので、今まで通り、フロントエンドの開発を進めることができるようになったと思います。

ここからは、ReactでHMRを行う方法について解説します。

まず、HMRを行うためにreact-hot-loaderというpackgaeを追加します。

$ yarn add react-hot-loader

そして、.babelrcにも以下の設定を追記します。

{
  "plugins": ["react-hot-loader/babel"]
}

次に、Reactコンポーネントの実装に移ります。 react-hot-loaderにあるhot関数に、Reactプロジェクトのトップコンポーネントを引数として渡します。

import React from 'react';
import { hot } from 'react-hot-loader'
import { Todo } from './todo'

const App = () => {
  return (
    <>
      <Todo />
    </>
  )
}

export default hot(App)

HMRの確認

上記のReactコンポーネントを管理しているWebpackから出力先されるビルドファイルがbundle.jsとすると、先程定義したRailsのヘルパーメソッドの引数にbundleを指定します。

<%= javascript_bundle_tag('bundle') %>

これでwebpack_dev_serverを立ち上げて、Chromeのコンソールに以下の内容が出ていれば、HMRが有効になっています。 f:id:koki0906:20210115152754p:plain

試しに、先程指定したトップコンポーネント配下のコンポーネントをいじってみてください。HMRによって即時に変更内容が反映されるはずです!

まとめ

webpack_dev_serverとReactにおけるHMRの導入について解説しました。 RailsのAssets Pipelineの仕組みや無数にあるWebpackの設定など、技術的に理解するべき範囲は広く、難しい部分はありましたが、今回のプロジェクトを通じてフロントエンドのコードを触るエンジニアの生産性改善に貢献できたのは良かったです。

弊社CTOの記事にあるように、事業の成長に伴いエンジニアの人数も増えていくなかで、メンバー全体に関わる開発環境の問題は、プロジェクトを進めていく上で非常に大きな問題です。 すでに顕在化している問題や今後起きそうな課題に対して、エンジニアがその都度問題を提起し、解決に向けて行動することはとても大切なので、これからもそうした意識を持ち続けたいと思います。

スタメンでは一緒に働くエンジニアを募集しています。 興味がある方は、ぜひ採用サイトからご連絡ください!