◆ 複雑なことしないなら思ったより簡単

これまでは webpack は設定とか大変そうだし覚えること多いし と parcel を使ってました
ですが parcel は IE 用にバンドルしたら node_modules は babel 通してくれないとかで webpack を使うことになりました

webpack を使わない理由は 「設定が複雑だし関連するものも多くて覚える事が多そうで大変」 ということだったので 一度使ってしまうともうどっちでも良いという感じです
遅いと言われていた webpack も今の 4 だとそんなに遅くないですし
設定ファイルも基本はコピペで微調整くらいなので 状況によって使えたり使えなかったりする parcel より webpack で統一でも良い気もしてきました
他に rollup とか poi とか pax とか packem とかいろいろありますけど 機能揃ってなかったりライブラリ向けだったりで 万能で高機能の webpack で困っていない限り それらは別に使わなくてもいいかなって感じです

使い方

使ってみたら webpack も結構簡単でした
4 で簡単になったみたいな噂もありますし 昔の悪いイメージはそれほどあてにならないのかも知れません

4 だと webpack と webpack-cli の 2 つが必要です

yarn global add webpack webpack-cli

インストールしたら設定ファイルを用意して

webpack

を実行するだけです

parcel とは逆で 何も指定しないとビルドだけで --watch をつければ watch して変更があれば自動更新します

設定ファイル

--config で設定ファイルの指定もできますが デフォルトの webpack.config.js という名前で作ります
webpack コマンドはこのファイルのあるフォルダの内側で実行すると設定が参照されます
外側だと自動判定で処理されます
parcel みたいな zero configuration ですね

単純に JavaScript ファイルをバンドルするだけならこれでできます

const { join } = require("path")

module.exports = {
mode: "development",
entry: join(__dirname, "src/index.js"),
output: {
path: join(__dirname, "dist/"),
filename: "bundle.js",
},
}

mode は必須で development や production を設定します
開発用か本番用かで自動でいろいろ処理を変えてくれます

entry にエントリポイントを設定します
複数設定もできます

{
entry: {
app1: join(__dirname, "src/app1/index.js"),
app2: join(__dirname, "src/app2/index.js"),
app3: join(__dirname, "src/app3/index.js"),
}
}

複数の場合の出力名は filename に [name] を使ってエントリポイントに指定した名前を使えます

{
output: {
path: join(__dirname, "dist/"),
filename: "[name].js"
}
}

にすると app1.js や app2.js という名前にできます

最低限ならエントリポイントと出力とモードだけで十分です

src/index.js
import x from "./test.js"

x(100)

src/test.js
export default x => console.log(x)

の場合はこうなります

/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./src/index.js");
/******/ })
/************************************************************************/
/******/ ({

/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test.js */ \"./src/test.js\");\n\n\nObject(_test_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(100)\n\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ }),

/***/ "./src/test.js":
/*!*********************!*\
!*** ./src/test.js ***!
\*********************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (x => console.log(x));\n\n\n//# sourceURL=webpack:///./src/test.js?");

/***/ })

/******/ });

上の方の /*****/ から始まってるところが webpack 自体の処理部分です
以降のファイル名を書いている部分がバンドル対象のファイル部分です
ファイル名があるので場所はわかりやすいのですが eval されてて読みづらいです

ブラウザで開けば開発者ツールのソースタブで webpack:// にわかりやすく表示されますけど そのまま見たい場合は不便です
そういうときは設定ファイルに devtool: false を追加すれば見やすくなります

const { join } = require("path")

module.exports = {
mode: "development",
entry: join(__dirname, "src/index.js"),
devtool: false, // これを追加
output: {
path: join(__dirname, "dist/"),
filename: "bundle.js",
},
}

/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _test_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test.js */ "./src/test.js");


Object(_test_js__WEBPACK_IMPORTED_MODULE_0__["default"])(100)


/***/ }),

/***/ "./src/test.js":
/*!*********************!*\
!*** ./src/test.js ***!
\*********************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = (x => console.log(x));


/***/ })

eval なしになって インポート・エクスポートの変換以外はそのままです

babel

これまでの設定は単にバンドルするだけでした
他によく使うのが babel での変換です
と言っても TypeScript とか JSX とかは使わないし 基本 Chrome で動作する機能で十分なので Chrome 以外のブラウザで動かすようです

