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

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

- ビルドはRubyからコマンド実行してるけど、実際に発行しているnpm scriptは`webpack(-dev-sever) --config config/webpack/(development|test|production).js`なんで覚えておけ。
- デフォルトでmultiple entryになっているので[SplitChunksPlugin](https://github.com/rails/webpacker/blob/master/docs/webpack.md#add-splitchunks-webpack-v4)か[CommonsChunkPlugin](https://github.com/rails/webpacker/blob/master/docs/webpack.md#add-common-chunks-deprecated-in-webpack-v4)の設定はとりあえず入れておけ。
- 本当に**webpacker**は必要ですか？時期が来たら捨てろ。

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

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

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

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

こうみると何も知らない人からすれば(`webpack`による)JavaScriptのビルドはRubyから実行しないといけないということになるのですが、そうではありません。
実際`webpacker`のソースを見ればわかるのですが、npm scriptを実行してるだけなのです。
書かれているソースは[これ](https://github.com/rails/webpacker/blob/master/lib/webpacker/webpack_runner.rb)

```ruby: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はこれです。

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

## docker-compose（複数コンテナ）での開発環境設定
次にRubyがなくてもJavaScriptをビルドできるってことでバックエンドとフロントエンドのコンテナを開発することができます。
が、`webpacker`の処理には便利なようでちょっと癖のあるものがあります。

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

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

じゃあ、こいつはどういう条件で動いているかというと[こいつ](https://github.com/rails/webpacker/blob/master/lib/webpacker/manifest.rb)です

```ruby: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`のこの設定のことです。

```yaml
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接続できるかどうかで判定しています。

```ruby: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`から取得します。

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

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

```yaml: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(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ですが[ドキュメント](https://github.com/rails/webpacker/blob/master/docs/webpack.md#add-splitchunks-webpack-v4)に書いてるので以下のように変更して下さい。

```diff:config/webpack/environment.js
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` を使用するようにします。

```erb
<%# 従来のjavascript_pack_tagでは必要ファイルがすべてロードされないため、javascript_packs_with_chunks_tagを使用する %>
<%#= javascript_pack_tag 'hoge' %>
<%= javascript_packs_with_chunks_tag 'hoge' %>
```

### webpacker v3

まだ、v3を使ってる方は[こちら](https://github.com/rails/webpacker/blob/master/docs/webpack.md#add-common-chunks-deprecated-in-webpack-v4)に書いてるに書いてるので以下のように変更して下さい。

```diff: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
```

使用する方法は以下のようにvender.jsに纏まるので、以下のように足します。

```diff: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のコンフィグ変えるの面倒だし~~
使う道具はその特性をちゃんと理解するといいです。