導入部
新規PJでwebpackerの利用を考えている人へ(2022/01/24 追記)
WebpackerはRails 7以降では使う理由がなくなり、今後は開発が止まっていきます。
新規PJでwebpackerは採用しない方が良いでしょう。以下の公式リンクをご参照ください。
はじめに
RubyやRailsをメインで開発していて、「正直JSはよくわからん」という人は、結構いると思います。
しかしブラウザがそこにある限り人類はJSから逃れることはできません。
この記事は、JSに苦手意識のあるRubyistたちにwebpackとお友達になってもらうために書きました。
webpackerが何なのか知っておくと、役に立つ日が絶対に来るので、ぜひ知らない人は読んでください。
もし私がJSをよく知らないRails開発者だったら、webpackやwebpackerが何者なのかを、こう教えてほしいという気持ちで書いています。
対象読者
- webpackがなんのためにあるのか分かっていない開発者
- webpackerをよく分からないうちに使っているRails開発者
概要
この記事で説明すること
- なぜ私達はwebpackを使うのか(前半)
- webpackとwebpackerは何をしているのか(後半)
この記事で説明しないこと
- webpack.config.jsの詳細な書き方
webpackの目的を理解する
webpackとwebpackerの関係
まず前提として、webpackとwebpackerは別物です。
webpackはJSのnpmのパッケージです。JSのコミュニティの中で育ちました。npmというのは、Node.jsで使えるパッケージ管理ツールのことで、つまりはRubyでいうbundlerです。JSの開発者たちは、このnpmか、その代替のyarnをみんな使っています。
そしてwebpackerはRubyのgemです。Railsでもwebpackが楽に使えるように作られました。
なので、大本のwebpackが何者かを知ることで、webpackerが何をしたいのかも知ることができます。
ではwebpackとは何なのでしょうか。
webpack怪獣説
「webpackとは何か」を一言で表すと、だいたいモジュールバンドラーと説明されます。
モジュールバンドラー……? 意味不明ですね。ウルトラマンの怪獣にそんな名前のやつがいたかもしれません。
しかし怪獣図鑑を見てもモジュールバンドラーの説明はどこにも書いてありません。
「JSの人たちはなんて不親切なんだ、JSは俺向きの言語じゃない。俺はオープンクラスやmethod_missingやモンキーパッチで生きていくぜ」となる気持ちも分かりますが、私の話を聞いて下さい。
モジュールバンドラーとはモジュールをひとまとめに、つまりバンドルしてくれるためのものです。
待って下さい、まだ何も説明していません。もうちょっと話を聞いて下さい。
Railsではこれができるよね
Railsでそれなりの規模のWebアプリを作るとしたら、そのプロジェクトにはきっと、沢山ファイルがあるはずです。
それらのファイルにはなにかしらのクラスが書いてあり、メソッドが書いてあります。
もしかしたらそのクラスは、更にモジュールの中にあるかもしれません。
例えば、こんな感じ。Railsによくありそうな架空のコードです。
いかにも、管理者がユーザー情報を一括CSVインポートするためのページを表示しそうなコントローラです。
module Admin
class ImportUsersController < ApplicationController
# [GET] /admin/import_users(.:format)
def index
# 何かの処理A
end
end
end
さて、機能追加をしていく中で、上のコードとは全然別の場所にこういうコントローラを生やしました。
module User
class ImportUsersController < ApplicationController
# [GET] /user/import_users(.:format)
def index
# 何かの処理B
end
end
end
ImportUsersController
という名前のコントローラが異なるmoduleに2つできてしまいましたが、何か問題になるでしょうか? ……何も問題はありません。moduleで名前空間が分かれているからです。
ちょっと無理やりな例ですが、「どうしても別モジュールの中でAdminの中身を呼びたい!」という時でも、
Admin::ImportUsersController
のように指定することで、呼ぶこと自体も一応可能です。やっていいかは別ですが。
JSの昔話
Rubyでは当たり前のようにできるこのようなことが、JSでは長らく簡単にはできませんでした。
具体的に何ができなかったかというと、モジュールを分割してモジュール間のスコープを分けるというただそれだけのことができなかったのです。ファイルを分けても、複数のファイルを同時に読み込んでしまえば容易に名前が衝突してしまいました。
「JSは全てがグローバルスコープだったということ?」
いいえ、流石にそこまででもありません。
古(いにしえ)のJSにも、グローバルじゃないスコープは一応ありました。関数スコープです。
関数スコープとは何かというと、関数の中で宣言した変数は外からはアクセスできないというスコープです。
var globalScope = "グローバルなスコープ";
function hogeFunc() {
var functionalScope = "関数の中では、どんな変数名をつけても外側からは上書きされない";
}
古のJSには、Rubyのmoduleのような名前空間を分ける仕組みも、class構文さえもありませんでした。
ゆえに人々は知恵を使い、グローバル汚染を防ぐために、関数スコープを利用して、「クロージャ」と呼ばれるクソ読みにくい謎の構文や、「即時関数」というカッコのオバケを生み出してなんとか騙し騙しやってきました。
しかしそれは過去の話です。
俺の考えた最強のJS
グローバル汚染を防ぐためには嫌々クソキモい構文を使わなくてはいけなかったつらすぎるJSですが、世の天才たちは当然のようにブチ切れ、プログラマ三大美徳の短気さをパワーに、より良いJSを生み出そうとしました。
JSでの開発体験をより良くしようと、それぞれの開発者がJSの仕様策定をすべく立ち上がりました。
それぞれの場所でそれぞれの開発者が「俺の考えた最強のJS」で仕様を作っていった結果、様々なモジュールの仕組みが生み出されます。
- CommonJS (CJS)
- ECMAScript Module (ESM)
- Asynchronous Module Definition (AMD)
これらは全てがモジュールを解決するための文法を定義しています。
今も生き残っているのはCJS
とESM
です。モジュールのインポートの仕方を雑に解説します。
CommonJSの書き方
その歴史的経緯からNode.jsで見ることが多いと思います。こんな感じです。
const a = require('./AnotherFile.js');
a.anotherFileMethod();
このように書くことで、名前空間を汚染せずにファイルを分けることができるようになりました。
Sprocketsの//= require
とは別物なのでお気をつけください。
ECMAScript Module
ECMAScriptというのは、雑に言うと一番正式なJavaScriptの仕様です。
そこで定義されている文法です。今後はこれが主流になっていくと思って頂いて大丈夫です。
import a from './AnotherFile.js';
a.anotherFileMethod();
こちらの方法でも、名前空間を汚染せずにファイルを分けることができるようになりました。
かなり説明が雑なので、詳しい使い方は別の記事等を参照してください。
ともかく、名前空間を分けられる文法ができた
そんなわけで、コードのモジュール化ができて、JS開発者はみんな幸せになりました。
解決したいことは1つなのに、複数の仕様ができちゃっていますが、些細なことなので目を瞑りましょう。
めでたしめでたし。
ただしIE、テメーはダメだ
RubyにCRubyがあり、mrubyがあり、JRubyがあるように、JSにも様々な実装があります。Rubyが1つではないように、JSもまた1つではないのです。
Chromeが搭載しているV8、FirefoxのSpiderMonkey、Internet ExplorerのChakraのように、各社別々のJSのエンジンを持っています。JSそれ自体を解釈するための実装を、個別に持っているのですね。
ブラウザ以外でもこれらのエンジンは使われています。例えばNode.jsや、WindowsのJScriptでも使われています。もともとのエンジンに、更に機能追加をしている形です。
最近だとどこもGoogleのV8に収束していっているようですが、それはひとまず置いておきましょう。
問題はIEです。
今でこそMicrosoftさえもOffice365でIEのサポートを打ち切ったりしていますが、5, 6年前は、IEのサポートを打ち切るだなんてことはとても考えられませんでした。
考えてみてください、「新しい素敵な文法をJSの仕様に増やしたよ!」と言っても、仕様はあくまで仕様であり、実装してくれないと話になりません。「IEのChakraに、ECMAScript Module追加してね。お願い☆」と言ったらMicrosoftは追加してくれるでしょうか? 逆に、もしあなたがMicrosoftの役員だったら、高いお金をかけて実装する決断を下せるでしょうか? もう新ブラウザのEdgeを開発しているのに。
そういうわけでIEではECMAScript ModuleもCommonJSも動きません。
諦めたらそこで試合終了ですよ
新しい文法がすぐそこにあるのに、クロージャとか即時関数とかいうキモい構文は絶対書きたくない。
でもIEでもコードを動かさなきゃいけない。
そのようなジレンマを解消すべく、開発者は再び知恵を絞りました。
「なら、モダンなモジュールで書いたものを、IEでも読めるように、うまいこと1つのファイルにまとめてくれる開発ツールがあれば良いんじゃない?」
webpackとwebpackerが何をするか
webpackができること
webpackはモジュールバンドラーである
下の画像は私が作ったものではなく、webpack公式HPの単なるスクショです。
色々な種類のファイルがコンパイルされて、jsやcssになっていそうな雰囲気を感じます。
実際、手元でbin/webpack
コマンドを実行して静的ファイルを生成してみたRubyist諸兄も、そう感じたのではないでしょうか。
ですが勘違いしてはいけません。webpackはコンパイラではありません。モジュールバンドラーです。
先程も書いた通り、webpackはあくまで、ESMのimport
やCJSのrequire
などをもとにしてファイルの依存性を解決して、IEでも読めるような1つの新しいJSファイルにしてくれるための存在であり、sassをcssにコンパイルしたり、TypeScriptをJavaScriptにコンパイルするための機能は持っていません。
画像をよく見てください。上の方に"bundle your scripts"と書いてあります。"compile your scripts"ではないのです。
(※伝えやすさのために「1つのJSファイル」と何度も言っていますが、例えばSplitChunksPluginを使用した時など、webpackの設定の仕方によっては複数のファイルになることもあります)
loader (バンドルする前にコンパイルとかできるやつ)
しかしwebpackerでビルドを実行したら、現実にTypeScriptがIEでも読めるJavaScriptになってくれたり、sassがいい感じのcssになってくれたりしています。ということは、webpackでコンパイルもできるということです。
実はwebpackは外部ライブラリを使用することで、**前処理としてコンパイルを行ってから、モジュールをバンドルすることができます。**そのライブラリのことはloader
と呼ばれます。
例えばTypeScriptであったらts-loader
、Sassだったらsass-loader
というように、npmのパッケージが存在します。
そのloaderをwebpackに設定してあげることで、webpackは「最初にts-loader
を使ってTypeScriptをJavaScriptに変換してから、モジュールを解決するのだな」と理解することができます。その結果として、もともとTypeScriptであった複数のファイルから、バンドルされた1つのJavaScriptのファイルを生成できるのです。
webpack公式HPに載ってるだけでもこんなにたくさんのloaderがありますし、頑張れば自作することもできます。
loaderは複数設定できます。例えば、こんな風に。
- sass-loaderでsassをコンパイルする
- postcss-loaderでPostCSSをコンパイルする
- css-loaderでJSの中に書かれたCSSを読み込む
- style-loaderで、JSの中にあるCSS文字列をDOMに挿入する
これを1から順に処理していくには、webpack.config.jsにちゃんと順番を守って記述する必要があります。
webpack.configのloaderの配列は、後ろのloaderから先に処理されるので、先に処理したいloaderは配列の末尾に入れましょう。
use: [
{ loader: 'style-loader' }, // (4番目)
{ loader: 'css-loader' }, // (3番目)
{ loader: 'postcss-loader' }, // (2番目)
{ loader: 'sass-loader' } // (1番目)
]
いきなりCSSや画像の話が出てきた!? (2020/11/03追記)
今までJSの話ばかりしていたのに、急にCSSの話が出てきたり、webpackの公式HPには画像の拡張子が書いてあったりするので、びっくりした方もいると思います。
全て理解する必要はないので、下のReact + TypeScriptのコードをなんとな〜く見てください。
import React from "react";
import style from "./Layout.module.css"; // ポイント! cssをimportしている
const Layout: React.FC = ({ children }) => {
return (
<div className={style.app}>
<div className={style.appContainer}>{children}</div>
</div>
);
};
export default Layout;
Railsのサーバー側でHTMLを生成するのみならず、今時はReactやVueを使うことによって、フロントエンドのJS側でHTMLを生成することもあります。その場合に、webpackはこのような.jsx
や.tsx
や.vue
の形式のコードも読める必要があります。見慣れない拡張子かもしれませんが、全て.js
の亜種みたいなものです。
このコードのポイントは、JS側(例はTSですが)に、CSSのファイルをimportしているところです。今日はJS側にスタイルや画像もimportされるようになったため、webpackはJSやTS以外にも依存関係を解決する対象があるということです。そうしないと、こういったプログラムをIEでも読めるように変換することができません。
しかし覚えておいて頂きたいのは、webpackはあくまで、JSを中心として依存関係を解決するということです。CSSのimportなどに対応しているのはオマケみたいなものだと思ってください。
plugin (バンドルされた後に、JSのファイルサイズ圧縮とかできるやつ)
loaderとは別に、pluginもあります。webpackにおけるpluginは、loaderとは区別されます。loaderは前処理を行うのに対して、pluginはバンドル後、つまり、1つのJSファイルになった後に処理を行うことができます。例えば生成されるJSのファイルサイズを小さくすることをminifyと呼ぶのですが、そのminifyをやってくれるプラグインとしてterser-webpack-plugin
が有名だと思います。
webpack公式HPに載ってるpluginの一覧はこちらです。
loaderやpluginはどう使えばよいか
webpackerではなく、生のwebpackを使う場合、まずは使いたいloaderやpluginを、yarn add
やnpm install
して自分のプロジェクトに追加します(JS版のbundle installみたいなものです)。その後で、webpack.config.js
などの、webpackの設定ファイルに、何をどう使うのかを設定してあげます。この記事は概要を伝えることを目的としているため、具体的にどう設定したらよいかまでは触れません。別の記事や、webpack公式ページを参照してください。
公式ページにも英語ですが結構色々導入方法が書いてあります。(例えばterser-webpack-plugin)
webpackができることまとめ
一旦まとめます。
- webpackは、
require
とかimport
とかを解決して、IEでも読めるようにファイルをまとめ上げることができる。 - loaderを使用することで、webpackの前処理でTypeScript → JavaScriptに変換したりもできる。
- pluginを使用することで、webpackの後処理でJSにminifyをかけたりすることもできる。
もちろんTSのコンパイルやJSのminifyだけではなく、loaderやpluginを追加することで色々なことができますが、ここでは分かりやすいように具体的に書きました。
Rubyのwebpackerができること
「webpack.config.jsとか書きたくない……」
「JSのこと全然わからんRailsプログラマにwebpack.config.jsの設定をやれと言われても困る。どうかよしなに設定して欲しい。そもそもwebpack.config.js自体が難しいじゃん……」と思ったことはありませんか? webpackerは、そんなありがちなお悩みを解決してくれそうな雰囲気を醸し出しているgemです(解決するとは言っていない)。
さて、webpackerは、そのREADMEにも書いてある通り、Railsのプロジェクトでwebpackを楽に使えるようにしたものです。webpackerのpackage.jsonを見てみます。package.jsonは、プロジェクトのnpmパッケージを管理する際にGemfileのような役割も果たしています。(他にも色々役割があります)
// 略
"dependencies": {
// ※比較的分かりやすいものだけ抽出します。
"babel-loader": "^8.1.0",
"core-js": "^3.6.5",
"css-loader": "^5.0.0",
"file-loader": "^6.1.1",
"mini-css-extract-plugin": "^1.0.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"postcss": "^8.1.3",
"postcss-loader": "^4.0.4",
"sass-loader": "^10.0.3",
"style-loader": "^2.0.0",
"terser-webpack-plugin": "^4.0.0",
"webpack": "^4.44.1",
// 略
}
見ると、何やらdependencies
の中に、既に各種のloader(sass-loader
など)やplugin(terser-webpack-plugin
など)がもう入っているではありませんか!
(ts-loader
は最初は入っていませんが、webpacker公式のドキュメントにある通り、bundle exec rails webpacker:install:typescript
のようにコマンドを打ったりすると入るようです。)
webpackerは、config/webpacker.yml
というwebpacker専用のymlの設定ファイルを読むことで、何をどう実行したらよいかを決定し、よしなにファイルを書き出してくれたりします。
webpackerの挙動はどう設定されるか
config/webpacker.yml (具体例付き)
例えば、config/webpacker.yml
には、extract_css
という項目があります。この項目をtrueに設定することで、CSSをJSとは別のファイルとして書き出すことができるようになっています。
production:
<<: *default
extract_css: true
extract_css
がtrueの時に何が起こるか、webpackerのコードを見てみましょう。
// ... 省略 ...
const styleLoader = {
loader: 'style-loader'
}
// ... 省略 ...
// useはオブジェクトの配列。unshiftは配列の先頭に要素を追加するメソッド。
if (config.extract_css) {
use.unshift(MiniCssExtractPlugin.loader)
} else {
use.unshift(styleLoader)
}
config.extract_css
がtrueだと、MiniCssExtractPlugin.loader
がloaderとして読まれていることが分かります。webpackは基本的にJSを中心として依存関係を解決するため、基本的にはCSS in JS
です。mini-css-extract-plugin
は、CSS in JS
なCSSを、CSS単品のファイルとして書き出すことができるようにするためのnpmのパッケージです。pluginという名前がついていますが、ここではloaderとして処理を先頭に追加しています。(MiniCssExtractPluginはpluginですがloaderとしても設定が必要なのです)
一方でfalseの場合は、{loader: 'style-loader'}
が先頭に追加されていることが分かります。style-loader
は、CSS in JS
を実際にDOMに描画する時に必要なloaderです。
大事なことなので何度でも書きますが、webpack.configのloaderの配列は、後ろのloaderから順番に処理されます。なので、配列の先頭に設定を追加するということは、loaderの最後にその処理を実行するということです。extract_css: true
にすると、loaderの最後の処理でmini-css-extract-plugin
が実行され、結果としてCSSが書き出される、ということがなんとなく分かったと思います。
mini-css-extract-plugin
はもしかしたら未来には別のパッケージになっているかもしれませんが、webpacker.ymlの設定は何をするか、webpackerがどうやってwebpackの設定を簡単にしているかがだいたい分かったかと思います。
config/webpack/environment.js
webpackerの設定をするにはもう一つの方法があります。webpacker側が提供するメソッドで、webpackerのデフォルトの設定との差分を記述する方法です。
こちらは、ymlでの設定と比較するとちゃんとwebpack.config.jsを意識して書く必要があります。environment.jsに書き込みを行うことで、例えばloaderの先頭や末尾に別のloaderを追加することができたりします。
webpackとwebpackerが何者なのか知っておいた方がいい理由
早く退社したいならば、あなたも知るべきだ
仕事でRails開発をやっている人であるならば100%知っておくべきです。特にリリース担当者は知らないと、とてもつらい思いをするリスクがあります。
Rails6からwebpackerが同梱されていることは、多分ご存知の通りだと思います。webpackerを使うことで、自分の書いたコードがどう加工されて、どんな成果物が本番環境にデプロイされているかを知らないと……例えば本番デプロイの途中、あるいは直後に何らかの原因でエラーが起きた時、そのトラブルシューティングを行ったり、原因の根本解消を行うことができません。
webpackerの問題点は、生のwebpackよりも問題の特定が難しくなることにあります。最終的にどんなwebpack.configになっているかが分かりにくいためです。
コードを趣味で書いている分には問題ないと思いますが、お客様がいる状態で「不具合の修正がデプロイできないよふぇぇ」となると、待っているのはブチギレのお客様です。
実例: ビルド成果物がでかすぎる
「本番デプロイのassets:precompileのビルド中になんか知らんけどコンパイルが失敗したわ!!!」
「なんじゃこのエラーは!」
Error: write EPIPE
at ChildProcess.target._send (internal/child_process.js:806:20)
at ChildProcess.target.send (internal/child_process.js:676:19)
at ChildProcessWorker.send (/XXX/hoge/releases/XXXXXXXXXXXXXX/node_modules/terser-webpack-plugin/node_modules/jest-worker/build/workers/ChildProcessWorker.js:299:17)
「JSのminifyをしてるwebpackerのTerserPlugin
のところでなんか失敗してるみたいやな」
「調べたらメモリ不足の時に起きがちなエラーらしいな」
「/var/log/messages
見たらOOM Killerにnodeのプロセスが殺されとる」
「なんでメモリ不足が起きるんや……ビルドのためだけにサーバースペックを上げるしかないんか……?」
「webpackがメモリに保持してるビルド成果物がでかいなぁ」
「ビルド成果物をもっと効率的に小さくするのはどうしたらええんや」
「おっ、webpackにSplitChunksPlugin
入れたらいい感じに小さくなるらしいやんけ! 一旦解決!」
実例: application.cssが存在せずエラー
「デプロイ直後にサイトにアクセスしたら謎のエラーが出て全画面が表示されへん!!!」
「なんじゃこのエラーは!」
ActionView::Template::Error: Webpacker can't find application.css
in /XXX/hoge/releases/20XXXXXXXXXXXX/public/packs/manifest.json
「ホンマにapplication.cssがあらへん……? なんでや?」
「どうもこのcssはextract_css: true
にした時にwebpackerのMiniCssExtractPlugin
が書き出すっちゅう実装になっとんな」
「cssがあらへんのに、slim側のstylesheet_pack_tag 'application'
でcssを呼ぼうとして死んどる」
「今までcssが書き出されてたのに、なんで突然出ぇへんようになったんや」
「あーっ、デッドコードになってて消したVueコンポーネント……。お前だったんか……いつもwebpackerでCSSが書き出されていたのは……」
「もともと大した仕事せぇへんかったしextract_css: false
にしてstylesheet_pack_tag 'application'
を消すか」
「一旦はこれで解決」
結局、webpackとwebpackerが何者なのか知っておいた方がいいの?
もしwebpackのことをある程度知っていたら、上記の実例のように、webpackのビルド時のトラブルシューティングに強くなり、デプロイができず残業戦士になることを多少回避できるようになります。デプロイ時にブラックボックスの恐怖に怯えることも少なくなります。
逆に、もしwebpackに関する知識が皆無でデバッグに取り組んでいたら、問題の原因の特定に何日もかかってしまうことでしょう。
webpackerは、webpackを簡単にしてくれそうな雰囲気ですが、webpackのことをよく分かっていないと、結局いざというときにつらい思いをする気がします。
webpackerはwebpack導入時の設定を簡略化してくれますが、それはwebpackへの理解を放棄していいということではないのです。本番環境でwebpackerを運用し続けた時、きっとあなたにもそれが分かると思います。
webpackerを剥がして生のwebpackを使うというのも、一つの選択だと思います。
おわりに
どうでしょうか、webpackが少し身近に感じられたら幸いです。
webpackのことがまだ全然わからんという場合は、私の力不足でごめんなさいという気持ちです。
JSの歴史はかなり雑に簡略化しています。別に私はMicrosoftの中の人でも、ESを策定している中の人でもないので、不正確なところがあるかもしれません。それから実際はwebpackよりも前に別のモジュールバンドラーが出てきたりしていますし、JSの文法はES2015以降全然変わって、関数スコープ以外にブロックスコープが出てきたりもしていますが、本題からずれそうなので省いてしまいました。
後はwebpackでビルドするのはIEのためだけではなく、かつてHTTP/1.1下ではページ表示の速度面でも優位性がありました。今はもうHTTP/2が広く普及しているので、速度面での優位性は分かりませんが。
簡単に説明するために色々はしょってしまった場所も多いのですが、私は「だいたい合ってる」んじゃないかと思っているので、致命的な間違いを犯していない限りご容赦ください。
みなさんも私と一緒にwebpackのエラーを眺めながらトラブルシューティングしましょう。エラーを起こさないに越したことはないですけどね!