Rails アプリのアセットを Webpacker で管理する

Rails アプリのアセットを Webpacker で管理する

世間では GW ですが、皆さんいかがお過ごしでしょうか。こちらは仕事も落ち着いてきたため、直近のプロジェクトでそこそこ時間を使う羽目になった Webpacker についてのブログ記事を書くことにしました。

はじめに

Ruby on Rails では Sprockets というモジュール(?)によって、JavaScript, CSS, 画像などのアセットを管理する事が出来ます。これは Rails 独自の仕組みであり、それによる問題点なども目立つようになったため、フロントエンド界隈で広く使われている Webpack を使いたいという要望は以前からあったようです。(その辺りの経緯などは、検索すれば色々出てくるので、ここでは記載しません。)

そうした要望に応えるものが Webpacker で、簡単に言うと Webpack の機能を Rails に組み込んだもので、使う側からすると主に以下の2つの機能が提供されています。

  • rails コマンドに、Webpacker 関連のサブコマンドの追加
  • Sprockets と同様に、pack 済みのアセットを読み込むためのタグ(ヘルパー)

本記事では、

  • Webpacker の設定方法、使い方
  • Sprockets との併用方法
  • 実際に起こった細かい問題とその解決方法

について説明します。

なお、私自身はフロントエンドエンジニアでもなければ、Rails にも特別詳しいわけでは無いため、不正確な記述があるかもしれませんので、その際にはご指摘頂ければ幸いです。

環境は、特別断りがない限り、弊社のプロジェクトで使用した以下のバージョンのものです。

  • Rails 5.1
  • Webpacker 3.4.3

Webpacker とは

主な用途

Webpacker の README.md の冒頭に、以下のような記述があります。

Webpacker makes it easy to use the JavaScript pre-processor and bundler webpack 4.x.x+ to manage application-like JavaScript in Rails. It coexists with the asset pipeline, as the primary purpose for webpack is app-like JavaScript, not images, CSS, or even JavaScript Sprinkles (that all continues to live in app/assets).

However, it is possible to use Webpacker for CSS, images and fonts assets as well, in which case you may not even need the asset pipeline. This is mostly relevant when exclusively using component-based JavaScript frameworks.

簡単に要約するとこんな感じです。

  • Webpacker は webpack 4.x.x を Rails で管理するためのもの
  • asset pipeline (Sprockets) と共存できる
  • webpack の主な用途は、”app-like” JavaScript のため
    • 画像、CSS、あるいは JavaScript “Sprinkles” のためのものではない(※)
      • ただし、Webpacker をそれらの用途に使うことも出来る

※ ページ単位で必要となる、小さな JavaScript の事を指しているようです

ただし、webpack の web サイトを見る限り、Webpacker チームの(?) webpack に対するこの理解は正しくないように思えます。現に、webpack のトップページには以下のような画像が掲載されているのですが、画像、CSS が JS と同列に扱われていることは明らかです。

Sprockets との使い分け

Webpacker と Sprockets という、似たようなことをするものが2つあるのですが、これらをどう使い分ければ良いのでしょうか。ネット上には色々意見があるので、参考になるかと思います。個人的には、以下の2つのページが参考になりました。

色々な意見があることは承知の上で、自分なりの結論を書くなら、以下の通りです。

  • 新規プロジェクトでは、Webpacker を基本とする
    • Sprockets に依存する gem などが必要な場合は、Sprockets を併用する
  • 既存プロジェクトの移行の場合は、新規モジュールなどから徐々に Webpacker ベースに移行する

まだまだ開発中

Webpacker は、積極的に開発されている途中で、API も頻繁に変わります。プロジェクトで使用したときは 3.4.3 が最新だったのですが、既に 3.5 がリリースされているようです。

また、Webpacker の情報を扱った web ページは、情報が古いものが多いので注意が必要です。本ページの内容もすぐに古くなってしまう可能性もありますので、ご注意下さい。

Webpacker のインストール、設定方法

インストール

