言いたいこと
webpack
をあまり知らない人がwebpacker
を用いることで簡単に導入できるけど以下のことはちゃんと知っておいてほしかったり、考えてほしい。(切実)
- ビルドはRubyからコマンド実行してるけど、実際に発行しているnpm scriptは
webpack(-dev-sever) --config config/webpack/(development|test|production).js
なんで覚えておけ。 - デフォルトでmultiple entryになっているのでSplitChunksPluginかCommonsChunkPluginの設定はとりあえず入れておけ。
- 本当にwebpackerは必要ですか?時期が来たら捨てろ。
webpacker
の落とし穴
昨今では当たり前となっているwebpack
の導入を**webpack
をあまり理解していない人が簡単に入れれるという特徴がある。このwebpack
をあまり理解していない人**はまずいので導入当初によくおきた問題と解消方法をツラツラ書いていきます。
Rubyからしかビルド方法を知らない
まず、webpack
のビルドを簡単に行えるように準備してくれています。方法は3つくらい
-
bin/webpack(-dev-server)
を実行するとビルド -
bundle exec rake webpacker:compile
のrakeタスクでビルド -
bundle exec rake assets:precompile
をしてもビルド
詳しくは説明しませんが、上2つはwebpacker
が提供しているコマンドもしくはrakeタスクです。最後はwebpacker
がassets:precompile
をフックして、2つ目の自身が作ったrakeタスクを実行することでビルドするということが定義されていることによる実行です。
こうみると何も知らない人からすれば(webpack
による)JavaScriptのビルドはRubyから実行しないといけないということになるのですが、そうではありません。
実際webpacker
のソースを見ればわかるのですが、npm scriptを実行してるだけなのです。
書かれているソースはこれ
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を開発する際は邪魔です。
じゃあ、こいつはどういう条件で動いているかというとこいつです
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接続できるかどうかで判定しています。
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
から取得します。
dev_server:
https: false
host: localhost
port: 3035
しかし、よく見ると環境変数の値が設定されているとそれを優先して使用する処理になっているのでこれをうまく使い、docker-composeではdocker networkをうまく使い、WEBPACKER_DEV_SERVER_HOST
を設定することでバックエンドコンテナからフロントエンド(webpack-dev-server)コンテナへの接続を可能とします。
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(v4)ではSplitChunksPluginというものがあり、共通で使用するpackageのJavaScriptファイルを纏める機能があります。詳しくはググるなり以下を参照して下さい。後述するCommonsChunkPluginはv4では廃止です。
https://webpack.js.org/plugins/split-chunks-plugin/
まだwebpack(er)
(v3)を使ってる方はCommonsChunkPluginを使って纏めます。
https://webpack.js.org/plugins/commons-chunk-plugin/
ではwebpacker
ではどうするかですが、v4とv3で対応は異なります。
webpacker v4
まず、v4ですがドキュメントに書いてるので以下のように変更して下さい。
const { environment } = require('@rails/webpacker')
+ // デフォルトのセッティングでいいならこの行を足すだけで有効になる。
+ environment.splitChunks()
+ // カスタムなセッティングを入れるなら以下のようにファンクションで処理をする
+ //environment.splitChunks((config) => Object.assign({}, config, { optimization: { splitChunks: false }}))
module.exports = environment
この設定を入れたら、JavaScriptファイルを読み込むヘルパーメソッドは javascript_packs_with_chunks_tag
を使用するようにします。
<%# 従来のjavascript_pack_tagでは必要ファイルがすべてロードされないため、javascript_packs_with_chunks_tagを使用する %>
<%#= javascript_pack_tag 'hoge' %>
<%= javascript_packs_with_chunks_tag 'hoge' %>
webpacker v3
まだ、v3を使ってる方はこちらに書いてるに書いてるので以下のように変更して下さい。
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
使用する方法は以下のようにvender.jsに纏まるので、以下のように足します。
<!DOCTYPE html>
<html>
<head>
+ <%= javascript_pack_tag "vendor" %>
<%= javascript_pack_tag 'entry_file_name' %>
</head>
まとめ
ここまで読むとわかると思いますが、ここまでするならwebpacker
入れるよりwebpack
で純粋にビルドすればいいという雰囲気になってきますよね。webpackerでwebpackのコンフィグ変えるの面倒だし
使う道具はその特性をちゃんと理解するといいです。