Rails
docker
webpack
webpacker

Webpacker使うなら最低限これだけは知っておいてほしいこと

言いたいこと

webpackをあまり知らない人がwebpackerを用いることで簡単に導入できるけど以下のことはちゃんと知っておいてほしかったり、考えてほしい。(切実)

  • ビルドはRubyからコマンド実行してるけど、実際に発行しているnpm scriptはwebpack(-dev-sever) --config config/webpack/(development|test|production).jsなんで覚えておけ。
  • デフォルトでmultiple entryになっているのでCommonsChunkPluginの設定はとりあえず入れておけ。
  • 本当にwebpackerは必要ですか?時期が来たら捨てろ。

webpackerの落とし穴

昨今では当たり前となっているwebpackの導入をwebpackをあまり理解していない人が簡単に入れれるという特徴がある。このwebpackをあまり理解していない人はまずいので導入当初によくおきた問題と解消方法をツラツラ書いていきます。

Rubyからしかビルド方法を知らない

まず、webpackのビルドを簡単に行えるように準備してくれています。方法は3つくらい

  1. bin/webpack(-dev-server)を実行するとビルド
  2. bundle exec rake webpack:compileのrakeタスクでビルド
  3. bundle exec rake assets:precompileをしてもビルド

詳しくは説明しませんが、上2つはwebpackerが提供しているコマンドもしくはrakeタスクです。最後はwebpackerassets:precompileをフックして、2つ目の自身が作ったrakeタスクを実行することでビルドするということが定義されていることによる実行です。

こうみると何も知らない人からすれば(webpackによる)JavaScriptのビルドはRubyから実行しないといけないということになるのですが、そうではありません。
実際webpackerのソースを見ればわかるのですが、npm scriptを実行してるだけなのです。
書かれているソースはこれ

webpacker/lib/webpacker/webpack_runner.rb
require "shellwords"
require "webpacker/runner"

module Webpacker
  class WebpackRunner < Webpacker::Runner
    def run
      env = { "NODE_PATH" => @node_modules_path.shellescape }
      cmd = [ "#{@node_modules_path}/.bin/webpack", "--config", @webpack_config ] + @argv

      Dir.chdir(@app_path) do
        exec env, *cmd
      end
    end
  end
end

見ての通り、ただnpm scriptを実行しているので、これさえわかっておけばRubyがないとビルドできない!てことはなくなる。
実際に叩かれるnpm scriptはこれです。

./node_modules/.bin/webpack --config config/webpack/development.js

docker-compose(複数コンテナ)での開発環境設定

次にRubyがなくてもJavaScriptをビルドできるってことでバックエンドとフロントエンドのコンテナを開発することができます。
が、webpackerの処理には便利なようでちょっと癖のあるものがあります。

assets:precompile同様に開発環境ではリクエスト時に未ビルドもしくは変更済みJavaScriptファイルをビルドする

Dockerとか使わない単一のローカル開発ならばいいんでしょうが、先に書いたバックとフロントのDockerコンテナを分けたdocker-composeを開発する際は邪魔です。

じゃあ、こいつはどういう条件で動いているかというとこいつです

webpacker/lib/webpacker/manifest.rb
class Webpacker::Manifest
  class MissingEntryError < StandardError; end

...
  def lookup(name)
    compile if compiling?
    find name
  end
...
  private
    def compiling?
      config.compile? && !dev_server.running?
    end
...
end

要は指定されたファイルを返す処理の際に都度ビルド(config.compile?)が設定されており、webpack-dev-server(!dev_server.running?)が動いていない時にコンパイルしたファイルを返すという処理になっています。

1つ目の条件である都度ビルド(config.compile?)の設定ですが、これはwebpacker.ymlのこの設定のことです。

default: &default
  source_path: app/javascript
...
development:
  <<: *default
  compile: true # < This!