とりあえず IE11 用に ES5 変換をしてみます

babel を使うのでインストールが必要です
parcel と違って 必要なものは自分で入れることになります

yarn add -D @babel/core @babel/preset-env babel-loader

babel-loader が webpack で babel を使うためのツールで残りが babel 自身の機能を使うためのものです

webpack.config.js に module プロパティを追加します

const { join } = require("path")

module.exports = {
mode: "development",
entry: join(__dirname, "src/index.js"),
devtool: false,
output: {
path: join(__dirname, "dist/"),
filename: "bundle.js",
},
// ⇩ を追加
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
],
},
],
},
}

module.rules の配列に変換設定を書いたオブジェクトを追加します
test には対象にするファイルを test する正規表現を書きます
正規表現オブジェクトの test メソッドを使うからと考えると覚えやすいです

use にはローダーのリストを配列で書きます
ローダーはバンドル前に何かの変換を行うものです
なので特に変換が必要なかった バンドルだけのときには使ってませんでした
今回は JavaScript ファイルを babel に通して変換するので .js にマッチするファイルに babel-loader を通します

最初困ったのですが ローダーを複数書くと 後ろから実行されます
一番最後のローダーの結果を最後から 2 番目のローダーに渡して……最初のローダーの結果が最終的なバンドルに含まれます

ローダーの設定では loader にローダー名を書いて options にローダーのオプションを書きます
オプションがないなら use の配列の要素にローダー名を書いても動きます

今回の例だと babel の preset-env を使うので options で指定してます
targets に browserslist を書けますが parcel で package.json に書く方に慣れているので package.json に browserslist を書きます

{
"dependencies": {},
"devDependencies": {
"@babel/core": "^7.4.5",
"@babel/preset-env": "^7.4.5",
"babel-loader": "^8.0.6"
},
"browserslist": [
"IE 11"
]
}

これで webpack でビルドします

/***/ "./src/test.js":
/*!*********************!*\
!*** ./src/test.js ***!
\*********************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = (function (x) {
return console.log(x);
});

/***/ })

アロー関数が普通の関数式になってますね
ES5 にコンパイルされています

IE で開くと console に 100 と表示できています


babel は構文変換だけで Promise だったり includes メソッドだったりの polyfill は含みません
必要なら @babel/polyfill をインストールして import します
他の polyfill と競合してエラーが出ることがあるので外部ライブラリもロードするなら import ではなく bundle 済みの polyfill を HTML の最初でロードしておくのが良いかもです

postcss

JavaScript の次は CSS を変換してみます
ちょっと前は cssnext を使うのが良さそうだったのですが いつのまにか deprecated になってました

postcss-preset-env を使えばいいそうなので これを入れてみます
css は Chrome でも未実装で使いたい機能がいろいろあるので 対象は Chrome にします

yarn add -D style-loader css-loader postcss-loader postcss-preset-env

const { join } = require("path")

module.exports = {
mode: "development",
entry: join(__dirname, "src/index.js"),
devtool: false,
output: {
path: join(__dirname, "dist/"),
filename: "bundle.js",
},
// ⇩ を追加
module: {
rules: [
{
test: /\.css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: { importLoaders: 1 }
},
{
loader: "postcss-loader",
options: {
ident: "postcss",
plugins: [
require("postcss-preset-env")({ stage: 0 })
]
},
},
],
},
],
},
}

Babel に比べるとちょっと複雑です
CSS を使うときは style-loader と css-loader の組み合わせが基本です
css-loader が CSS をロードして style-loader が JavaScript の処理で head タグ内に style タグを追加します
動的に style タグを追加するので HTML は JavaScript ファイルひとつのロードだけで済みます
css-loader だけだと style タグは追加されず import した値に CSS の文字列やその他の情報が入っています
自分でこれをもとに style タグを追加することもできなくはないです

post-css を使う場合は css-loader より前に処理する必要があるので use の最後に指定しています
post-css の各機能はプラグインなので plugins に postcss-preset-env を指定しています
require して得られる関数の引数にオプションを渡した返り値を plugins の配列に設定します
とりあえず全機能ということで stage: 0 を指定しました