基本的にはドキュメントに従えば良いので、気をつける点などを中心に記載します。

Gemfile に以下を追加し、

gem 'webpacker', '~> 3.5'

以下のコマンドを実行します。

bundle
bundle exec rails webpacker:install

ディレクトリ構成

変更することも出来ますが、デフォルトでは app/javascript 以下が Webpacker で管理するアセットを置く場所です。CSS などもこの app/javascript ディレクトリに入れることになるので、名前が気に入らない方は変更した方が良いと思います。

そのディレクトリ配下の構成ですが、README に記載のものを少し修正しつつ説明します。あくまで一例ですので、「こうした方が良いよ」とかのご意見があれば、コメント等でお願いいたします。

app/javascript:
  ├── packs:
  │   # エントリーポイント
  │   └── application.js
  └── src:
      └── application.css
      └── js
      │   └── foo.js # 自分たちで開発した JS
      └── scss
      │   └── style.scss # 他の SCSS をここから読み込む
      │       └── mixins
      │           └── _bar.scss
      │       └── :
      └── images: # README だと javascript 直下ですが、 src の下に含めるようにしました
          └── logo.svg

packs ディレクトリにあるものがエントリーポイントとなり、複数のエントリーポイントを用意することも出来ますが、本プロジェクトでは application.js を唯一のエントリーポイントとしました。なお、application.JS という名前ですが、このファイルから CSS などの各種アセットを import します。詳しくは、webpack の以下のドキュメントなどを参照して下さい。

Asset Management

エントリーポイント

今回作成した application.js は以下のようになります。

import "jquery"
global.$ = require('jquery')

// JS ----------
// yarn 経由でインストールしたもの
import 'popper.js/dist/umd/popper';
import 'bootstrap/dist/js/bootstrap';
import 'slick-carousel/slick/slick';
import '@fengyuanchen/datepicker/dist/datepicker';
// 自前の
import 'src/js/foo';

// CSS ----------
// yarn 経由でインストールしたもの
import 'slick-carousel/slick/slick.css';
import '@fengyuanchen/datepicker/dist/datepicker.css';
// 自前の
import 'src/scss/style'

// 後述
import Rails from 'rails-ujs'
Rails.start()

エントリーポイントはこのような形式です。yarn (あるいは npm)でインストールしたモジュールの JS, SCSS、自前の JS, SCSS をこのエントリーポイントから import します。

Rails には ajax 周りの処理を楽にしてくれる jquery-ujs というものがあったのですが、Rails 5.1 からは、jQuery への依存がなくなって、 rails-ujs という名前に変わり、Rails 本体に取り込まれるようになりました。エントリーポイントで rails-ujs を読み込んで Rails.start() を実行しておくと、今までと同様の処理が出来るのだと思います。(ここは自信なし)

各種モジュールのインストール

webpack で使用できる CommonJS や UMD 形式に対応したモジュールは、Webpacker で管理できます。そうしたモジュールのインストール方法としては

  • yarn を使う
  • (yarn でインストール出来ないものは)ダウンロードしてきて、ソースツリーに加える

という方法になります。

その上で、上述のエントリーポイント(application.js)でそうしたモジュールを import する必要があります。詳しくは、各モジュールのドキュメントか webpack のドキュメントを参照して下さい。

基本的な使い方

アセットのコンパイル

コンパイルの仕方は主に2通りあります。

1つは事前に全てコンパイルする方法で、以下のコマンドを実行します。

bundle exec rails webpacker:compile

もう1つは、ファイルの更新を watch しておき、変更があったら随時コンパイルをする方法です。やり方は何通りかあるのですが、一番単純なのは、1つターミナルを開いておき、以下のコマンドを実行して立ち上げっぱなしにしておくことです。

./bin/webpack --watch --colors --progress

なお、今回は開発環境に Vagrant を使っていたのですが、Vagrant box 内で上のコマンドを実行しても、ホスト上でのファイルの変更が検知できないという問題がありました。これについては、後述します。