このyamlファイルはNodeスクリプトが読みこんで実行することがあるため、動的に設定することはちょっと面倒です。なのでこの部分の変更は諦めるか思い切ってfalseにしてもいいと思います。

2つ目の条件であるwebpack-dev-server(!dev_server.running?)が動いるという条件でなんとかします。そもそもwebpack-dev-serverが動いているというのはどうやって見ているかというとwebpack-dev-serverのホストとポート設定でTCP接続できるかどうかで判定しています。

webpacker/lib/webpacker/dev_server.rb
class Webpacker::DevServer
...
  def running?
    if config.dev_server.present?
      Socket.tcp(host, port, connect_timeout: connect_timeout).close
      true
    else
      false
    end
  rescue
    false
  end
...
  def host
    fetch(:host)
  end

  def port
    fetch(:port)
  end
...
  private
    def fetch(key)
      ENV["WEBPACKER_DEV_SERVER_#{key.upcase}"] || config.dev_server.fetch(key, defaults[key])
    end
...
end

ここのホストとポートは同じくwebpacker.ymlから取得します。

webpacker.yml
  dev_server:
    https: false
    host: localhost
    port: 3035

しかし、よく見ると環境変数の値が設定されているとそれを優先して使用する処理になっているのでこれをうまく使い、docker-composeではdocker networkをうまく使い、WEBPACKER_DEV_SERVER_HOSTを設定することでバックエンドコンテナからフロントエンド(webpack-dev-server)コンテナへの接続を可能とします。

docker-compose.yml
version: '3'

services:
  backend:
    command: 'bundle exec rails s -b 0.0.0.0'
    environment:
      - WEBPACKER_DEV_SERVER_HOST: frontend
  frontend:
    command: './node_modules/.bin/webpack-dev-server --config config/webpack/development --host 0.0.0.0 --port 3035'

これによりバックエンドコンテナでのビルドを抑制することができます。

webpack-dev-serverがクソ重くなる

原因はただのメモリ使いすぎです。画像は仕方ないとしてもwebpack-dev-serverはビルドしたものをすべてメモリに保持します。webpackerはデフォルトでmultiple entryになっているため、エントリーのファイル数が増えれば増えるほどこの傾向が出てきます。
特にJavaScriptではエントリーのファイルにimportしたファイルがどんどん結合されてファイルが肥大化することによりメモリ使用量が増えます。また、ファイルが肥大化することによって実際のwebページに訪れた人にも大きいJavaScriptファイルをダウンロードさせることになることとなってしまう可能性があります。
そこで、webpackにはCommonsChunkPluginというものがあり、共通で使用するpackage等のJavaScriptファイルを纏める機能があります。詳しくはググるなり以下を参照して下さい。
(ver4で廃止とか見るけど、documentには残ってるし使えるけどどうなってんの?)
https://webpack.js.org/plugins/commons-chunk-plugin/

ではwebpackerではどうするかですが、これはドキュメントに書いてるので以下のように変更して下さい。

config/webpack/environment.js
  const { environment } = require('@rails/webpacker')
+ const webpack = require('webpack')
+
+ environment.plugins.append(
+   'CommonsChunkVendor',
+    new webpack.optimize.CommonsChunkPlugin({
+      name: 'vendor',
+      minChunks: (module) => {
+        // this assumes your vendor imports exist in the node_modules directory
+        return module.context && module.context.indexOf('node_modules') !== -1
+      }
+    })
+  )

  module.exports = environment
app/view/layouts/application.html.erb
  <!DOCTYPE html>
  <html>
    <head>
+     <%= javascript_pack_tag "vendor" %>
      <%= javascript_pack_tag 'entry_file_name' %>
    </head>

まとめ

ここまで読むとわかると思いますが、ここまでするならwebpacker入れるよりwebpackで純粋にビルドすればいいという雰囲気になってきますよね。webpackerでwebpackのコンフィグ変えるの面倒だし
使う道具はその特性をちゃんと理解するといいです。