require("postcss-preset-env")({ features: { "nesting-rules": true } })

のように機能 (features) ごとに ON にもできます

package.json はこうなります

{
"dependencies": {},
"devDependencies": {
"css-loader": "^3.0.0",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.6.0",
"style-loader": "^0.23.1"
},
"browserslist": [
"last 2 Chrome versions"
]
}

ビルド対象のコードはシンプルにセレクタのネスト機能だけ使って JavaScript は HTML を出力だけ行います

src/index.js
import "./index.css"

document.body.innerHTML = `
<div>
div
<p>p in div</p>
</div>
<section>
<div>div in section</div>
</section>
`

src/index.css
div {
color: red;
& p {
color: blue;
}
@nest section & {
color: green;
}
}

結果は

div (⇦赤)
p in div (⇦青))
div in section (⇦緑)

となっていてちゃんと反映できています

出力された style タグはこうなってました

<style type="text/css">div {
color: red
}
div p {
color: blue;
}
section div {
color: green
}
</style>

CSS を別ファイルにする

style タグを動的追加だと HTML 中に CSS の設定が存在して 開発中にあまり見やすくないです
CSS を別ファイルに分けるには MiniCssExtractPlugin を使います

yarn add -D mini-css-extract-plugin webpack

このプラグインは webpack パッケージを使うようで プロジェクトのローカルに webpack がインストールされていないと 「webpack がないのでインストールしてください」 というエラーが表示されます
プロジェクトごとに webpack をインストールしたくないので基本は global にインストールしてコマンドを使っていますが global のものは認識されないようです
global の方に mini-css-extract-plugin を入れると mini-css-extract-plugin が見つからずエラーでした
なのでこのプラグインを使うには webpack をプロジェクトローカルにインストール必要です

インストールしたら webpack.config.js を編集します

const { join } = require("path")
const MiniCssExtractPlugin = require("mini-css-extract-plugin") // 追加

module.exports = {
mode: "development",
entry: join(__dirname, "src/index.js"),
devtool: false,
output: {
path: join(__dirname, "dist/"),
filename: "bundle.js",
},
module: {
rules: [
{
test: /\.css$/,
use: [
// ⇩ style-loader を置き換える
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: join(__dirname, "dist/"),
},
},
{
loader: "css-loader",
options: { importLoaders: 1 },
},
{
loader: "postcss-loader",
options: {
ident: "postcss",
plugins: [require("postcss-preset-env")({ stage: 0 })],
},
},
],
},
],
},
// ⇩ 追加
plugins: [
new MiniCssExtractPlugin({
filename: "bundle.css",
}),
],
}

webpack の plugin に MiniCssExtractPlugin のインスタンスを追加して style-loader の代わりに MiniCssExtractPlugin.loader を設定します
これで webpack を実行すると dist フォルダに bundle.css もできています

div {
color: red
}
div p {
color: blue;
}
section div {
color: green
}

これは自動ではロードされません
bundle.js の中を見てもインポートされるのは何もしないモジュールです

/***/ "./src/index.css":
/*!***********************!*\
!*** ./src/index.css ***!
\***********************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

// extracted by mini-css-extract-plugin

/***/ }),

HTML に自分でロードするタグを書く必要があります

<link rel="stylesheet" href="./bundle.css" />

その他機能

webpack は多分規模の一番大きいバンドラーですからまだまだできることがいっぱいあります
dev-server や HMR は人によっては欲しいかもですが私は別になくても困ってないので今のところは使ってません
サーバは静的ファイルを作るだけが目的じゃなければ post したりするサーバがあるので そこの静的ファイル置き場に出力して同じサーバでアクセスできるようにしています
別だとクロスオリジンになって面倒ですから
HMR は parcel では自動で入ってましたが使えないことが多くてオフにするくらいでした
customElements.define みたいな上書きできないものがあると 置き換えても 2 回目以降はエラーになるのでリロードしないとダメです

webpack を使いはじめて 1 日 2 日程度の知識で書いているので 昔から何年も使ってる人からすると足りてない部分も多いかも知れませんが 最低限これだけあれば十分なものにはなってると思います
バイナリファイルを含めるとか コードスプリットとかは要らないことのほうが多いですからね