アセットの読み込み

Sprockets と大体同じだと思いますが、以下のタグで JS, CSS を読み込みます。

    <%# CSS の読み込み。"application" は、エントリーポイントの名前 %>
    <%= stylesheet_pack_tag 'application' %>

    <%# JS の読み込み。"application" は、エントリーポイントの名前 %>
    <%= javascript_pack_tag 'application' %>

Sprockets の併用

最初の方に書いた通り

  • 既存の Rails アプリに Webpacker を導入する
  • Sprockets に依存する gem を使う

といった場合に、Sprockets と Webpacker を併用する必要があります。以下で具体的な方法をいくつか記載します。

アプリケーション全体では使わないようにする

例えば、Rails アプリを作成した直後は、Sprockets 用の以下ようなファイルが存在すると思います。

// app/assets/javascripts/application.js
//= require turbolinks
/*
 * app/assets/stylesheets/application.css
 *= require_tree .
 *= require_self
 */

そして、それらを、一般的には layout 内で以下のように読み込むと思います。

<%= stylesheet_link_tag "application", media: "all" %>
<%= javascript_include_tag "application" %>

まずは、このタグを消して、基本的には Sprockets のアセットは読み込まず、ページ(あるいは Controller )単位で必要に応じて読み込むようにします。

弊社のプロジェクトでは、controller 単位で JS を作って、それを読み込むようにしました。(次項)

コントローラー単位で JS を作成

今回は、 app/assets/javascripts 配下に、各コントローラー毎の JS を作成し、それを読み込むことにしました。

今までは config/initializers/assets.rbRails.application.config.assets.precompile に precompile する JS を書いていくというのが一般的だったようですが、最近は manifest ファイルを使うのが推奨されているっぽいので、まずは、config/initializers/assets.rb に以下のように記載します。

Rails.application.config.assets.precompile += %w[manifest.js]

app/assets/config/manifest.js は、以下のように書きました。

//= link_tree ../images
//= link_tree ../javascripts .js
// ES6 は、デフォルトでは使えない。詳細は後述
//= link_tree ../javascripts .es6
//= link_directory ../stylesheets .css

そして、layout に以下のタグを追記し、コントローラー単位の JS を読み込みます。

    <%= javascript_include_tag params[:controller] %>

Webpacker 管理の JS モジュールを Sprockets 管理の JS から使用する

Webpacker 管理の JavaScript のモジュールは、global 空間を汚染せず、他のモジュールからは import して使用します。全ての JS が Webpacker 管理の場合は、特に問題にならないと思いますが、Sprockets を併用する場合、Webpacker で管理されている JS モジュールの機能を Sprockets 管理の JS から使用したい場合もあると思います。

その場合の手順としては、大雑把に以下の通りです。

  1. 各モジュールで、外部に晒したいものを export する
  2. エントリーポイント(application.js)で、export する
  3. Webpacker の設定で、”output” の対象とする
  4. Sprockets 管理の JS で使用する

(Webpack に詳しくないので、もしかしたらもっと良い方法があるかもしれません。)

以下に、各手順を説明します。

1. 各モジュールで、外部に晒したいものを export する

app/javascript/src/js/foo.js で、以下のようにします。

function foo_func() {
  console.log('foo! foo!');
}

export default foo_func

これで、foo_func は、Webpacker 管理の他の JS で import 出来るようになります。

2. エントリーポイント(application.js)で、export する

1で export したものを、エントリーポイントの app/javascript/packs/application.js 内で読み込んで export します。

export { default as foo } from 'src/js/foo';

3. Webpacker の設定で、”output” の対象とする

config/webpack/environment.js で、以下の設定を追加します。

environment.config.set('output.library', ['Packs', '[name]'])

[name] というのはプレースホルダーですので、app/javascript/packs/application.js の内容は、 Packs.application として出力されます。

詳しくは、webpack の以下のドキュメントを参照して下さい。
Output

4. Sprockets 管理の JS で使用する

