Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 前編(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

タイトルは内容に即したものにしました。画像は元記事からの引用です。


  • 2020/01/16: 初版公開
  • 2021/05/13: 更新

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 前編(翻訳)

皆さんはアセットやJavaScript周りの変更で消耗してませんか?npm、Babel、ES6、Yarn、Webpack、Webpacker、Sprockets、これらのどこがどう違うのかさっぱりわからなかったりしますか?

Rails 6アプリケーションでJavaScriptエコシステム全体がどのように機能しているかという概念を急いで理解する必要にかられているそこのお方、あなたが探しているのはまさにこの記事です。

本記事の最後に、Rails 6プロジェクトにBootstrap 4とFontAwesome 6を追加する手順を記載しますのでご期待ください。

🔗 npm

npmは、JavaScript(正確にはNode.jsモジュール)のパッケージマネージャです。言ってみれば、JavaScript世界におけるRubygemです。

npm install <パッケージ名>

たとえばbootstrapをインストールするには以下を実行します。

npm install bootstrap

npmは、ダウンロードしたパッケージを./node_modulesに保存し、パッケージのリストを./package.jsonに保存します。

この時点では、npmとRailsがどう関係するのかについてはまだ何も説明していません。理由についてはこの先をお読みください。

🔗 yarn

yarnは、npmより新しいパッケージマネージャです。yarnはnpmのリポジトリからパッケージを取得する点は同じですが、その他の機能もあります。yarn.lockというファイルを自動生成し、必要なバージョンのnpmパッケージをそのファイルでロックします(RubyのGemfile.lockと同じようなものです)し、npmよりずっと高速です。

Rails 6アプリケーションでJavaScriptのライブラリが必要になった場合の操作は以下のとおりです。

  • 従来: JavaScriptのライブラリを提供するgemを追加し、app/assets/application.jsでライブラリをrequireします(これはSprocketsがコンパイルします)。
  • 現在: 同じ作業をyarn(https://yarnpkg.com)で行います。yarn add <パッケージ名>を実行してからrequireします(詳しくは後述します)。

原注: npmにもその後package-lock.jsonでロックする機能が追加されました。

🔗 ES6

ES6はJavaScriptの新しい標準です(JavaScriptの新バージョンと呼んでも結構です)。ES6にはクラス定義やデストラクタ、アロー関数といった極めて便利な機能が搭載されています。

さよならCoffeeScript、実は君のことがずっとキライだったよ。

訳注

CoffeeScriptはES6の仕様に影響を与えたという功績も指摘されています。

🔗 Babel

すべてのブラウザがES6を理解できるとは限らないため、ES6のJavaScriptがどんなブラウザでも動くようにするには、ES6のJavaScriptコードを読み取って旧来のES5 JavaScriptに変換するツールが必要になります。Babelはそのための変換を行うコンパイラです。

🔗 Webpack

BabelとYarn、それらの設定ファイルが揃ったら、次はアセットを自動コンパイルして環境を管理したりする手段が欲しくなります。

開発者はコードを書くこととアセットプリコンパイルの自動化だけに集中したいので、Webpackをバンドマスターとして使うことになります。Webpackはアセットを取り出して適切な個別のプラグインに渡します。プラグインは入力ファイルを適切なツールに処理させて、期待どおりの結果を得ます。

Webpackではたとえば以下のようなことができます。

  • ES6 JavaScriptコードを取り出す
  • babel-loaderプラグインでBabelにES6をコンパイルさせ、ES5 JavaScriptコードに変換する
  • できあがったpackをHTML DOMにインクルードできる形の1つのファイルにまとめる(<script type="text/javascript" src="path-to-es5-javascript-pack.js"></script>

🔗 Webpacker

Webpackerは、RailsアプリケーションにWebpackをいい感じに取り込めるgemです。Webpackerにはいくつかの初期設定ファイルが揃っているので(実際これだけで十分です)、設定のことを気にせずにコードを書き始められるようになります。

Webpackerのデフォルト設定では以下が指定されます。

  • 自分のJavaScript pack(つまりapplication.js)はapp/javascript/packs/の下に置かれること
  • ビューでJavaScript packをインクルードするにはjavascript_pack_tag '<pack名>'を使う(例: <%= javascript_pack_tag 'my_app' %>app/javascript/packs/my_app.jsをインクルードする)

本記事の最後に、これらをうまく動かせる極めて明快な例を示しますが、最初にSprocketsについて少しだけ説明しておく必要があります。

原注

他にもextract_css: falseというデフォルト設定がconfig/webpacker.ymlにあります。これは、WebpackがCSS packをstylesheet_pack_tagで提供する方法を認識していたとしても提供をオフにする設定です。本記事はJavaScriptに特化しているのでこれ以上深追いしませんが、この機能がデフォルトでオフになっていることは頭の隅に置いておくとよいでしょう。Webpackerではextract_css: falseがデフォルトの振る舞いであり、バグではないことを知っておけば、デバッグで無駄に時間を使わずに済みます。

もひとつ原注

rails assets:precompileを実行するとapp/assets/の下にあるものだけがプリコンパイルされると思っている方もいるかもしれません。実際のRailsは、app/javascript/の下にあるWebpackのアセットと、app/assets/の下にあるSprocketsのアセットを「両方」コンパイルします。

🔗 Sprockets 4

Webpackと同様に、Sprokectsも「アセットパイプライン」です。アセットパイプラインとは、アセットファイル(JavaScriptやCSSや画像など)を入力に取り、それらを処理して欲しいフォーマットを出力として生成するものです。

Rails 6から、Sprocketsに代わってWebpack(er)がRailsアプリケーションでJavaScriptを書くための新しい標準となりました。しかしSprocketsは「現在も」アプリケーションにCSSを追加するデフォルトの方法です。

Sprocketsは以下のように使われていました。

  • 従来: config.assets.precompileにある利用可能なアセットをリストするのに使われた(Sprockets 3とRails 5の時代)
  • 現在: 同じことをapp/assets/config/manifest.jsのマニフェストファイルでやるように変更された(Sprockets 4とRails 6の時代)

Sprocketsのパイプラインを用いてアセットをインクルードしたい場合は以下のようにします。

  • CSSを書く(ここではapp/assets/stylesheets/my_makeup.cssとする)
  • app/assets/config/manifest.jslink_treelink_directorylinkを用いて、stylesheet_link_tagでそのCSSを利用できるようにする(例: link my_makeup.css
  • ビューにstylesheet_link_tagを書いてCSSをインクルードする(例: <%= stylesheet_link_tag 'my_makeup' %>

🔗 Sprocketsを使ってたときのようにWebpackを使わないこと

流れに逆らって時間を無駄に溶かしたくなければ、このセクションの内容を理解しておくことが重要です。そんな時間があればES6を学ぶことに少しでも回せれば理想ですが、少なくともこれだけは言えます。

WebpackがSprocketsと違う点は、Webpackがコンパイルするのは「モジュールである」という点です。

正確には「ES6モジュール」です(Rails 6のデフォルト設定の場合)。これはどういうことなのでしょうか?つまり、モジュール内で宣言されるものは、ある意味すべて名前空間化されるということです。というのも、これはグローバルスコープからアクセスできるようにするものではなく、むしろインポートしてから使うものだからです。いくつか例を挙げてみましょう。

Sprocketsでは以下のように書けます。

  • app/assets/javascripts/hello.js:
function hello(name) {
  console.log("Hello " + name + "!");
}
  • app/assets/javascripts/user_greeting.js:
function greet_user(last_name, first_name) {
  hello(last_name + " " + first_name);
}
  • app/views/my_controller/index.html.erb:
<%= javascript_link_tag 'hello' %>
<%= javascript_link_tag 'user_greeting' %>

<button onclick="greet_user('Dire', 'Straits')">Hey!</button>

かなりシンプルに理解できますね。ではWebpackerではどうなるのでしょうか?

「上の2つのJavaScriptファイルをapp/javascript/packsの下に移動してjavascript_pack_tagでそれぞれインクルードすればいいんじゃね?」とお思いの方、ちょっと待った。それではたぶん動かないでしょう。

その理由は、hello()は1つのES6モジュールとしてコンパイルされるからです(user_greeting()も同様に(別の)ES6モジュールとしてコンパイルされます)。つまり、ビューで両方のJavaScriptファイルをインクルードしたとしても、user_greeting()関数にとってhello()関数は存在しないのです。

ではWebpackでSprocketsと同じ結果を得るには以下のようにすればよいのでしょうか?

  • app/javascript/packs/hello.js:
export function hello(name) {
  console.log("Hello " + name + "!");
}
  • app/javascript/packs/user_greeting.js:
import { hello } from './hello';

function greet_user(last_name, first_name) {
  hello(last_name + " " + first_name);
}
  • app/views/my_controller/index.html.erb:
<%= javascript_pack_tag 'user_greeting' %>

<button onclick="greet_user('Dire', 'Straits')">Hey!</button>

残念ながら、これも同じ理由で動きません。greet_user()がコンパイルされると、モジュール内に隠蔽されてしまうため、ビューからgreet_user()にはアクセスできないのです。

いよいよ本セクションで最も肝心な部分にたどり着きました。

  • Sprocketsでやる場合: ビューはJavaScriptファイルで公開されているものとやりとり可能(変数アクセスや関数呼び出しなど)
  • Webpackでやる場合: JavaScript packに含まれているものにはビューからアクセスできない

ではJavaScriptアクションをボタンでトリガーするにはどうすればよいのでしょう?答えは、packから、ある振る舞いをHTML要素に追加することです。後はvanilla JSだろうとjQueryだろうとStimulusJSだろうとどれでもやれます。

以下はjQueryでやる場合の例です。

import $ from 'jquery';
import { hello } from './hello';
function greet_user(last_name, first_name) {
  hello(last_name + " " + first_name);
}
$(document).ready(function() {
  $('button#greet-user-button').on(
    'click',
    function() {
      greet_user('Dire', 'Strait');
    }
  );
});

/* ES6の場合はこうやれる: */
$(() =>
  $('button#greet-user-button').on('click', () => greet_user('Dire', 'Strait'))
);
  • app/views/my_controller/index.html.erb:
<%= javascript_pack_tag 'user_greeting' %>

<button id="greet-user-button">Hey!</button>

結論: Webpackでは、欲しい振る舞いをビューではなく「packで」セットアップせよ

直前の例を用いて、繰り返し申し上げます。

何らかのライブラリ(たとえばselect2とかjQueryとか)が必要になった場合、そのライブラリをpackの中でインポートすればビューで使えるでしょうか?できません。あるpackでインポートしたライブラリはそのpackの中でお使いください。さもなければ本記事の次のセクションをお読みください。

原注

StimulusJSでJavaScriptコードを構成する方法やHTML要素に振る舞いをアタッチする方法を学びたいのであれば、StimulusJS on Rails 101がおすすめです。

「あらゆるものが隠蔽され名前空間化される世界」がどんなふうに動作するかを知りたい皆さまへ: ES6のあるモジュールがコンパイルされてES5コードに変換されると、そのモジュールの中身は1つの無名関数の内部に封じ込められ、その関数の外からはモジュール内で宣言された変数や関数に一切アクセスできなくなります。

(後編に続く)

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 後編(翻訳)

関連記事

Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた(翻訳)

Rails 5: Webpacker公式README — Webpack v4対応版(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。