3 で output された JS は、Sprockets 管理の JS から、以下のように使用できます。

Packs.application.foo()

細かい問題と解決方法

色々問題があったので、解決方法も含めて記載します。一部、Sprockets の話も含まれます(別エントリーに分けるのが面倒なので)。

jQuery を使う(あまり自身無し)

jQuery を Webpacker のバンドルに含めて使えるようにするのは、色々試して以下のようになっています。

app/javascript/packs/application.js

import "jquery"
global.$ = require('jquery')

config/webpack/environment.js

environment.plugins.prepend('Provide', new webpack.ProvidePlugin({
  $: 'jquery',
  jQuery: 'jquery',
  "Tether": 'tether',
  Popper: ['popper.js', 'default']
}))

正直なところ、この設定が正しいのかどうかもちょっと分からないですし、無駄な設定などがあるかもしれません。

Vagrant 環境での watch の問題

webpack に限らず、Vagrant box とファイル変更の監視系の処理は相性が悪いです。webpack の設定で、polling をするように修正しました。具体的には、config/webpack/development.js に以下を追記します。

environment.config.merge({
    devServer: {
        watchOptions: {
            poll: 5000
        }
    },
    watchOptions: {
        poll: 5000
    }
})

Compass を使う

最初にも書きましたが、私はフロントエンドエンジニアでは無いので詳しくは分かりませんが、以前は Compass ってのがそこそこ使われていたようですが、現在は開発が止まっていてサポートされていないようです。

が、今回のプロジェクトでは、別の会社の方が作成した HTML, CSS で Compass を使っていたので、使わざるを得ませんでした。

色々検索したり試したところ、compass-mixins というのが Compass を Bower レポジトリ用に使えるようにしたものらしく、これを yarn 経由でもインストールすることが出来ました。

compass-mixins をインストールすると、node_modules/compass-mixins/ 配下にファイルが色々置かれますが、それらのファイルの中で、さらに以下のように別のファイルを import しています。

@import "compass/functions";

そのため、それがうまくいくように、config/webpacker.yml で、以下の記述を追加しました。

  resolved_paths: ['node_modules/compass-mixins/lib']

IE でエラーになる

詳細は省きますが、Webpacker では、babel という transpiler を使って、ECMA Script 6 (以下 ES6) とかも、IE で認識できる通常の JS に変換をすることが出来るはずなのですが、現状はバグがあってうまくいきません。問題の詳細は以下のリンクを参照して下さい。

webpacker 3.2.1 hardcodes uglifyOptions: { ecma: 8 } which ignores Babel target options and breaks IE11 compatibility · Issue #1235 · rails/webpacker

解決方法は、同 issue に記載されているものか、あるいは以下の issue に記載の方法を行います。(どちらも、本質的には一緒です。)

webpacker:compile doesnt seem to be loading babelrc · Issue #1336 · rails/webpacker

Sprockets で ES6 を使う

検索するといくつかの方法があるのですが、今回は sprockets-es6 を使いました。使い方は、README を見て下さい。

その他の方法としては、以下のようなものがあります。

  • Sprockets 4 を使う → 新しいバージョンは怖い・・・
  • sprockets-commoner を使う → 試したもののうまくいきませんでした(詳細は省略)

まとめ

Webpacker を使うと、JS、CSS のアセットを webpack で管理しつつ、違和感なく Rails から使うことが出来ます。

ネットの古い情報だと、以前の Webpacker はもっと使いづらかったようですが、私が Webpacker を使った2018年初頭では、大分こなれてきた感じがします。私は、ガチフロントエンド勢では無いので、現状でも(設定方法が分かってしまえば)あまり不便は感じなかったのですが、もっと細かいことをやりたい場合には、素の webpack を使った方が良いケースがあるかもしれません。

Sprockets と Webpacker の関係ですが、今後の新規 Rails プロジェクトであれば、Sprokets は基本的には使わず、使いたい gem が Sprockets に依存しているときのみ使う、という感じにするのが良いと思います。