webpack の基本的な使い方

webpack は JavaScript などの依存関係のあるリソース(モジュール)を適切に1つのファイルにまとめてくれるツール(モジュールバンドラー)です。JavaScript ファイル以外にも cssファイルや画像ファイルなどもまとめることができます。

バンドル(bundle)は「束ねる」や「まとめる」というような意味があり、モジュールバンドラーはモジュール(JavaScript ファイル)をまとめてくれるものと言うような意味になります。

以下は webpack のインストール方法や webpack.config.js の設定方法、Loaders ローダー、Plugins プラグイン、splitChunks などの基本的な使い方の覚書です。

Node.js がインストールされていることを前提にしています。

関連ページ:React の環境構築(セットアップ)

作成日:2020年6月8日

webpack のインストール

webpack は Node.js のパッケージマネージャ npm を使ってインストールすることができます。

通常はプロジェクトごとに管理できるようにそれぞれのディレクトリでローカルインストールを行います。

コンテンツ(プロジェクト)のファイルが保存されるフォルダーを任意の場所に作成しそこに移動します。

$ mkdir myProject  return //フォルダーを任意の場所に作成
        
$ cd myProject  return //作成したフォルダー(ディレクトリ)に移動

npm を使ってインストールする場合、まず npm init コマンドで package.json を生成します。デフォルトのオプションで package.json を生成すれば良いので -y オプションを指定します。

$ npm init -y return //package.json をデフォルトで生成

//生成された package.json のパスと内容が表示される
Wrote to /Applications/MAMP/htdocs/sample/myProject/package.json:

{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

続いて npm install コマンドで webpack をインストールします。

コマンドライン操作用のパッケージは webpack-cli という別パッケージで提供されているので併せてインストールします(webpack 4.0以降)。

オプションの -D(--save-dev)は開発環境で使うパッケージに指定するオプションです。バージョンを指定しないでインストールすると最新版がインストールされます。

$ npm install -D webpack webpack-cli  return //webpack と webpack-cli をインストール
        
・・・中略・・・

+ webpack-cli@3.3.11  //バージョン 3.3.11 の webpack-cli がインストールされた
+ webpack@4.43.0 //バージョン 4.43.0 の webpack がインストールされた
added 414 packages from 228 contributors and audited 414 packages in 16.199s
//414個のパッケージがインストールされた

インストールされたパッケージは npm ls コマンドで確認できます。

オプションの -depth を指定しない場合は、インストールされた全ての依存ファイルも表示されます。

$ npm ls -depth=0  return //一番上の階層のみを表示
        
myProject@1.0.0 /Applications/MAMP/htdocs/sample/myProject
├── webpack@4.43.0   
└── webpack-cli@3.3.11 

$ npm ls -depth=1  return //第2階層まで表示(主な依存パッケージが表示される)

myProject@1.0.0 /Applications/MAMP/htdocs/sample/myProject
├─┬ webpack@4.43.0
│ ├── @webassemblyjs/ast@1.9.0
│ ├── @webassemblyjs/helper-module-context@1.9.0
│ ├── @webassemblyjs/wasm-edit@1.9.0
│ ├── @webassemblyjs/wasm-parser@1.9.0
│ ├── acorn@6.4.1
│ ├── ajv@6.12.2
・・・中略・・・
│ ├── terser-webpack-plugin@1.4.3
│ ├── watchpack@1.7.2
│ └── webpack-sources@1.4.3
└─┬ webpack-cli@3.3.11
  ├── chalk@2.4.2
  ├── cross-spawn@6.0.5
・・・中略・・・
  ├── v8-compile-cache@2.0.3
  └── yargs@13.2.4

インストールが完了すると、インストールされたパッケージは node_modules というフォルダに保存され、package.json 及び package-lock.json という npm の設定ファイルが生成されます。

$ tree -L 1  return //ツリー表示
.
├── node_modules   //npm でインストールされるパッケージのディレクトリ
├── package-lock.json  //npm の設定ファイル
└── package.json   //npm の設定ファイル

これで webpack のインストールは完了です。

webpack guides: installation

関連ページ:npm の基本的な使い方

JavaScript ファイルをバンドル

以下は webpack を使って複数の JavaScript を1つにバンドルする(まとめる)例です。

以下の例では設定ファイル webpack.config.js を使わずデフォルトで実行するので、エントリポイントの名前は index.js にして src フォルダに配置します。

※ webpack は src ディレクトリにエントリポイントの index.js ファイルさえあれば設定ファイルなしで処理(ビルド)を実行することができます(全てデフォルトが適用されて実行されます)。

また、デフォルトの出力先のフォルダ dist も予め用意しておき、この例では表示用の index.html を配置します。dist フォルダには1つにまとめられた JavaScript ファイルが出力されます。

プロジェクトのフォルダの中に dist 及び src という名前のフォルダを作成します。

更に src フォルダの中にモジュールを格納するフォルダ modules を作成します。

myProject
├── dist  //追加したフォルダ
├── node_modules
├── package-lock.json
├── package.json
└── src  //追加したフォルダ
    └── modules  //追加したフォルダ

src フォルダの中にエントリポイントとなる以下のような index.js というファイルを作成します。

ES6 のモジュール(ES Modules)の記述方法で src/modules/foo.js をインポートしています。モジュールのパス ./modules/foo.js の拡張子 .js は、バンドル後は省略可能です。

※ この例では ES Modules の記述方法を使っていますが、webpack は require や exports、module.exports を使う Node.js(CommonJS)の記述方法でも(どちらでも)問題なく動作します。

webpack guides: Module Methods

src/index.js(エントリポイント)
// import 文を使って foo.js の関数 greet をインポート
import { greet } from './modules/foo.js';

function component() {
  //div 要素を生成
  const element = document.createElement('div');

  // インポートした greet の実行結果を使って div 要素の HTML を作成
  element.innerHTML = '<p>' + greet() + '</p>';

  return element;
}

document.body.appendChild(component());

modules フォルダの中に以下のような ES Modules の記述方法で関数をエクスポートするモジュールのファイル foo.js を作成します。

src/modules/foo.js(モジュール)
// export 文を使って関数 greet を定義
export function greet() {
  return 'Hello webpack!';
}

dist フォルダには以下のような index.html を作成します。この時点では type="module" で src/index.js を読み込むようにしていますが、JavaScript をバンドル後は type="module" を削除してバンドルされた JavaScript ファイル(main.js)を読み込むように変更します。

dist/index.html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>My First webpack</title>
  </head>
  <body>
    <script src="../src/index.js" type="module"></script>
    <!--バンドル後は  <script src="main.js"></script> に変更-->
  </body>
</html>

この段階での構成は以下のようになっています。

myProject
├── dist
│   └── index.html  //追加したファイル 
├── node_modules  //npm でインストールされるパッケージ(webpack 等)
├── package-lock.json
├── package.json
└── src
    ├── index.js  //追加したファイル(エントリポイント) 
    └── modules
        └── foo.js  //追加したファイル(モジュール)

npm のパッケージに関する設定情報が記述された package.json を編集して、"main": "index.js" の行を削除し、"private": true を追加します(念の為プロジェクトを誤って公開しないようにするため)。

(※注意) 以下はコメントを入れてありますが、実際には JSON ファイルにはコメントを付けることはできません。コメントを入れたまま、ビルド(処理を実行)するとエラーになります。

package.json
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  //"main": "index.js", //削除します (※注意)
  "private": true,  //追加(※注意)
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11"
  }
}

以下のビルドコマンドを実行して JavaScript ファイルをバンドルします。npx は npm のパッケージ(バイナリ)を実行するコマンドです。

webpack 4からは mode オプションを指定することが推奨されていますが、以下は省略しているのでデフォルトの production が適用されている旨の WARNING が表示されています。

出力(6行目)で表示されている Chunk とは、webpack でバンドルされて(まとめられて)出力されるファイルを意味します(chunk は「かたまり」というような意味)。

$ npx webpack  return //webpack を実行
Hash: 222b17e93daff83b4c00
Version: webpack 4.43.0
Time: 172ms
Built at: 2020/05/28 19:44:32
  Asset      Size  Chunks             Chunk Names
main.js  1.05 KiB       0  [emitted]  main
Entrypoint main = main.js
[0] ./src/index.js + 1 modules 510 bytes {0} [built]
    | ./src/index.js 408 bytes [built]
    | ./src/modules/foo.js 102 bytes [built]

WARNING in configuration  //mode オプションを指定していないための警告
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

以下は mode オプションに development を指定して実行する例です。

$ npx webpack --mode=development  return //mode オプションを指定して webpack を実行
Hash: af0ee004e8201ec5945a
Version: webpack 4.43.0
Time: 50ms
Built at: 2020/05/29 15:50:34
  Asset      Size  Chunks             Chunk Names
main.js  4.99 KiB    main  [emitted]  main
Entrypoint main = main.js
[./src/index.js] 408 bytes {main} [built]
[./src/modules/foo.js] 102 bytes {main} [built]

index.js でインポートしている foo.js が統合され、dist フォルダーの中に main.js として出力されます。

myProject
├── dist
│   ├── index.html
│   └── main.js //webpack によりまとめられた JavaScript ファイル
├── node_modules  
├── package-lock.json
├── package.json
└── src
    ├── index.js   
    └── modules
        └── foo.js  

webpack で出力した dist フォルダー内の main.js を index.html で読みこむことで、バンドルされたコードが実行されます。

index.html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>My First webpack</title>
  </head>
  <body>
    <script src="main.js"></script><!--  main.js を読み込む -->
  </body>
</html>

ブラウザで index.html にアクセスすると、この例の場合は、<p>Hello webpack!</p> が body 要素に追加されて「Hello webpack!」と表示されます。

バンドルされた(まとめられた)JavaScript ファイル main.js は以下のようになっています。

以下は mode オプションを指定せずに実行した例で、デフォルトの production が適用されてコードが最適化及び圧縮(optimization.minimize)されています。

main.js
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof    ・・・中略・・・   return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t),document.body.appendChild(function(){const e=document.createElement("div");return e.innerHTML="<p>Hello webpack!</p>",e}())}]);

以下は mode オプションに development を指定して実行した場合の main.js の例です。

ファイルの先頭に webpack による変数や関数が定義され、その後に元のソースコード(src/index.js と src/modules/foo.js)が記述されています。

元のソースコード部分は devtool オプションの指定によって出力が変わります。

main.js
/******/ (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;
/******/  }

・・・中略・・・

/******/  // 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 _modules_foo_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./modules/foo.js */ \"./src/modules/foo.js\");\n// import 文を使って foo.js の関数 greet をインポート\n\n\nfunction component() {\n  //div 要素を生成\n  const element = document.createElement('div');\n\n  // インポートした greet の実行結果を使って div 要素の HTML を作成\n  element.innerHTML = '<p>' + Object(_modules_foo_js__WEBPACK_IMPORTED_MODULE_0__[\"greet\"])() + '</p>';\n\n  return element;\n}\n\ndocument.body.appendChild(component());\n\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ }),

/***/ "./src/modules/foo.js":
/*!****************************!*\
  !*** ./src/modules/foo.js ***!
  \****************************/
/*! exports provided: greet */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"greet\", function() { return greet; });\n// export 文を使って関数 greet を定義\nfunction greet() {\n  return 'Hello webpack!';\n}\n\n//# sourceURL=webpack:///./src/modules/foo.js?");

/***/ })
/******/ });

package.json の scripts フィールド

npx コマンドで webpack を実行する以外に、package.json の scripts フィールドにコマンド(npm script)を追加しておくと npm run コマンドで webpack を実行することができます。

package.json を編集して "build": "webpack" を scripts フィールドに追加します(8行目)。これで npm run build とコマンドラインで入力すると webpack が呼び出されてビルドが実行されます。

package.json
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11"
  }
}

npm run build とコマンドラインで入力することで、scripts フィールドの "build": に指定したコマンド webpack が呼び出されてビルドが実行されます。

$ npm run build  return //npx webpack と同じ

> myProject@1.0.0 build /Applications/MAMP/htdocs/sample/myProject
> webpack

Hash: 222b17e93daff83b4c00
Version: webpack 4.43.0
Time: 54ms
Built at: 2020/05/29 20:13:58
  Asset      Size  Chunks             Chunk Names
main.js  1.05 KiB       0  [emitted]  main
Entrypoint main = main.js
[0] ./src/index.js + 1 modules 510 bytes {0} [built]
    | ./src/index.js 408 bytes [built]
    | ./src/modules/foo.js 102 bytes [built]
    
WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/  

上記の場合、モードを指定していないので WARNING が表示されています。

モードは設定ファイル webpack.config.js でも指定することができますが、以下のように scripts フィールドにモードを指定したビルドコマンドを追加しておくこともできます。

以下のように設定すると、npm run build を実行すると webpack --mode production が呼び出され、npm run dev を実行すると webpack --mode development が呼び出されます。

package.json
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production",
    "dev": "webpack --mode development"
  },
  "devDependencies": {
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11"
  }
}

以下は実行例です。

$ npm run dev  return //npx webpack --mode development と同じ

> myProject@1.0.0 dev /Applications/MAMP/htdocs/sample/myProject
> webpack --mode development

・・・以下省略・・・

$ npm run build  return //npx webpack --mode production と同じ

> myProject@1.0.0 pro /Applications/MAMP/htdocs/sample/myProject
> webpack --mode production

・・・以下省略・・・

Node の環境変数を渡す(参考程度)

Node.js の環境変数は process.env というオブジェクトに格納され、よく使われる環境変数に process.env.NODE_ENV があります。

$ node -e "console.log(process.env)"  return  //設定されている環境変数が表示される
//node コマンドの -e(--eval)は引数を JavaScript として評価するオプションです

{
  TERM_PROGRAM: 'Apple_Terminal',
  SHELL: '/bin/bash',
  TERM: 'xterm-256color',
  TMPDIR: '/var/folders/c2/kytqb8ls7p941834kqccm05h0000gn/T/',
  Apple_PubSub_Socket_Render: '/private/tmp/com.apple.launchd.wmtH7tp5PO/Render',
  TERM_PROGRAM_VERSION: '421.2',
  OLDPWD: '/Applications/MAMP/htdocs/myProject2',
  TERM_SESSION_ID: '6CAA7C8A-2410-48D1-99DC-0757B043C615',
  USER: 'foo',
  SSH_AUTH_SOCK: '/private/tmp/com.apple.launchd.QueyXSMKYY/Listeners',
  ・・・以下省略・・・

}

$ node -e "console.log(process.env.NODE_ENV)"  return
undefined  //process.env.NODE_ENV は定義されていない(設定していないので)

必要であれば、webpack を実行する際に、NODE_ENV にパラメータを渡すことができます。

//webpack を実行時に NODE_ENV に development を渡す場合
$ NODE_ENV=development npx webpack

//webpack を実行時に NODE_ENV に production を渡す場合
$ NODE_ENV=production npx webpack

scripts フィールドに NODE_ENV を指定したビルドコマンドを追加しておくこともできます。

package.json
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "NODE_ENV=production webpack --mode production",
    "dev": "NODE_ENV=development webpack --mode development"
  },
  "devDependencies": {
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11"
  }
}
$ npm run build  return 

> myProject@1.0.0 build /Applications/MAMP/htdocs/sample/myProject
> NODE_ENV=production webpack --mode production
・・・以下省略・・・

$ npm run dev  return 

> myProject@1.0.0 dev /Applications/MAMP/htdocs/sample/myProject
> NODE_ENV=development webpack --mode development
・・・以下省略・・・

上記のように webpack を実行する際に、NODE_ENV にパラメータ(production や development)を渡すと、webpack.config.js やエントリポイントのファイルで process.env.NODE_ENV で値を取得することができます。

webpack.config.js や index.js で設定した環境変数を取得
if (process.env.NODE_ENV === 'production') {
  console.log('production');
}else{
  console.log('devlopement'); 
}

webpack.config.js

webpack はエントリポイントの src/index.js ファイルがあれば設定ファイルを使わなくても処理を実行できますが、webpack.config.js という名前の JavaScript ファイル(設定ファイル)を用意することで webpack の処理を詳細にカスタマイズすることができます。

例えば、モードを指定しておけば実行する都度オプションを指定せずに済みますし、エントリポイントのファイルや出力先のファイル・ディレクトリも自由に設定することができます。

よく使う設定項目としては、開発モードや本番モードを指定する mode、エントリーポイントを指定する entry、出力先を指定する output があります。

webpack.config.js はプロジェクトのルートに配置します。

myProject
├── dist
│   ├── index.html
│   └── main.js
├── node_modules  
├── package-lock.json
├── package.json
├── src
│   ├── index.js
│   └── modules
│       └── foo.js
└── webpack.config.js //追加 

webpack.config.js は JavaScript ファイルで、module.exports オブジェクトにプロパティを定義するようになっています。

以下はエントリポイント(entry)と出力先(output)、及びモード(mode)とを指定する例です。

webpack.config.js
const path = require('path');  //path モジュールの読み込み

module.exports = {
  entry: './src/index.js',  //エントリポイント(デフォルトと同じ設定)
  output: {  //出力先(デフォルトと同じ設定)
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  mode: 'development',  //モード
};

上記の例の場合、エントリポイントと出力先はデフォルトと同じ設定を指定していますが、それぞれ独自のファイルやディレクトリを指定することができます。

複数の設定ファイル

webpack コマンドはデフォルトでは webpack.config.js があるとその設定を使用しますが、--config オプションを指定して別の設定ファイルを利用することもできます。

//webpack.config.js 以外の設定ファイルを使用する場合
npx webpack --config 設定ファイル名

または、package.json の scripts フィールドに npm script を追加する方法もあります。

以下は npm run build コマンドで prod.config.js という名前の設定ファイルを使用する例です。

package.json
"scripts": {
  "build": "webpack --config prod.config.js"
}

webpack guides: Configuration

その他の設定タイプ

設定を単一のオブジェクトとしてエクスポート(module.exports オブジェクトにプロパティを定義)する以外にも、関数としてエクスポートしたり、Promise を使うこともできます。

webpack guides: Configuration Types

entry

エントリポイントは各モジュールを読み込んで処理を行う入力ファイルのことです。webpack はエントリポイントに指定されたファイルを元にモジュール間の依存関係を解析します。

デフォルトの entry プロパティの値は ./src/index.js ですが、entry プロパティで自由に指定することができます。

値は設定ファイルからの相対パスで指定します。

以下は ./path/to/my/entry ディレクトリにあるファイル file.js をエントリポイントに指定する例です。

webpack.config.js
//エントリポイントを設定(設定ファイル ./ からの相対パス)
module.exports = {
  entry: './path/to/my/entry/file.js' 
};

上記はデフォルトの main という名前(entryChunkName)の1つのエントリポイントを指定する場合のショートハンドです。以下のオブジェクト形式で指定したのと同じ意味になります。

webpack.config.js
module.exports = {
  entry: {
    main: './path/to/my/entry/file.js'
  }
};

複数のエントリポイントを指定することもできます。複数指定する場合はオブジェクト形式で指定します。

以下は2つのエントリポイント(./src/app.js と ./src/search.js)をそれぞれ app と searchApp という名前(entryChunkName)で設定する例です。

webpack.config.js
module.exports = {
  entry: {
    app: './src/app.js',
    searchApp: './src/search.js'
  }
};

webpack guides: Entry Points

output

output プロパティはバンドルしたファイルの出力先の設定です。

output.filename プロパティと output.path プロパティを使用して、バンドルしたファイルの名前と出力先のパスを指定します。

デフォルトではメインの出力ファイル(output.filename)は ./dist/main.js で、その他の生成ファイルの出力先のパス(output.path)は ./dist になります。

出力先(ディレクトリ)のパスは絶対パスで指定します。

また、以下では OS によってパスが異なることを防ぐため冒頭で Node.js のビルトインモジュールの path モジュールを require() を使ってインポートしてそのメソッド path.resolve を使用しています。

webpack.config.js
const path = require('path');  //path モジュールの読み込み

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {  // output プロパティ
    path: path.resolve(__dirname, 'dist'), //出力ファイルのディレクトリの絶対パス
    filename: 'my-first-webpack.bundle.js' //出力ファイルの名前
  }
};

__dirname はこのファイルが格納されているディレクトリ(webpack.config.js が存在するディレクトリ)の絶対パスを表す Node.js で予め用意されている変数です。

path.resolve(__dirname, 'dist') は webpack.config.js が /Applications/MAMP/htdocs/myProject/webpack.config.js にある場合、 /Applications/MAMP/htdocs/myProject/dist になります。

以下でも確認できます。node コマンドの -e または --eval は引数を JavaScript として評価するオプションです(Command Line Options)。

//webpack.config.js があるディレクトリ(myProject)で実行
$ pwd  return 
/Applications/MAMP/htdocs/myProject

$ node -e "console.log(__dirname)"  return 
.

$ node -e "console.log(path.resolve('/foo', 'bar'))"  return 
/foo/bar

$ node -e "console.log(path.resolve(__dirname))"  return 
/Applications/MAMP/htdocs/myProject

$ node -e "console.log(path.resolve(__dirname, 'dist'))"  return 
/Applications/MAMP/htdocs/myProject/dist

複数のエントリポイントがある場合

複数のエントリポイントがある場合でも、output.filename で指定する設定は1つなので、置換(substitutions)を使用して、各ファイルに一意の名前を付ける必要があります。

例えば以下のように [name] などの置換のテンプレート文字列(Template strings )を使って記述します。

[name] を使うとエントリポイントのファイル名が [name] に入ったファイル名のファイルが出力されます。

以下の場合、app.js、search.js というファイルが dist フォルダに出力されます。

webpack.config.js
const path = require('path');

module.exports = {
  entry: {
    app: './src/app.js',
    search: './src/search.js'
  },
  output: {
    filename: '[name].js',  //ファイル名.js
    path: path.resolve(__dirname, 'dist'),
  }
};

以下のように [name].bundle.js と記述すると、それぞれ app.bundle.js、search.bundle.js というファイルが dist フォルダ出力されます。

webpack.config.js
const path = require('path');

module.exports = {
  entry: {
    app: './src/app.js',
    search: './src/search.js'
  },
  output: {
    filename: '[name].bundle.js',  //ファイル名.bundle.js
    path: path.resolve(__dirname, 'dist'),
  }
};

以下のように [name].[hash] を使うと、[hash] の部分にはビルドごとに生成された一意のハッシュ(Hash)が入り、app.149cec35d7dc8920c912.bundle.js のようなファイル名のファイルが dist フォルダ出力されます。

const path = require('path');

module.exports = {
  entry: {
    app: './src/app.js',
    search: './src/search.js'
  },
  output: {
    filename: '[name].[hash].bundle.js',  //ファイル名.ハッシュ.bundle.js
    path: path.resolve(__dirname, 'dist'),
  }
};

webpack guides: Output

mode

mode プロパティのパラメーターを development、production、または none に設定することにより各環境に対応する最適化を有効にすることができます。デフォルトは production です。

production(デフォルト)を指定すると optimization.minimize という設定が適用されて、最適化及び圧縮(minify)されたファイルが出力されます。

最適化及び圧縮の設定は optimization プロパティでも設定することができます。

  • development:開発時向けのオプション(ソースコードが読みやすい状態で出力)
  • production:本番環境(公開時)向けのオプション(ソースコードを圧縮及び最適化)
webpack.config.js
module.exports = {
  entry: './path/to/my/entry/file.js',
  ・・・中略・・・
  mode: 'development'  //mode プロパティ
};

但し、mode を webpack.config.js で指定するとモードを変更する都度に記述を変更する必要があるため、npm scripts を使って webpack をそれぞれのモードで実行する方が便利かも知れません。

以下のように package.json を設定すると、npm run build で webpack --mode production を、npm run dev で webpack --mode development を実行できます。

package.json scripts フィールド部分の抜粋
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "build": "webpack --mode production",
  "dev": "webpack --mode development"
}

webpack guides: Mode

watch

watch オプションを利用すると処理対象のファイルを監視してファイルが変更されると自動的にビルドを再実行(リビルド)することができます。

watch オプションを指定して webpack コマンドを実行すると watch モードになり、ビルド処理を実行した後に待機状態になり、ファイルが変更されるたびにリビルドを実行します。

また、watch モードではキャッシュが有効になり、差分ビルド(リビルド)が行われるのでビルド時間が短くなります。

watch オプションを有効にするには webpack コマンドを実行する際に --watch オプションを指定するか、webpack.config.js に watch: true を追加します。

以下はコマンドラインの引数に --watch を指定して実行する例です。

npx webpack --watch

watch モードを終了するには、control + c でプロセスを終了します。

package.json の scripts フィールドにコマンド(npm scripts)を追加する方法もあります。

以下は watch に --mode と --watch オプションを指定したコマンドを定義する例です(5行目)。

package.json scripts フィールド部分の抜粋
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "build": "webpack --mode production",
  "dev": "webpack --mode development",
  "watch": "webpack --mode development --watch"
},

上記の場合、 npm run watch コマンドを実行します。

$ npm run watch  return //watch オプションのコマンドを実行
// npx webpack --mode=development --watch と同じこと

> myProject@1.0.0 watch /Applications/MAMP/htdocs/sample/myProject
> webpack --mode development --watch

webpack is watching the files…  //webpack はファイルを監視

Hash: 9c895dc798408458a434
Version: webpack 4.43.0
Time: 42ms
Built at: 2020/06/01 19:42:32

・・・中略・・・

Hash: 12e8f150603280206a42  //ファイルに変更があったので再ビルドされる
Version: webpack 4.43.0
Time: 9ms
Built at: 2020/06/01 19:43:10

または、以下のよう webpack.config.js で watch オプションを有効にすることもできます。

webpack.config.js
module.exports = {
  ・・・中略・・・
  watch: true  //watch オプションを有効にする
};
watchOptions

多くのファイルを監視すると、CPU やメモリの使用量が多くなる可能性があります。

watchOptions の ignored プロパティを使うと node_modules などの特定のディレクトリやファイルを監視対象から除外することができます。

webpack.config.js の watchOptions.ignored に文字列または正規表現で指定します。

webpack.config.js
module.exports = {
  //...
  watchOptions: {
    ignored: /node_modules/  //正規表現で指定(node_modules を除外)
  }
};

文字列にはワイルドカードが使え、複数指定する場合は配列で指定することができます。

webpack.config.js
module.exports = {
  //...
  watchOptions: {
    ignored: ['files/**/*.js', 'node_modules/**']   //配列とワイルドカードで指定
  }
};

webpack guides: Watch and WatchOptions

devtool ソースマップ

devtool オプションを使うと出力されるソースマップを設定することができます。デフォルトではソースマップファイルは出力されません。

devtool オプションにソースマップのタイプを指定してビルド(webpack を実行)すると、出力ファイルと同じディレクトリに、出力ファイルと同じ名前で拡張子が「.js.map」のソースマップファイルが作成されます。

出力するソースマップのタイプによりビルドにかかる時間やファイルのサイズが変わってきます。(devtool

以下は source-map タイプ(サイズが大きく、ビルドにも時間がかかる)のソースマップを出力する例です。ビルドすると dist フォルダに main.js.map というソースマップのファイルが出力されます。

webpack.config.js
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  mode: 'development',
  devtool: 'source-map' //source-map タイプのソースマップを出力
};

devtool オプションに 'source-map' を指定して webpack を実行する例。

$ npm run dev return //development モードでビルド
// または npx webpack --mode=development

> myProject@1.0.0 dev /Applications/MAMP/htdocs/sample/myProject
> webpack --mode development

Hash: fc514b9b90629362d324
Version: webpack 4.43.0
Time: 46ms
Built at: 2020/06/02 8:30:04
      Asset      Size  Chunks                   Chunk Names
    main.js   4.9 KiB    main  [emitted]        main
main.js.map  4.25 KiB    main  [emitted] [dev]  main //ソースマップファイル
Entrypoint main = main.js main.js.map
[./src/index.js] 408 bytes {main} [built]
[./src/modules/foo.js] 113 bytes {main} [built]

また development モードの場合、このオプションで指定する値(タイプ)により、バンドルしたファイル main.js の出力も変わります。

以下は devtool オプションの値により出力される main.js の例です

// devtool: 'eval' の場合の一部抜粋
        
・・・中略・・・
/***/ "./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 _modules_foo_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./modules/foo.js */ \"./src/modules/foo.js\");\n// import 文を使って foo.js の関数 greet をインポート\n\n\nfunction component() {\n  //div 要素を生成\n  const element = document.createElement('div');\n\n  // インポートした greet の実行結果を使って div 要素の HTML を作成\n  element.innerHTML = '<p>' + Object(_modules_foo_js__WEBPACK_IMPORTED_MODULE_0__[\"greet\"])() + '</p>';\n\n  return element;\n}\n\ndocument.body.appendChild(component());\n\n\n//# sourceURL=webpack:///./src/index.js?");
・・・以下省略・・・


//devtool: 'eval-cheap-source-map' の場合の一部抜粋

・・・中略・・・
/***/ "./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 _modules_foo_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./modules/foo.js */ \"./src/modules/foo.js\");\n// import 文を使って foo.js の関数 greet をインポート\n\n\nfunction component() {\n  //div 要素を生成\n  const element = document.createElement('div');\n\n  // インポートした greet の実行結果を使って div 要素の HTML を作成\n  element.innerHTML = '<p>' + Object(_modules_foo_js__WEBPACK_IMPORTED_MODULE_0__[\"greet\"])() + '</p>';\n\n  return element;\n}\n\ndocument.body.appendChild(component());\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,・・・中略・・・0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7Iiwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///./src/index.js\n");
・・・以下省略・・・


//devtool: 'source-map' の場合の一部抜粋

・・・中略・・・
/***/ "./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 _modules_foo_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./modules/foo.js */ "./src/modules/foo.js");
// import 文を使って foo.js の関数 greet をインポート

function component() {
  //div 要素を生成
  const element = document.createElement('div');

  // インポートした greet の実行結果を使って div 要素の HTML を作成
  element.innerHTML = '<p>' + Object(_modules_foo_js__WEBPACK_IMPORTED_MODULE_0__["greet"])() + '</p>';

  return element;
}

document.body.appendChild(component());
・・・以下省略・・・

webpack guides: Devtool

optimization

Webpack version 4 以上では選択したモードに応じて最適化を実行しますが、optimization プロパティでデフォルトの最適化の設定を上書き(変更)することができます。

optimization.minimize

optimization.minimize はデフォルトまたは optimization.minimizer で指定されたプラグインを使って圧縮を実行するかどうかを指定するプロパティです。

production モードではデフォルトで有効(true)になっています。

例えば、モードに関わらず常に圧縮(minify)を無効にするには以下のように minimize プロパティを false に設定します。

webpack.config.js
module.exports = {
  //...
  optimization: {
    minimize: false
  }
};

optimization.minimizer

optimization.minimizer を設定することで、デフォルトの圧縮方法を変更することができます。minimizer プロパティには圧縮に使用するプラグイン(TerserPlugin をカスタマイズしたものや他の圧縮用プラグイン)のインスタンスの配列を指定します。

以下は CSS の圧縮に optimize-css-assets-webpack-plugin を使用するように指定する例です。minimizer プロパティを指定するとデフォルトの JavaScript の圧縮にも影響があるため、以下では JavaScript の圧縮用に TerserPlugin も同時に指定しています(CSS を扱うには別途ローダーの設定も必要です)。

webpack.config.js
//プラグインの読み込み(プラグインは予め npm install でインストール)
const TerserPlugin = require('terser-webpack-plugin'); 
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');  
      
module.exports = {
  //...
  optimization: {
    minimizer: [
      new TerserPlugin({}),  //JavaScript 用の minify(圧縮)プラグイン
      new OptimizeCSSAssetsPlugin({})  //CSS 用の minify(圧縮)プラグイン
    ],
  }
};

以下は minimizer に TerserPlugin をカスタマイズして設定する例です。

webpack.config.js
//プラグインの読み込み
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        cache: true,
        parallel: true,
        sourceMap: true, // Must be set to true if using source-maps in production
        terserOptions: {
          // https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
        }
      }),
    ],
  }
};

optimization.splitChunks

複数のエントリポイントで共通のモジュールを使っている場合、それぞれのファイルに共通のモジュールをバンドルするのではなく、共通のモジュールだけ別のファイルとして出力することで全体のファイルサイズを小さくすることができます。

optimization.splitChunks は複数のエントリーポイントで利用している共通のモジュールをバンドルする際に適切に分離(chunk を split)するための設定です。

以下は SplitChunksPlugin のデフォルトの設定です。必要に応じて変更することができます。

webpack.config.js
module.exports = {
  //...
  optimization: {
    splitChunks: {  //default behavior of the SplitChunksPlugin
      chunks: 'async',
      minSize: 30000,
      minRemainingSize: 0,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 6,
      maxInitialRequests: 4,
      automaticNameDelimiter: '~',
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};

以下は node_modules からインポートされる全てのモジュールを別の vendors という名前の chunk に分離して出力する optimization.splitChunks の設定例です。

webpack.config.js
module.exports = {
  //...
  optimization: {
    splitChunks: {
    // 明示的に特定のファイル(共通モジュール)を chunk に分離して出力する場合に設定
      cacheGroups: {
        // 任意の名前
        commons: {  
          // node_modules 配下のモジュールを対象とする
          // パスセパレータはクロスプラットフォームに対応するため [\\/] を使用
          test: /[\\/]node_modules[\\/]/, 
          // バンドルされるファイルの名前
          name: 'vendors',
          //モジュールの種類(この場合は test の条件にマッチする全てを分割)
          chunks: 'all'
        }
      }
    }
  }
};

詳細は splitChunks を御覧ください。

webpack guides: SplitChunksPlugin

webpack guides: Optimization

resolve

webpack には import を使ってモジュールをインポートする際に、指定されたモジュールを検索して該当するファイルを探す仕組み(モジュール解決)があります。

resolve オプションはモジュール解決(モジュールの import を解決する仕組み)の設定を変更します。

resolve.modules

モジュールを解決するときに検索するディレクトリを webpack に指示します。

デフォルトは node_modules です。

module.exports = {
  //...
  resolve: {
    modules: ['node_modules']
  }
};

webpack guides: resolve.modules

resolve.extentions

このオプションで指定されている拡張子のファイルは import の際に拡張子を省略することができます。

省略する対象の拡張子を配列で指定します。これらの値を書き換えるとデフォルトを上書きすることになります。

module.exports = {
  //...
  resolve: {
    extensions: ['.wasm', '.mjs', '.js', '.json']
  }
};

webpack guides: resolve.extensions

resolve.alias

resolve.alias を使ってエイリアスを作成することで、特定のモジュールをより簡単にインポートすることができます。

例えば、よく使用する src /フォルダーのエイリアスを作成するには以下のように設定することができます。

const path = require('path');

module.exports = {
  //...
  resolve: {
    alias: {
      Utilities: path.resolve(__dirname, 'src/utilities/'),
    }
  }
};

以下のように相対バスでインポートする代わりに、

import Utility from '../../utilities/utility';

以下のようにエイリアスを使って記述してインポートすることができます。

import Utility from 'Utilities/utility';

webpack guides: resolve.alias

モジュール解決

以下は webpack のモジュール解決の概要です。

webpack で import を使ってモジュールをインポートする場合、以下のような指定が可能です。

絶対パス
import '/home/me/file';
import 'C:\\Users\\me\\file';
相対パス
import '../src/file1';
import './file2';
モジュールパス
import 'module';
import 'module/lib/file';

絶対パスと相対パスの場合は単純に指定されたディレクトリを検索します。

モジュールパスの場合は、resolve.modules で設定されているディレクトリを検索します。resolve.modules のデフォルトの検索先のフォルダは node_modules です。

パスが解決されたら、続いて指定されたパスがファイルかフォルダかで以下のようになります。

パスがファイルを指している場合

ファイルに拡張子が指定されていれば、該当するファイルで解決されます。

パスに拡張子がない場合は、resolve.extentions で指定された拡張子(例 .js)を使って解決します。

もし、該当するファイルが複数ある場合は、resolve.extentions で最初にリストされている拡張子が採用されます。

パスがフォルダを指している場合

そのフォルダに package.json があれば、resolve.mainFields で指定されているフィールドを参照してファイルを解決します。resolve.mainFields のデフォルトは ['browser', 'module', 'main'] です(デフォルトは target の値により異なります)。

package.json が存在しない場合や resolve.mainFields の値が有効でない場合は、resolve.mainFiles で指定されているファイル名(デフォルトは index)を検索し、resolve.extentions で指定されている拡張子で解決します。

webpack guides: Module Resolution

Loaders ローダー

ローダー(Loader)はリソース(ソースコード)を変換したりモジュール化するためのツールです。

webpack では、ローダーを使用してファイルを事前に処理することができます。

デフォルトでは webpack は JavaScript などのモジュール化されたものしかバンドルすることがでず、 画像や CSS ファイルなどのリソースはバンドルすることができません。

ローダーを使用すると webpack は他のタイプのファイルを処理してそれらを有効なモジュールに変換してバンドルしたり、バンドルする前にモジュールに対して変換などの処理を適用することができます。

ローダーはリソース(ファイル)の種類に合わせて使い分けます。

webpack guides: Loaders

ローダーを使用するには webpack.config.js を編集して設定します。 ローダーの設定は module プロパティのオプションに指定します。module の rules プロパティの test と use の2つのプロパティを使って設定します。

webpack.config.js
module.exports = {
  //ローダーの設定(module のプロパティにオプションを指定)
  module:{
    rules:[
      {
        test:/\.css$/,
        use:['style-loader','css-loader']
      }
    ]
  }
};
プロパティ 説明
module プロジェクト内の様々なタイプのモジュールがどのように処理されるかを設定するプロパティ
module.rules ローダーのルール(Rule)の配列 [Rule] を指定します。これらのルールはローダーをモジュールに適用したり、パーサーを変更したり、モジュールがどのように作成されるかを設定できます。
Rule.test
Condition.test
処理対象の変換するファイルを指定します。文字列の場合は絶対パスで指定します。正規表現や関数で指定することもできます(正規表現の場合は引用符は付けません)。
Rule.use 変換を行うために使用するローダーを指定します。ローダーの指定は文字列または、エントリ(UseEntry オブジェクト)の配列 [UseEntry] で指定することができます。use: [ 'style-loader' ] のように文字列を指定した場合は use: [ { loader: 'style-loader '} ] とオブジェクトでの指定のショートカット(同じ意味)になります。ローダーを複数指定してチェインすることができ、その場合、順番は最後に指定したものから適用されます。
UseEntry Rule.use プロパティで指定するエントリで、オブジェクトまたは関数で指定し、loader プロパティに文字列でローダーを指定します。文字列またはオブジェクトの options プロパティを指定することもできます。
Rule.exclude
Condition.exclude
ローダーの処理対象から外すディレクトリやファイルを指定することができます。文字列で指定する場合は絶対パスで指定します。正規表現や関数で指定することもできます。
Rule.enforce ローダーのカテゴリー('pre' または 'post')を指定します。例えば、enforce: 'pre'を指定すると enforce: 'pre'が付いていない通常のローダーより早く処理が実行されます。

babel-loader

Babel は JavaScript のコンパイラ(トランスパイラ)で、ECMAScript 2015(ES6)以上の仕様の JavaScript を互換性のある ES5 の構文に変換してくれるツールですが、Babel にはモジュールをまとめる機能がありません。

webpack と一緒に Babel を使用するにはローダー(babel-loader)を使用します。

Babel を使用するのに必要なモジュールをインストールします。

@babel/core(Babel の本体)、@babel/preset-env(Babel の環境設定)、babel-loader(ローダー)の3つのモジュールを最低限インストールする必要があります。モジュール名の先頭の @ は Scoped packages という指定方法です。

$ npm install babel-loader @babel/core @babel/preset-env --save-dev  return //必要なモジュールをインストール
+ @babel/core@7.10.2
+ babel-loader@8.1.0
+ @babel/preset-env@7.10.2
added 141 packages from 45 contributors and audited 555 packages in 9.873s

上記のモジュールインストール後の package.json は以下のようになっています。

package.json(例)
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production",
    "dev": "webpack --mode development"
  },
  "devDependencies": {
    "@babel/core": "^7.10.2",
    "@babel/preset-env": "^7.10.2",
    "babel-loader": "^8.1.0",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11"
  }
}

設定ファイル webpack.config.js にローダーの設定を追加します。

以下の例では test プロパティに正規表現で /\.js$/ を設定して、ファイル名が「.js」で終わるファイルを処理対象にするようにしています。「.mjs」も対象にするには /\.m?js$/ とします。

use プロパティでは、loader プロパティに変換を行うために使用するローダー babel-loader を指定して、options プロパティで使用するプリセット(presets)に関する設定をしています。

presets プロパティに @babel/preset-env を指定することで、最新の構文を ES5 の構文に変換できます。以下の例では targets を指定していませんが、必要に応じてターゲットブラウザなどを指定することができます。

webpack.config.js
const path = require('path');

module.exports = {
  // エントリポイントの設定
  entry: './src/index.js',
  // 出力先の設定
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  // ローダーの設定
  module: {
    rules: [
      {
        // ローダーの処理対象ファイル(拡張子 .js のファイルを対象)
        test: /\.js$/,
        // ローダーの処理対象から外すディレクトリ
        exclude: /node_modules/,
        // 処理対象のファイルに対する処理を指定
        use: [
          {
            // 利用するローダーを指定
            loader: 'babel-loader',
            // ローダー(babel-loader)のオプションを指定
            options: {
              // プリセットを指定
              presets: [
                // targets を指定していないので、一律に ES5 の構文に変換
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  }
};

上記の設定ファイルを保存して webpack を実行すると、src フォルダーに配置した JavaScript ファイルがトランスパイル(ES5 の構文に変換)され、dist フォルダーにバンドルされたファイル main.js が出力されます。

$ npm run build return // webpack を実行
// または npx webpack --mode=production

> myProject@1.0.0 build /Applications/MAMP/htdocs/sample/myProject
> webpack --mode production

Hash: 597cf13e2f36a217ed53
Version: webpack 4.43.0
Time: 415ms
Built at: 2020/06/02 14:02:35
  Asset      Size  Chunks             Chunk Names
main.js  1.05 KiB       0  [emitted]  main
Entrypoint main = main.js
[0] ./src/index.js + 1 modules 603 bytes {0} [built]
    | ./src/index.js 402 bytes [built]
    | ./src/modules/foo.js 201 bytes [built]

確認方法としては、例えば以下のプロジェクトのモジュールとして読み込んでいるファイル foo.js の関数をアロー関数に書き換えます。

プロジェクトのファイル構成

myProject
├── dist
│   ├── index.html
│   └── main.js
├── node_modules  
├── package-lock.json
├── package.json
├── src
│   ├── index.js
│   └── modules
│       └── foo.js //このファイルを編集して確認
└── webpack.config.js  
dist/index.html(表示用ファイル)
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>My First webpack</title>
  </head>
  <body>
    <script src="main.js"></script>
  </body>
</html>
src/index.js(エントリポイント)
// import 文を使って foo.js の関数 greet をインポート
import { greet } from './modules/foo.js';
 
function component() {
  //div 要素を生成
  const element = document.createElement('div');
  // インポートした greet の実行結果を使って div 要素の HTML を作成
  element.innerHTML = '<p>' + greet() + '</p>';
  return element;
}
 
document.body.appendChild(component());
src/modules/foo.js(このファイルを編集)
// 関数リテラルを使った定義
export function greet() {
  return 'Hello webpack!';
}
書き換え後の src/modules/foo.js
// アロー関数を使った定義
export const greet = ()  => 'Hello webpack arrow function!';

バンドルされたファイルを確認しやすいように webpack.config.js に mode: 'development', と devtool: 'source-map' を指定して webpack コマンドを実行します。

webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  },
  mode: 'development', // 追加
  devtool: 'source-map' // 追加
};

webpack を実行します。

$ npm run dev return //development モードでビルド
// または npx webpack --mode=development

> myProject@1.0.0 dev /Applications/MAMP/htdocs/sample/myProject
> webpack --mode development

出力されるバンドルされたファイル main.js を確認すると最後の方にアロー関数が関数リテラルに、const が var に書き換えられているのが確認できます。

main.js
・・・中略・・・
/***/ "./src/modules/foo.js":
/*!****************************!*\
  !*** ./src/modules/foo.js ***!
  \****************************/
/*! exports provided: greet */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "greet", function() { return greet; });
var greet = function greet() {
  return 'Hello webpack arrow function!';
};

webpack guides: babel-loader

eslint-loader

ESLint は JavaScript の文法や括弧やスペースの使い方などのスタイルを検証するツールです。

webpack と一緒に ESLint を使用するにはローダー(eslint-loader)を使用します。

ESLint を使用するのに必要な ESLint 本体と eslint-loader をインストールします。

$ npm install --save-dev eslint eslint-loader  return //必要なモジュールをインストール
+ eslint-loader@4.0.2
+ eslint@7.3.1
added 78 packages from 59 contributors and audited 610 packages in 3.462s

上記のモジュールインストール後の package.json は以下のようになっています。

package.json(例)
{
  "name": "myProject5",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production",
    "dev": "webpack --mode development"
  },
  "devDependencies": {
    "@babel/core": "^7.10.2",
    "@babel/preset-env": "^7.10.2",
    "babel-loader": "^8.1.0",
    "eslint": "^7.3.1",
    "eslint-loader": "^4.0.2",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11"
  }
}

webpack.config.js に ESLint 関連の設定を追加します(14〜29行目)。

enforce: 'pre' を指定して babel-loader で変換する前にコード検証するようにします(18行目)。

options: で fix: true を指定すると可能な場合は自動的に修正(変換)することができます(25行目)。options: には ESLint のオプションを設定することができます。

webpack.config.js
const path = require('path');
 
module.exports = {
  // エントリポイントの設定
  entry: './src/index.js',
  // 出力先の設定
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  // ローダーの設定
  module: {
    rules: [
      {
        // ESLint のローダー
        test: /\.js$/,
        //babel-loader で変換する前にコード検証する
        enforce: 'pre',
        exclude: /node_modules/,
        use: [
          {
            loader: 'eslint-loader',
            options: {
              //自動修正をする場合は以下のコメントを外す
              //fix: true,
            },
          },
        ],
      },
      
      {
        // ローダーの処理対象ファイル(拡張子 .js のファイルを対象)
        test: /\.js$/,
        // ローダーの処理対象から外すディレクトリ
        exclude: /node_modules/,
        // 処理対象のファイルに対する処理を指定
        use: [
          {
            // 利用するローダーを指定
            loader: 'babel-loader',
            // ローダー(babel-loader)のオプションを指定
            options: {
              // プリセットを指定
              presets: [
                // targets を指定していないので、一律に ES5 の構文に変換
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  }
};

Babel などのトランスパイラと一緒に使用する場合は、指定する順序に注意する必要があります。

例えば、Babel と ESLint のローダーをまとめて設定する場合は、以下のように eslint-loader を後に記述する必要があります。

これにより eslint-loader で処理された後に babel-loader によって処理されます。

または、上記の例のように eslint-loader に enforce: 'pre' を指定して、他のローダーより先に処理をするように設定できます。

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        //以下の場合 eslint-loader → babel-loader の順で処理される
        use: ['babel-loader', 'eslint-loader'],
      },
    ],
  },
  // ...
};

ESLint の設定ファイルを作成します。

設定ファイルは手動またはコマンドラインで対話形式で作成することができます。

また、設定ファイルの形式は JavaScript、YAML、JSON のいずれかで作成することができます。

以下は npx eslint --init コマンドを使って対話形式で作成する例です。選択した回答により、次の質問や生成されるファイルが変わってきます。

$ npx eslint --init   return //設定ファイルを生成
//以下のような質問と選択肢が表示されるので矢印キーで選んで return を押して確定します
? How would you like to use ESLint? … 
  To check syntax only
❯ To check syntax and find problems  //選択
  To check syntax, find problems, and enforce code style
 
?What type of modules does your project use? … 
❯ JavaScript modules (import/export)  //選択
  CommonJS (require/exports)
  None of these

? Which framework does your project use? … 
  React
  Vue.js
❯ None of these  //選択

? Does your project use TypeScript? › No / Yes   //No 

? Where does your code run? …  (Press <space> to select, <a> to toggle all, <i> to invert selection)
✔ Browser  //選択
✔ Node

? What format do you want your config file to be in? … 
  JavaScript
  YAML
❯ JSON  //選択

Successfully created .eslintrc.json file in /Applications/MAMP/htdocs/sample/myProject5

以下が上記コマンドで生成された設定ファイル .eslintrc.json の例です。

JSON ファイルですが1行コメントが使えます。

.eslintrc.json
{
  //環境設定
  "env": {
    //ブラウザで実行されるコードを検証
    "browser": true,
    //ES2020 を有効に
    "es2020": true
  },
  //推奨設定のルールを指定(ESLint が推奨するルールを使用)
  "extends": "eslint:recommended",
  "parserOptions": {
    "ecmaVersion": 11,
    //ES Modules 機能を有効にする
    "sourceType": "module"
  },
  //個別のルールの指定
  "rules": {
  }
}

再度上記コマンドを実行すると、新たに選択した内容で設定ファイルが上書きされます。フォーマットが異なる場合は新たに指定したフォーマットの設定ファイルが生成されます(不要な設定ファイルは削除します)。

以下は最初の選択肢で「To check syntax, find problems, and enforce code style」を選択して、スタイルガイドに airbnb を選択し、JavaScript フォーマットを選択した場合の例です。

$ npx eslint --init  return //設定ファイルを生成
✔ How would you like to use ESLint? · style //3番めの選択肢を選択
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · none
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · browser
✔ How would you like to define a style for your project? · guide
✔ Which style guide do you want to follow? · airbnb  //スタイルガイドに airbnb を選択
✔ What format do you want your config file to be in? · JavaScript
Checking peerDependencies of eslint-config-airbnb-base@latest  
//スタイルガイドに airbnb を選択したので依存ファイルがチェックされる
The config that youve selected requires the following dependencies:

eslint-config-airbnb-base@latest eslint@^5.16.0 || ^6.8.0 || ^7.2.0 eslint-plugin-import@^2.21.2
//依存ファイルをインストールするかどうかを聞かれる(Yes を選択)
✔ Would you like to install them now with npm? · No / Yes  
//依存ファイルがインストールされる
Installing eslint-config-airbnb-base@latest, eslint@^5.16.0 || ^6.8.0 || ^7.2.0, eslint-plugin-import@^2.21.2
+ eslint@7.3.1
+ eslint-plugin-import@2.22.0
+ eslint-config-airbnb-base@14.2.0
added 52 packages from 29 contributors, updated 1 package and audited 662 packages in 4.163s

Successfully created .eslintrc.js file in /Applications/MAMP/htdocs/sample/myProject5

以下のような JavaScript の設定ファイルが生成されます(この例では使用しません)。

.eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2020: true,
  },
  //eslint-config-airbnb-base のルールが指定されている
  extends: [
    'airbnb-base',
  ],
  parserOptions: {
    ecmaVersion: 11,
    sourceType: 'module',
  },
  rules: {
  },
};

もし、上記でインストールした依存ファイル(この例では eslint-config-airbnb-base)が不要であれば npm uninstall で削除できます。

$ npm uninstall eslint-config-airbnb-base  return //削除
removed 3 packages and audited 659 packages in 1.946s

以下は最初に生成した JSON の設定ファイルの例です。

デフォルトではルールが有効になっていませんが、"extends": "eslint:recommended"が設定されていると、ESLint のルールのページの一覧でチェックマーク が付いていいるルールが有効になります。

個別のルールを指定することで、設定されているルールを上書きすることができます。試しに以下のようなインデントとセミコロンに関する個別のルールを追加します。

.eslintrc.json
{
  "env": {
    "browser": true,
    "es2020": true
  },
  //ESLint の推奨ルール
  "extends": "eslint:recommended",
  "parserOptions": {
    "ecmaVersion": 11,
    "sourceType": "module"
  },
  "rules": {
    //インデントは2スペース
    "indent": [
      // error を指定するとルールに適合しないとエラーが発生します 
      "error",
      2
    ],
    //セミコロンを必ず付ける
    "semi": [
      "error",
      "always"
    ]
  }
}

index.js を以下のように変更してビルドしようとすると、上記で指定した個別のルールに適合しないため ESLint によりエラーになります。

index.js
import { greet } from './modules/foo.js';
 
function component() {
  const element = document.createElement('div');
  //セミコロンを省略
  element.innerHTML = '<p>' + greet() + '</p>'
   //インデントをスペース4つに変更
    return element;
}
 
document.body.appendChild(component());

ビルドしようとするとエラーになり、コンパイルできなくなります。実際のコンソールではエラーの部分は赤い字で表示されます。

$ npm run build   return
・・・中略・・・

ERROR in ./src/index.js  //エラー(赤字で表示される)
Module Error (from ./node_modules/eslint-loader/dist/cjs.js):

/Applications/MAMP/htdocs/sample/myProject5/src/index.js
  8:47  error  Missing semicolon                             semi
  9:1   error  Expected indentation of 2 spaces but found 4  indent

✖ 2 problems (2 errors, 0 warnings)  //エラー(赤字で表示される)
  2 errors and 0 warnings potentially fixable with the `--fix` option.

ルールのオプションを "warn" に変更すると、警告が表示されますがビルドは実行されます。

.eslintrc.json 抜粋
"rules": {
    //インデントは2スペース
    "indent": [
      //error から warn に変更
      "warn", 
      2
    ],
    //セミコロンを必ず付ける
    "semi": [
      //error から warn に変更
      "warn", 
      "always"
    ]
  }

実際のコンソールでは警告の部分は黄色い字で表示されます。

$ npm run build   return
・・・中略・・・

  Asset      Size  Chunks             Chunk Names
main.js  1.03 KiB       0  [emitted]  main
Entrypoint main = main.js
[0] ./src/index.js + 1 modules 471 bytes {0} [built] [1 warning]
    | ./src/index.js 405 bytes [built] [1 warning]
    | ./src/modules/foo.js 66 bytes [built]

WARNING in ./src/index.js  //警告
Module Warning (from ./node_modules/eslint-loader/dist/cjs.js):

/Applications/MAMP/htdocs/sample/myProject5/src/index.js
  8:50  warning  Missing semicolon                             semi
  9:1   warning  Expected indentation of 2 spaces but found 4  indent

✖ 2 problems (0 errors, 2 warnings)  //警告
  0 errors and 2 warnings potentially fixable with the `--fix` option.

webpack.config.js の fix: true を指定すると、エラーも警告も表示されず、自動的に修正されてビルドされます。

但し fix: true を指定しても全てのルールが自動的に修正されるわけではなく、ESLint のルールの一覧ページでレンチ のアイコンがついているルールだけが自動で修正されます。

webpack.config.js 抜粋
{
  // ESLint のローダ
  test: /\.js$/,
  //babel-loader で変換する前にコード検証する
  enforce: 'pre',
  exclude: /node_modules/,
  use: [
    {
      loader: 'eslint-loader',
      options: {
        //自動修正を有効にする
        fix: true,
      },
    },
  ],
},

この項目(eslint-loader)は後から追加したもので、package.json や webpack.config.js での eslint-loader の設定はここだけに記載されています。

css-loader/style-loader

CSS を webpack で扱うには、CSS を変換する css-loaderstyle-loaderMiniCssExtractPlugin(プラグイン)などを利用します。

以下は CSS を css-loader と style-loader を使って、output に指定した出力ファイル(main.js)にバンドルする例です。

CSS を別ファイルとして出力する方法は MiniCssExtractPlugin を御覧ください。

今までの例のプロジェクトの src フォルダに以下のようなスタイルシートを追加します。

style.css
p {
  color: green;
  background-color: yellow;
}

エントリポイント index.js で import 文を使って上記の CSS ファイル(style.css)を読み込む記述を追加します。

index.js
// import 文を使って style.css を読み込む
import  './style.css';

import { greet } from './modules/foo.js';

function component() {
  const element = document.createElement('div');
  element.innerHTML = '<p>' + greet() + '</p>';
  return element;
}

document.body.appendChild(component());
ファイル構成
myProject
├── dist
│   ├── index.html
│   └── main.js
├── node_modules  
├── package-lock.json
├── package.json
├── src
│   ├── index.js  //import 文を使って style.css を読み込む
│   ├── modules
│   │   └── foo.js
│   └── style.css  //追加したスタイルシート
└── webpack.config.js 

その他のファイルは今までの例と同じものを使用しています。

foo.js(エントリポイント index.js で読み込まれて使用されるファイル=モジュール)
export const greet = ()  => 'Hello webpack with CSS';
index.html(バンドルされた main.js を読み込みブラウザで表示するファイル)
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>My First webpack</title>
  </head>
  <body>
    <script src="main.js"></script>
  </body>
</html>

CSS の処理や挿入に必要な以下のローダー(モジュール)をプロジェクトにインストールします。

  • css-loader:CSSを処理するためのモジュール
  • style-loader:style 要素を生成して CSS を head 要素に挿入するモジュール

以下をプロジェクトのディレクトリで実行します。

$ npm install css-loader style-loader --save-dev return //ローダーをインストール
+ css-loader@3.5.3
+ style-loader@1.2.1
added 19 packages from 51 contributors and audited 574 packages in 2.882s

上記のモジュールインストール後の package.json は、css-loader と style-loader が追加され以下のようになります。12〜14行目は Babel 用のモジュールで、前述の例に追加しているため表示されていますが、CSS のローダーとは関係ありません。

package.json(例)
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production",
    "dev": "webpack --mode development"
  },
  "devDependencies": {
    "@babel/core": "^7.10.2",
    "@babel/preset-env": "^7.10.2",
    "babel-loader": "^8.1.0",
    "css-loader": "^3.5.3",
    "style-loader": "^1.2.1",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11"
  }
}

設定ファイル webpack.config.js に CSS 用のローダーの設定を追加します。

以下の例では test プロパティに正規表現で /\.css$/i を設定して、ファイル名が「.css」や「.CSS」で終わるファイルを処理対象にするようにしています(15行目)。

use プロパティでは、loader プロパティに変換を行うために使用するローダー(style-loader と css-loader)を指定しています。指定したローダーが後ろから順番(css-loader → style-loader の順)に適用されます(17行目)。

css-loader は CSS を CommonJS に変換するなどの処理をします。画像も扱う場合は file-loader url-loader と組み合わせて使用します。

style-loader は JavaScript から style 要素(ノード)を生成して CSS を DOM(head 要素)に挿入します。

webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  // ローダーの設定
  module: {
    rules: [
      {
        // CSS を扱うローダー
        //ローダーの処理対象ファイル(拡張子 .css や .CSS のファイル)
        test: /\.css$/i,
        //ローダーを指定
        use: [
          // CSS を出力するローダー
          'style-loader', 
          // CSS を CommonJS に変換するローダー
          'css-loader'  
        ],
      },
      
      {
        // Babel 用のローダー
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  }
};

npm run dev で webpack を実行します。

$ npm run dev  return // webpack を実行 
// または npx webpack --mode=development

> myProject@1.0.0 dev /Applications/MAMP/htdocs/sample/myProject
> webpack --mode development

Hash: e4dadb4ea5e9f716d09d
Version: webpack 4.43.0
Time: 408ms
Built at: 2020/06/03 17:19:53
  Asset      Size  Chunks             Chunk Names
main.js  17.4 KiB    main  [emitted]  main
Entrypoint main = main.js
[./node_modules/css-loader/dist/cjs.js!./src/style.css] 288 bytes {main} [built]
[./src/index.js] 477 bytes {main} [built]
[./src/modules/foo.js] 75 bytes {main} [built]
[./src/style.css] 519 bytes {main} [built]  // CSS の処理
    + 2 hidden modules

ブラウザで index.html を開いて再読み込みすると p 要素へ設定したスタイルが適用されているのが確認できます。また、開発ツールで見ると style 要素が head 内に挿入されているのが確認できます。

これは index.html で読み込んでいる main.js(バンドルされた JavaScript)により style 要素が head 内に挿入されているためです。以下が実際の index.html です。

index.html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>My First webpack</title>
  </head>
  <body>
    <script src="main.js"></script> 
    <!-- main.js により style 要素が head 内に挿入される -->
  </body>
</html>

バンドルされたファイル main.js を確認すると css-loader や style-loader の記述が確認できます。

main.js 一部抜粋
/***/ "./node_modules/css-loader/dist/cjs.js!./src/style.css":
/*!*************************************************************!*\
  !*** ./node_modules/css-loader/dist/cjs.js!./src/style.css ***!
  \*************************************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval("// Imports\nvar ___CSS_LOADER_API_IMPORT___ = __webpack_require__(/*! ../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\nexports = ___CSS_LOADER_API_IMPORT___(false);\n// Module\nexports.push([module.i, \"p {\\n  color: green;\\n  background-color: yellow;\\n}\\n\\n\", \"\"]);\n// Exports\nmodule.exports = exports;\n\n\n//# sourceURL=webpack:///./src/style.css?./node_modules/css-loader/dist/cjs.js");

/***/ }),

webpack guides: css-loader

webpack guides: style-loader

CSS のソースマップ

css-loader には CSS のソースマップを有効にするオプション sourceMap がありますが、有効にすると処理時間と出力されるファイルのサイズが大きくなるためデフォルトでは無効(false)になっています。

CSS のソースマップを有効にするにはローダーの設定に sourceMap オプションを追加します。

webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  // ローダーの設定
  module: {
    rules: [
      {
        // CSS を扱うローダー
        //ローダーの処理対象ファイル(拡張子 .css や .CSS のファイル)
        test: /\.css$/i,
        //ローダーを指定
        use: [
          // CSS を出力するローダー
          'style-loader', 
          {
            // CSS を CommonJS に変換するローダー
            loader: 'css-loader',
            options: {
              // ソースマップを有効に
              sourceMap: true,
            }
          }
        ],
      },
      
      {
        // Babel 用のローダー
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  }
};

webpack.config.js を変更後 npm run dev で webpack を実行するとソースマップが有効になります。

CSS の画像

スタイルシートで画像を読み込んでいる場合、画像も処理の対象になるので css の url() メソッドを変換したり、画像を変換するためのローダーが必要になります。

例えば、前述の例の CSS を以下のように背景色から背景画像(background-image)に変更して、画像を用意してもうまくいきません。

style.css
p {
  color: red;
  background-image: url("./images/sample.png");
}

npm run dev で webpack を実行すると以下のように「このファイルを処理するように設定されているローダーはありません」というようなエラーになります。

$ npm run dev  return // webpack を実行 
// または npx webpack --mode=development
・・・中略・・・
  Asset      Size  Chunks             Chunk Names
main.js  20.2 KiB    main  [emitted]  main
Entrypoint main = main.js
[./node_modules/css-loader/dist/cjs.js?!./src/style.css] ./node_modules/css-loader/dist/cjs.js??ref--4-1!./src/style.css 1.22 KiB {main} [built]
[./src/images/sample.png] 281 bytes {main} [built] [failed] [1 error] //エラー
[./src/index.js] 295 bytes {main} [built]
[./src/modules/foo.js] 66 bytes {main} [built]
[./src/style.css] 529 bytes {main} [built]
    + 3 hidden modules

ERROR in ./src/images/sample.png 1:0 //エラーの詳細
Module parse failed: Unexpected character '�' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders

画像を webpack の処理の対象外にする

css-loader のオプション url で URL の解決を無効にすることで追加のローダーを使わずに画像を扱うことができます。

この場合、スタイルシートで読み込む画像は出力先の dest 配下に配置します。CSS での画像のパスは表示用の HTML(index.html)からのパス(./images/sample.png)になります。

myProject
├── dist
│   ├── images
│   │   └── sample.png  //background-image で使う背景画像を配置
│   ├── index.html
│   └── main.js
├── package-lock.json
├── package.json
├── src
│   ├── index.js
│   ├── modules
│   │   └── foo.js
│   └── style.css
└── webpack.config.js

webpack.config.js を編集して css-loader のオプション url に false を設定(19行目)して URL の解決を無効にします。

これにより src/style.css の画像の読み込みの url() メソッドは変換されずに出力されるため、画像を参照することができます。

webpack.config.js
const path = require('path');
 
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          'style-loader', 
          {
            loader: 'css-loader',
            options: {
              //URL の解決を無効に
              url: false,
              // ソースマップを有効に
              sourceMap: true,
            }
          }
        ],
      },
      
      {
        // Babel 用のローダー
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  }
};

以下は main.js の src/style.css 部分の抜粋です。./images/sample.png がそのまま出力されています。

main.js 一部抜粋
/***/ "./node_modules/css-loader/dist/cjs.js?!./src/style.css":
/*!***********************************************************************!*\
  !*** ./node_modules/css-loader/dist/cjs.js??ref--4-1!./src/style.css ***!
  \***********************************************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval("// Imports\nvar ___CSS_LOADER_API_IMPORT___ = __webpack_require__(/*! ../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\nexports = ___CSS_LOADER_API_IMPORT___(true);\n// Module\nexports.push([module.i, \"p {\\n  color: red;\\n  background-image: url(\\\"./images/sample.png\\\");\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"style.css\"],\"names\":[],\"mappings\":\"AAAA;EACE,UAAU;EACV,4CAA4C;AAC9C\",\"file\":\"style.css\",\"sourcesContent\":[\"p {\\n  color: red;\\n  background-image: url(\\\"./images/sample.png\\\");\\n}\\n\"]}]);\n// Exports\nmodule.exports = exports;\n\n\n//# sourceURL=webpack:///./src/style.css?./node_modules/css-loader/dist/cjs.js??ref--4-1");

/***/ }),

画像も webpack で処理してバンドルするには、url-loaderfile-loader などのローダーを使用します。

url-loader

url-loader を使うとスタイルシートで読み込んでいる画像も JavaScript ファイル(出力ファイル)にバンドルすることができます。url-loader は画像を Data URI(Base64)に変換し、出力される JavaScript ファイルに含めます。

画像をバンドルすることでリクエスト回数を減らすことが可能になりますが、大きなサイズの画像はデータが大きくなり過ぎてかえって逆効果になる場合もあります。

Base64 でエンコードされたバイナリデータの最終サイズは、元のデータサイズの1.3x倍になるようです。

url-loader を使用するには url-loader のモジュールを追加でインストールします。

$ npm install url-loader --save-dev  return // url-loader を追加インストール
+ url-loader@4.1.0
added 4 packages from 6 contributors and audited 708 packages in 2.493s

画像を src フォルダに配置します。この例では src/images に配置しています。

myProject
├── dist
│   ├── index.html
│   └── main.js
├── package-lock.json
├── package.json
├── src
│   ├── images
│   │   └── sample.png  //背景画像を配置
│   ├── index.js  //エントリポイント
│   ├── modules
│   │   └── foo.js
│   └── style.css
└── webpack.config.js

以下が CSS です。画像のパスは同じ src フォルダ内なので ./images/sample.png としています。

style.css
p {
  color: red;
  background-image: url("./images/sample.png");
}

webpack.config.js を編集してローダーの設定を追加します(27行目〜36行目)。

処理対象の画像ファイルの拡張子を test プロパティに正規表現で指定し、use プロパティでは、loader プロパティに変換を行うために使用するローダー(url-loader)を指定しています。

webpack.config.js
const path = require('path');
 
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          'style-loader', 
          {
            loader: 'css-loader',
            options: {
              //URL の解決を有効に
              url: true,  //デフォルトは true なので省略可能
              // ソースマップを有効に
              sourceMap: true,
            }
          }
        ],
      },
      
      {
        // 対象となるファイルの拡張子
        test: /\.(gif|png|jpe?g|svg|eot|wof|woff|woff2|ttf)$/i,
        use: [
          {
            //画像をData URI(Base64)に変換するローダー
            loader: 'url-loader',
          }
        ]
      },
      
      {
        // Babel 用のローダー
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  }
};

npm run dev で webpack を実行します。

$ npm run dev  return // webpack を実行 
// または npx webpack --mode=development

> myProject3@1.0.0 dev /Applications/MAMP/htdocs/sample/myProject
> webpack --mode development

Hash: 703cde6459617f8044a3
Version: webpack 4.43.0
Time: 712ms
Built at: 2020/06/10 14:01:55
  Asset      Size  Chunks             Chunk Names
main.js  21.2 KiB    main  [emitted]  main
Entrypoint main = main.js
[./node_modules/css-loader/dist/cjs.js?!./src/style.css] ./node_modules/css-loader/dist/cjs.js??ref--4-1!./src/style.css 807 bytes {main} [built]
[./src/images/sample.png] 1.55 KiB {main} [built]  // 画像が処理され main にバンドル
[./src/index.js] 295 bytes {main} [built]
[./src/modules/foo.js] 66 bytes {main} [built]
[./src/style.css] 529 bytes {main} [built]
    + 3 hidden modules

webpack を実行すると、画像は Data URI(文字列)に変換され、main.js にバンドルされます。

main.js 一部抜粋
/***/ "./src/images/sample.png":
/*!*******************************!*\
  !*** ./src/images/sample.png ***!
  \*******************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgA・・・中略・・・2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hLvJ8oXkAujHo4Jwx6/MefizwFGAAp5TxAum/cgwAAAABJRU5ErkJggg==\");\n\n//# sourceURL=webpack:///./src/images/sample.png?");

/***/ }),

サイズによる制御 limit

オプションの limit を使うと一定のサイズ以上の画像の場合は Data URI に変換せずに、file-loader を使ってファイルとして分離することができます。

file-loader をインストールします。

$ npm install file-loader --save-dev  return // file-loader を追加インストール
+ file-loader@6.0.0
added 4 packages from 6 contributors and audited 712 packages in 2.404s

webpack.config.js を編集して url-loader の limit オプションと name オプションの設定を追加します(34行目〜39行目)。

limit オプションに指定したサイズより大きい画像の場合は、画像は変換されず file-loader によって出力先の dist フォルダーにコピーされ、その画像への URL がバンドルされたファイルに埋め込まれます。単位はバイトで指定します(デフォルトは無制限)。

name オプションでは dist フォルダーにコピーされる画像の名前とパスを設定できます。[name] は元のファイル名、[ext] は元の拡張子です。

fallback オプションを使うと、ile-loader 以外のローダーを指定することができます。

const path = require('path');
 
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          'style-loader', 
          {
            loader: 'css-loader',
            options: {
              //URL の解決を有効に
              url: true,
              // ソースマップを有効に
              sourceMap: true,
            }
          }
        ],
      },
      
      {
        // 対象となるファイルの拡張子
        test: /\.(gif|png|jpe?g|svg|eot|wof|woff|woff2|ttf)$/i,
        use: [
          {
            //画像をData URI(Base64)に変換するローダー
            loader: 'url-loader',
            options: {
              // 50KB以上だったらファイルとしてコピー(分離)する
              limit: 50 * 1024, 
              // 画像ファイルの名前とパスの設定
              name: './images/[name].[ext]'
            }
          }
        ]
      },
      
      {
        // Babel 用のローダー
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  }
};

webpack guides: url-loader

file-loader

file-loader を使うとスタイルシートで読み込んでいる画像などを出力ファイルにバンドルせずに、出力フォルダーにコピーして参照することができます。

css-loader の url オプションに false を指定する場合、自分で dist フォルダーに画像を配置しておきますが、file-loader を使う場合は src フォルダーに画像を配置して、file-loader によって画像が dist フォルダーにコピーされます(同じ画像が src フォルダーと dist フォルダーに存在することになります)。

file-loader のモジュールがインストールされていない場合は、追加でインストールします。

$ npm install file-loader --save-dev  return // file-loader を追加インストール
+ file-loader@6.0.0
added 4 packages from 6 contributors and audited 712 packages in 2.404s

スタイルシートで読み込む画像を src フォルダーに配置します。この例では src/images に配置しています。

myProject
├── dist
│   ├── index.html
│   └── main.js
├── package-lock.json
├── package.json
├── src
│   ├── images
│   │   └── sample.jpg  //背景画像を配置
│   ├── index.js  //エントリポイント
│   ├── modules
│   │   └── foo.js
│   └── style.css
└── webpack.config.js
style.css
p {
  color: red;
  background-image: url("./images/sample.jpg");
}

webpack.config.js を編集してローダーの設定を追加します(24行目〜37行目)。

処理対象の画像ファイルの拡張子を test プロパティに正規表現で指定し、use プロパティでは、loader プロパティに変換を行うために使用するローダー(file-loader)を指定しています。

options の name プロパティではコピーされる(画像)ファイルの名前とパスを設定しています。

webpack.config.js
const path = require('path');
 
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          'style-loader', 
          {
            loader: 'css-loader',
            options: {
              sourceMap: true,
            }
          }
        ],
      },
      
      {
        // 対象となるファイルの拡張子
        test: /\.(gif|png|jpe?g|svg|eot|wof|woff|ttf)$/i,
        use: [
          {
            //画像を出力フォルダーにコピーするローダー
            loader: 'file-loader',
            options: {
              // 画像ファイルの名前とパスの設定
              name: './images/[name].[ext]'
            }
          }
        ],
      },
      
      {
        // Babel 用のローダー
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  }
};

npm run dev で webpack を実行します。

$ npm run dev  return // webpack を実行 
// または npx webpack --mode=development

> myProject3@1.0.0 dev /Applications/MAMP/htdocs/webdesignleaves/pr/jquery/webpack/myProject3
> webpack --mode development

Hash: 407fcadbf24f0d0a5720
Version: webpack 4.43.0
Time: 6030ms
Built at: 2020/06/10 15:58:33
              Asset      Size  Chunks             Chunk Names
./images/sample.jpg  42.3 KiB          [emitted]  
            main.js  19.7 KiB    main  [emitted]  main
Entrypoint main = main.js
[./node_modules/css-loader/dist/cjs.js?!./src/style.css] ./node_modules/css-loader/dist/cjs.js??ref--4-1!./src/style.css 807 bytes {main} [built]
[./src/images/sample.jpg] 63 bytes {main} [built]  //sample.jpg
[./src/index.js] 295 bytes {main} [built]
[./src/modules/foo.js] 66 bytes {main} [built]
[./src/style.css] 529 bytes {main} [built]
    + 3 hidden modules

上記 webpack を実行すると src フォルダーの画像が file-loader によって dist フォルダーにコピーされます。

ブラウザで index.html を開いて再読み込みすると url(./images/sample.jpg) で画像が読み込まれているのが確認できます。

url-loader の limit オプションで一定のサイズ以上の画像の場合は Data URI に変換せずに、file-loader を使ってファイルとして分離することもできます(サイズによる制御 limit)。

webpack guides: file-loader

sass-loader

webpack は sass-loader を使って Sass を CSS へ変換する処理を追加することができます。

以下の例では、Sass でタイルを定義して、sass-loader を使って CSS に変換しています。以下が大まかなローダーの処理の流れです。

  1. sass-loader を使って Sass を CSS へ変換
  2. css-loader で CSS を JavaScript(CommonJS) に変換
  3. style-loader で JavaScript から style 要素を生成して CSS を head 要素に挿入

index.html では出力される(バンドルされた) main.js を読み込むことでスタイルが適用されます。

前述の例の CSS(style.css)を変数を使った Sass の記述に書き換えて style.scss という名前(拡張子:.scss)で保存します。この例の場合、元の CSS(style.css)は不要なので削除します。

style.scss(Sass ファイル)
$p_color: green;
$p_bg_color: yellow;

p {
  color: $p_color;
  background-color: $p_bg_color;
}

エントリポイント index.js で import 文を使って上記の Sass ファイルを読み込む記述に変更します。

index.js
// import 文を使って style.scss(Sass ファイル)を読み込む
import  './style.scss';  

import { greet } from './modules/foo.js';

function component() {
  const element = document.createElement('div');
  element.innerHTML = '<p>' + greet() + '</p>';
  return element;
}

document.body.appendChild(component());

その他のファイルは今までの例と同じものを使用しています。

foo.js(エントリポイント index.js で読み込まれて使用されるファイル=モジュール)
export const greet = ()  => 'Hello webpack with CSS';
index.html(バンドルされた main.js を読み込みブラウザで表示するファイル)
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>My First webpack</title>
  </head>
  <body>
    <script src="main.js"></script>
  </body>
</html>
ファイル構成
myProject
├── dist
│   ├── index.html
│   └── main.js
├── node_modules  
├── package-lock.json
├── package.json
├── src
│   ├── index.js  //import 文を使って style.scss(Sass ファイル)を読み込む
│   ├── modules
│   │   └── foo.js
│   └── style.scss  //追加した Sass ファイル(元の CSS は削除)
└── webpack.config.js 

Sass ファイルを CSS に変換する sass-loader と Sass をコンパイルするための node-sass をプロジェクト(myProject)に追加インストールします。

以下をプロジェクトのディレクトリで実行します。

$ npm install sass-loader node-sass --save-dev  return // パッケージをインストール 

上記パッケージのインストール後の package.json は、node-sass と sass-loader が追加され以下のようになります。12〜14行目は Babel 用のモジュールで Sass や CSS のローダーとは関係ありません。

package.json(例)
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production",
    "dev": "webpack --mode development"
  },
  "devDependencies": {
    "@babel/core": "^7.10.2",
    "@babel/preset-env": "^7.10.2",
    "babel-loader": "^8.1.0",
    "css-loader": "^3.5.3",
    "node-sass": "^4.14.1",
    "sass-loader": "^8.0.2",
    "style-loader": "^1.2.1",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11"
  }
}

設定ファイル webpack.config.js を編集します。

test: /\.s[ac]ss$/i に変更して対象のファイルを Sass ファイル(.scss や .sass)にし、use に sass-loader を追加します。

sass-loader を最後に追加したのは css-loader で css を CommonJS に変換する前に sass-loader で Sass を CSS に変換する必要があるためです。

指定したローダーが後ろから順番(sass-loader → css-loader → style-loader の順)に適用されます

webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  // ローダーの設定
  module: {
    rules: [
      {
        // Sass 用のローダー
        //ローダーの処理対象ファイル(拡張子 .scss や .sass のファイル)
        test: /\.s[ac]ss$/i,  
        // または test: /\.(scss|sass|css)$/i, とすれば css も対象にできる
        use: [
          // CSS を出力するローダー
          'style-loader',
          // CSS を CommonJS に変換するローダー
          'css-loader',
          // Sass をコンパイルするローダー
          'sass-loader',
        ],
      },
      
      {
        // Babel のローダー用
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  }
};

webpack.config.js を変更後 npm run dev で webpack を実行します。

$ npm run dev  return // webpack を実行 
// または npx webpack --mode=development

> myProject@1.0.0 dev /Applications/MAMP/htdocs/sample/myProject
> webpack --mode development

Hash: 2d14fb0a7e5c59c58b25
Version: webpack 4.43.0
Time: 420ms
Built at: 2020/06/04 10:17:52
  Asset      Size  Chunks             Chunk Names
main.js  17.7 KiB    main  [emitted]  main
Entrypoint main = main.js
[./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./src/style.scss] 285 bytes {main} [built]
[./src/index.js] 478 bytes {main} [built]
[./src/modules/foo.js] 75 bytes {main} [built]
[./src/style.scss] 560 bytes {main} [built]  //Sass
    + 2 hidden modules

ブラウザで index.html を開いて再読み込みすると Sass で p 要素へ設定したスタイルが確認できます。また、開発ツールで見ると style 要素が head 内に挿入されているのが確認できます。

index.html で読み込んでいるバンドルされた JavaScript main.js によりスタイルが挿入されています。

以下は css-loader と sass-loader のソースマップの出力を有効にし、Sass のアウトプットスタイルを compressed に指定する例です。

use での指定を文字列(css-loader、sass-loader)からオブジェクトでの記述に変更してオプション options を指定しています。

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  // ローダーの設定
  module: {
    rules: [
      {
        // Sass 用のローダー
        //ローダーの処理対象ファイル(拡張子 .scss や .sass のファイル)
        test: /\.s[ac]ss$/i,
        use: [
          // CSS を出力するローダー
          'style-loader',
          // CSS を CommonJS に変換するローダー
          {    
            loader: 'css-loader',
            options: {
              // ソースマップを有効に
              sourceMap: true,
            },
          },
          // Sass をコンパイルするローダー
          {
            loader: 'sass-loader',
            options: {
              // ソースマップを有効に
              sourceMap: true,
              sassOptions: {
                // アウトプットスタイルの指定
                outputStyle: 'compressed',
              },
            }
          }
        ],
      },
      
      {
        // Babel のローダー用
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  }
};

webpack.config.js を変更後 npm run dev で webpack を実行するとソースマップが有効になります。

webpack guides: sass-loader

Plugins プラグイン

ローダーは特定のタイプのモジュールの変換に使用しますが、プラグインはバンドルの最適化や資産管理など様々なものが存在し、利用することで幅広いタスクを実行することができます。

webpack guides: Concept/Plugins

プラグインを使用するには、require() を使ってプラグインを読み込み、webpack.config.js で plugins プロパティの配列に追加します。

プラグインの指定では、 new 演算子でプラグインのインスタンスを生成します。ほとんどのプラグインは引数にオプションを指定することができます。

MiniCssExtractPlugin

前述の例では、style-loader を使って CSS を JavaScript にバンドルしましたが、 CSS を別ファイルとして出力するには MiniCssExtractPlugin(mini-css-extract-plugin)を使います。

以下は MiniCssExtractPlugin を使って CSS を別ファイルとして出力する例です。

ファイル構成は前述の sass-loader の例と同じものを使用します。

ファイル構成
myProject
├── dist
│   ├── index.html  //ブラウザで表示するファイル(script タグで main.js を読み込む)
│   └── main.js //ビルドされて出力されるファイル
├── node_modules  
├── package-lock.json
├── package.json
├── src
│   ├── index.js  //import 文を使って style.scss と foo.js を読み込む
│   ├── modules
│   │   └── foo.js  //モジュール
│   └── style.scss   //Sass
└── webpack.config.js 

以前の例では、ブラウザで表示するファイル index.html ではバンドルされて出力される JavaScript(main.js)のみを読み込んでしましたが、MiniCssExtractPlugin を使うことで CSS は JavaScript から抽出され別ファイルとして出力されるので、link タグを使って出力される CSS(style.css)を読み込みます。

この時点では style.css は生成されていないので、ブラウザで表示してもスタイルは適用されていません。

index.html
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>My First webpack</title>
    <link rel="stylesheet" href="style.css"><!-- 追加 -->
  </head>
  <body>
    <script src="main.js"></script><!-- 出力される main.js の読み込み -->
  </body>
</html>
index.js(エントリポイント)
// import 文を使って style.scss(Sass)を読み込む
import  './style.scss';  

import { greet } from './modules/foo.js';

function component() {
  const element = document.createElement('div');
  element.innerHTML = '<p>' + greet() + '</p>';
  return element;
}

document.body.appendChild(component());
style.scss(Sass ファイル)
$p_color: green;
$p_bg_color: yellow;

p {
  color: $p_color;
  background-color: $p_bg_color;
}
foo.js(エントリポイント index.js で読み込まれて使用されるファイル=モジュール)
export const greet = ()  => 'Hello webpack with CSS';

MiniCssExtractPlugin(mini-css-extract-plugin)のインストール

この例では既に css-loader と sass-loader はインストール済みなので、CSS を別ファイルとして出力するプラグイン mini-css-extract-plugin をインストールします。

$ npm install mini-css-extract-plugin --save-dev return
+ mini-css-extract-plugin@0.9.0
added 7 packages from 3 contributors and audited 711 packages in 2.551s

上記のモジュールインストール後の package.json は、mini-css-extract-plugin が追加され(16行目)以下のようになります。

package.json(例)
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production",
    "dev": "webpack --mode development"
  },
  "devDependencies": {
    "@babel/core": "^7.10.2",
    "@babel/preset-env": "^7.10.2",
    "babel-loader": "^8.1.0",
    "css-loader": "^3.5.3",
    "mini-css-extract-plugin": "^0.9.0",
    "node-sass": "^4.14.1",
    "sass-loader": "^8.0.2",
    "style-loader": "^1.2.1",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11"
  }
}

設定ファイル webpack.config.js を編集します。

require() を使ってプラグイン MiniCssExtractPlugin(mini-css-extract-plugin)を読み込みます。

plugins プロパティに new 演算子で MiniCssExtractPlugin プラグインのインスタンスを生成し、引数に出力される CSS のファイル名を指定します。

ローダーには style-loader の代わりに MiniCssExtractPlugin.loader を指定し、CSS を抽出して出力するようにします(style-loader は必要ないので削除)。以下が大まかなローダーの処理の流れです。

  1. sass-loader を使って Sass を CSS へ変換
  2. css-loader で CSS を JavaScript(CommonJS) に変換
  3. MiniCssExtractPlugin.loader で CSS を 抽出して別ファイルとして出力
webpack.config.js
const path = require('path');
//MiniCssExtractPlugin の読み込み
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  //プラグインの設定
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'style.css',
    }),
  ],
  // ローダーの設定
  module: {
    rules: [
      {
        //ローダーの処理対象ファイル(拡張子 .scss や .sass のファイル)
        test: /\.s[ac]ss$/i,
        use: [
          // CSSファイルを抽出するように MiniCssExtractPlugin のローダーを指定
          {
            loader: MiniCssExtractPlugin.loader,
          },
          // CSS を CommonJS に変換するローダー
          'css-loader',
          // Sass をコンパイルするローダー
          'sass-loader',
        ],
      },
      
      {
        // Babel のローダー用
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  },
};

webpack.config.js を変更後 npm run dev で webpack を実行します。

$ npm run dev  return // webpack を実行 
// または npx webpack --mode=development

> myProject@1.0.0 dev /Applications/MAMP/htdocs/sample/myProject
> webpack --mode development

Hash: 6a8edd55d8e929ca0f21
Version: webpack 4.43.0
Time: 831ms
Built at: 2020/06/05 20:26:08
    Asset      Size  Chunks             Chunk Names
  main.js  5.59 KiB    main  [emitted]  main
style.css  51 bytes    main  [emitted]  main
Entrypoint main = style.css main.js
[./src/index.js] 480 bytes {main} [built]
[./src/modules/foo.js] 75 bytes {main} [built]
[./src/style.scss] 39 bytes {main} [built]
    + 1 hidden module
Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/sass-loader/dist/cjs.js!src/style.scss:
    Entrypoint mini-css-extract-plugin = *
    [./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./src/style.scss] 285 bytes {mini-css-extract-plugin} [built]
        + 1 hidden module

問題なく実行されれば、Sass から CSS に変換された style.css が dist フォルダに出力されます。ブラウザで index.html を確認するとスタイルが適用されているはずです。

webpack guides: MiniCssExtractPlugin

CSS の圧縮 optimize-css-assets-webpack-plugin

CSS は JavaScript ファイルとは異なり、モードを production に指定しても CSS は圧縮(minify)されません。CSS を圧縮して出力するには optimize-css-assets-webpack-plugin というプラグインを使用します。

optimize-css-assets-webpack-plugin を使用するには、まずプラグインをインストールします。

$ npm install optimize-css-assets-webpack-plugin --save-dev return
+ optimize-css-assets-webpack-plugin@5.0.3
added 112 packages from 96 contributors and audited 880 packages in 5.522s

上記のモジュールインストール後の package.json は、optimize-css-assets-webpack-plugin が追加され(18行目)以下のようになります。

package.json (例)
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production",
    "dev": "webpack --mode development"
  },
  "devDependencies": {
    "@babel/core": "^7.10.2",
    "@babel/preset-env": "^7.10.2",
    "babel-loader": "^8.1.0",
    "css-loader": "^3.5.3",
    "mini-css-extract-plugin": "^0.9.0",
    "node-sass": "^4.14.1",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "sass-loader": "^8.0.2",
    "style-loader": "^1.2.1",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11"
  }
}

設定ファイル webpack.config.js を編集します。

インストールしたプラグインを require() で読み込み、optimization プロパティの minimizer プロパティで読み込んだプラグインを指定します。

optimization.minimizer を設定することで、デフォルトの圧縮方法を変更することができます。

但し、minimizer プロパティを指定するとデフォルトの JavaScript の圧縮にも影響があるため、以下では JavaScript の圧縮に使用する TerserPlugin も冒頭(7行目)で読み込んで同時に指定しています(26行目)。

webpack.config.js
const path = require('path');
//mini-css-extract-plugin の読み込み
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
//optimize-css-assets-webpack-plugin の読み込み
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
//JavaScript の圧縮用のプラグイン TerserPlugin の読み込み
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  //プラグインの設定
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'style.css',
    }),
  ],
  //
  optimization: {
    //圧縮方法(圧縮に使うプラグイン)を変更
    minimizer: [
      //JavaScript 用の圧縮プラグイン
      new TerserPlugin({}), 
      //CSS 用の圧縮プラグイン
      new OptimizeCSSAssetsPlugin({})
    ],
  },
  // ローダーの設定
  module: {
    rules: [
      {
        // Sass 用のローダー
        //ローダーの処理対象ファイル(拡張子 .scss や .sass のファイル)
        test: /\.s[ac]ss$/i,
        use: [
          // CSSファイルを抽出するように MiniCssExtractPlugin のローダーを指定
          {
            loader: MiniCssExtractPlugin.loader,
          },
          // CSS を CommonJS に変換するローダー
          'css-loader',
          // Sass をコンパイルするローダー
          'sass-loader',
        ],
      },
      
      {
        // Babel のローダー用
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  },
};

webpack.config.js を変更後 npm run build(または npx webpack --mode=production)で webpack を実行すると、圧縮(minify)された CSS が出力されます。

$ npm run build  return // webpack を production モードで実行
//または npx webpack --mode=production

> myProject@1.0.0 build /Applications/MAMP/htdocs/sample/myProject
> webpack --mode production

Hash: 99653d14684b4c7c7d36
Version: webpack 4.43.0
Time: 1187ms
Built at: 2020/06/06 13:46:42
    Asset      Size  Chunks             Chunk Names
  main.js  1.06 KiB       0  [emitted]  main
style.css  36 bytes       0  [emitted]  main
Entrypoint main = style.css main.js
[0] ./src/style.scss 39 bytes {0} [built]
[1] ./src/index.js + 1 modules 559 bytes {0} [built]
    | ./src/index.js 479 bytes [built]
    | ./src/modules/foo.js 75 bytes [built]
    + 1 hidden module
Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/sass-loader/dist/cjs.js!src/style.scss:
    Entrypoint mini-css-extract-plugin = *
    [0] ./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./src/style.scss 270 bytes {0} [built]
        + 1 hidden module

npm run dev(または npx webpack --mode=development)を実行すると CSS とバンドルされて出力される main.js は圧縮されません。

HtmlWebpackPlugin

HtmlWebpackPlugin は webpack でバンドルされた JavaScript や CSS を表示する HTML を自動的に生成するプラグインです。生成される HTML にはバンドルされた JavaScritp や CSS を読み込む script や link タグが自動的に埋め込まれています。

オプションで webpack で生成される JavaScript や CSS のファイル名の最後にハッシュを追加することができ、簡単にブラウザキャッシュの更新ができます。

また、title や meta タグなどを設定したり、独自の HTML をテンプレートに指定することもできます。

以下は前述の例の表示用の HTML ファイル index.html を削除して、HtmlWebpackPlugin で自動的に HTML ファイルを生成する例です。

以下がファイル構成で、前述の例との違いは index.html がないことです。ファイルの内容はこの時点では前述の例と同じです。

ファイル構成
myProject
├── dist
│   ├── //index.html ★この時点では取り敢えず削除する(コンパイル時に自動的に生成される)
│   └── main.js //ビルドされて出力されるファイル
│   └── style.css  //MiniCssExtractPlugin で出力される CSS
├── node_modules  
├── package-lock.json
├── package.json
├── src
│   ├── index.js  //import 文を使って style.scss と foo.js を読み込む
│   ├── modules
│   │   └── foo.js  //モジュール
│   └── style.scss   //Sass
└── webpack.config.js 

HtmlWebpackPlugin をインストールします。

$ npm install --save-dev html-webpack-plugin  return //インストール
+ html-webpack-plugin@4.3.0
added 57 packages from 122 contributors and audited 768 packages in 9.108s

上記のモジュールインストール後の package.json は、html-webpack-plugin が追加され(16行目)以下のようになります。

package.json
{
  "name": "myProject",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production",
    "dev": "webpack --mode development"
  },
  "devDependencies": {
    "@babel/core": "^7.10.2",
    "@babel/preset-env": "^7.10.2",
    "babel-loader": "^8.1.0",
    "css-loader": "^3.5.3",
    "html-webpack-plugin": "^4.3.0",
    "mini-css-extract-plugin": "^0.9.0",
    "node-sass": "^4.14.1",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "sass-loader": "^8.0.2",
    "style-loader": "^1.2.1",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11"
  }
}

設定ファイル webpack.config.js を編集し、HtmlWebpackPlugin プラグインの設定を追加します。

require() を使ってプラグイン html-webpack-plugin を読み込みます(3行目)。

plugins プロパティに new 演算子で HtmlWebpackPlugin プラグインのインスタンスを生成します。必要に応じて引数にオプションを指定することができます(20行目)。

webpack.config.js
const path = require('path');
//html-webpack-plugin の読み込み
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  //プラグインの設定
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'style.css',
    }),
    //HtmlWebpackPlugin プラグインのインスタンスを生成(指定)
    new HtmlWebpackPlugin(),
  ],
  //圧縮(minify)の設定
  optimization: {
    minimizer: [
      new TerserPlugin({}), 
      new OptimizeCSSAssetsPlugin({})
    ],
  },
  // ローダーの設定
  module: {
    rules: [
      {
        // Sass と CSS のローダー 
        test: /\.s[ac]ss$/i,  //.sass .scss
        use: [
          // CSSファイルを抽出する MiniCssExtractPlugin のローダー  
          {
            loader: MiniCssExtractPlugin.loader,
          },
          // CSS を CommonJS に変換するローダー
          'css-loader',
          // Sass をコンパイルするローダー
          'sass-loader',
        ],
      },
      
      {
        // Babel のローダー
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
              ]
            }
          }
        ]
      }
    ]
  },
};

webpack.config.js を変更後 npm run dev で webpack を実行します。

html-webpack-plugin
$ npm run dev  return // webpack を実行 
// または npx webpack --mode=development

> myProject@1.0.0 dev /Applications/MAMP/htdocs/sample/myProject
> webpack --mode development

Hash: c8717225b0bca1992e7f
Version: webpack 4.43.0
Time: 444ms
Built at: 2020/06/05 20:35:36
     Asset       Size  Chunks             Chunk Names
index.html  265 bytes          [emitted]  // index.html の出力
   main.js   5.59 KiB    main  [emitted]  main
 style.css   51 bytes    main  [emitted]  main
Entrypoint main = style.css main.js
[./src/index.js] 480 bytes {main} [built]
[./src/modules/foo.js] 75 bytes {main} [built]
[./src/style.scss] 39 bytes {main} [built]
    + 1 hidden module
Child HtmlWebpackCompiler:
     1 asset
    Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
       1 module
Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/sass-loader/dist/cjs.js!src/style.scss:
    Entrypoint mini-css-extract-plugin = *
    [./node_modules/css-loader/dist/cjs.js!./node_modules/sass-loader/dist/cjs.js!./src/style.scss] 285 bytes {mini-css-extract-plugin} [built]
        + 1 hidden module
myProject $ 

webpack を実行すると、output プロパティで指定したディレクトリ(dist)に以下のような index.html が自動的に生成され、バンドルされて出力された JavaScript ファイル(main.js)と抽出されて出力された CSS(style.css)が自動的に読み込まれているのが確認できます。

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Webpack App</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="style.css" rel="stylesheet">
  </head>
  <body>
    <script src="main.js"></script>
  </body>
</html>
  • 複数のエントリポイントがある場合、それらは全て HTML ファイルの script タグで読み込まれます。
  • Webpack の出力に CSS アセットがある場合(例:mini-css-extract-plugin で抽出されたCSS)、それらは HTML ファイルの link タグで読み込まれます。
オプション

webpack.config.js の plugins プロパティでオプションを指定することができます。以下は指定できるオプションの一部です。

指定できるオプションの一部抜粋
オプション名 デフォルト値(型) 説明
title Webpack App
(文字列)
HTML の <title> 要素の値
filename 'index.html'
(文字列)
生成する HTML ファイルの名前
template ''(文字列) テンプレートとして使用する HTML ファイルのパスを指定。デフォルトでは src/index.ejs が存在すればそのファイルを使用(template option
favicon ''(文字列) ファビコンのパスを指定
meta {} (オブジェクト) 指定した meta タグを HTML に挿入。
minify モードによる
(真偽値)
production の場合は true でミニファイされ、development の場合は false でミニファイされない
hash false(真偽値) true の場合、含まれている全てのスクリプトと CSS ファイルに一意の webpack コンパイルハッシュを追加します。ブラウザのキャッシュを更新できます。
chunks ?(文字列) 特定の chunk のみを含む場合に指定。例:chunks: ['app']。複数ある場合はカンマ区切りで指定。
excludeChunks ''(文字列) 特定の chunk を除外する場合に指定。例:chunks: ['app']。複数ある場合はカンマ区切りで指定。

new HtmlWebpackPlugin ( { オプション名:値, オプション名:値, ...} ) でオプションを指定します。

filename と title の設定

以下は生成される HTML のファイル名(filename)を html/index.html、title を 'My Project X' に設定する例です。

webpack.config.js 一部抜粋
plugins: [
  //title と filename オプションを指定
  new HtmlWebpackPlugin({
    title: 'My Project X',
    filename: 'html/index.html'
  }),
],

webpack コマンドを実行すると、この例の場合、dist フォルダ内に html フォルダが自動的に作成され、その中に index.html という HTML ファイルが生成されます。

また、生成される HTML ファイルでは link 及び script タグの読み込みのファイルのパスも更新されます。

dist/html/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>My Project X</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="../style.css" rel="stylesheet">
  </head>
  <body>
    <script src="../main.js"></script>
  </body>
</html>

meta の設定

以下は生成される HTML のファイルに meta タグを設定する例です。それぞれの meta タグをオブジェクトの配列で指定できます。

webpack.config.js 一部抜粋
plugins: [
  new HtmlWebpackPlugin({
    title: 'My Project X',
    filename: 'html/index.html',
    //meta オプションを指定
    meta: [
      {'http-equiv': 'X-UA-Compatible', content: 'IE=edge'},
      {'name': 'description', content: 'My First HtmlWebpackPlugin'} 
    ]
  }),
],

以下のように指定した meta タグが追加されます。

dist/html/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>My Project X</title>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"><!-- 追加 -->
    <meta name="description" content="My First HtmlWebpackPlugin"><!-- 追加 -->
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="../style.css" rel="stylesheet">
  </head>
  <body>
    <script src="../main.js"></script>
  </body>
</html>

hash の設定

オプションに hash: true を指定すると、webpack で生成される JavaScript や CSS のファイル名の最後に ? に続けてハッシュを追加することができます。

webpack.config.js 一部抜粋
plugins: [
  new HtmlWebpackPlugin({
    title: 'My Project X',
    filename: 'html/index.html',
    meta: [
      {'http-equiv': 'X-UA-Compatible', content: 'IE=edge'},
      {'name': 'description', content: 'My First HtmlWebpackPlugin'} 
    ],
    //Hash の値をファイル名に追加
    hash: true
  }),
],

以下のようにファイル名の最後に webpack が生成する Hash の値が付加されます。

ビルドする際に差分がない場合は、Hash の値は変わりません。

dist/html/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>My Project X</title>
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- webpack が生成する Hash の値が末尾に追加される -->
    <link href="../style.css?5b7cf7765e3aeec5a863" rel="stylesheet">
  </head>
  <body>
    <!-- webpack が生成する Hash の値が末尾に追加される -->
    <script src="../main.js?5b7cf7765e3aeec5a863"></script>
  </body>
</html>

template の設定

自動的に生成される HTML(default template)の代わりに独自のテンプレートを用意して使用することもできます。

デフォルトでは lodash の記述方法が使えます(lodash template)。

また、webpack.config.js で設定したオプションの値を「htmlWebpackPlugin.options.オプション名 」でテンプレートで参照することができます。

独自のテンプレートを使用するには webpack.config.js のオプション template でテンプレートとして使用する HTML ファイルのパスを指定します。

以下は独自のテンプレートを作成して使用する例です。

src ディレクトリ内に template ディレクトリを新たに作成して、その中にテンプレート用のファイル index.html を作成します。

ファイル構成
myProject
├── dist
│   ├── html
│   │   └── index.html //自動的に生成されるファイル
│   ├── main.js
│   └── style.css
├── node_modules  
├── package-lock.json
├── package.json
├── src
│   ├── index.js  
│   ├── modules
│   │   └── foo.js  
│   ├── style.scss  
│   └── template  // 追加したディレクトリ
│       └── index.html // テンプレートファイル
└── webpack.config.js 

また、テンプレートファイルで使用するためのオプション h1 を設定しています。この値はテンプレートで htmlWebpackPlugin.options.h1 で参照することができます。

webpack.config.js 一部抜粋
plugins: [
    //
    new HtmlWebpackPlugin({
      title: 'My Template X',
      //生成する HTML ファイル
      filename: 'html/index.html',
      hash: true,
      // テンプレートで使用するファイルのパスを指定
      template: 'src/template/index.html',
      // テンプレートで使用する変数 h1 を設定
      h1: 'Heading Title H1'
    }),
  ],

以下は作成したテンプレート src/template/index.html です。

webpack が出力する JavaScritp や CSS は自動的に読み込まれるので記述する必要はありません。

<%= htmlWebpackPlugin.options.title %> は,options.title の値(この例では My Template X)を出力することを示します。

src/template/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title><%= htmlWebpackPlugin.options.title %></title>
    <meta name="description" content="My First HtmlWebpackPlugin Template">
  </head>
  <body>
    <h1><%= htmlWebpackPlugin.options.h1 %></h1>
  </body>
</html>

webpack コマンドを実行してコンパイルすると、以下のような HTML ファイル(html/index.html)が生成されます。

dist/html/index.html
<!DOCTYPE html>
  <html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>My Template X</title>
    <meta name="description" content="My First HtmlWebpackPlugin Template">
    <link href="../style.css?ea09d11809982cbebd49" rel="stylesheet">
  </head>
  <body>
    <h1>Heading Title H1</h1>
    <script src="../main.js?ea09d11809982cbebd49"></script>
  </body>
</html>
複数のエントリポイント

複数のエントリポイントがある場合、webpack によりバンドルされた JavaScritp は全て HtmlWebpackPlugin により生成される1つの HTML ファイルの script タグに含まれます。

例えば、以下のように2つのエントリポイントがある場合、webpack により output で指定した dist/js に2つの JavaScritp(one.bundle.js と two.bundle.js)が生成されます。

また、HtmlWebpackPlugin により output で指定した dist/js に1つの HTML ファイル(index.html)が生成され、その script タグで one.bundle.js と two.bundle.js が読み込まれます。

webpack.config.js
const path = require('path');
//html-webpack-plugin の読み込み
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  //2つのエントリポイントを設定
  entry: {
    one: './src/one.js',
    two: './src/two.js',
  },
  //出力先の設定
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist/js'),
  },
  //プラグインの設定
  plugins: [
    //HtmlWebpackPlugin プラグインのインスタンスを指定
    new HtmlWebpackPlugin({}),
  ],
};

以下は上記の場合に、HtmlWebpackPlugin により出力される index.html の例です。2つの JavaScritp が script タグで読み込まれています。

dist/js/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Webpack App</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
  <body>
  <!-- 2つの JavaScritp が読み込まれる -->
  <script src="one.bundle.js"></script>
  <script src="two.bundle.js"></script></body>
</html>

複数の HTML を生成

複数の HTML ファイルを生成するには、plugins の配列でプラグインを複数回宣言します。

webpack guides: Generating Multiple HTML Files

以下は、chunks オプションを使って、2つの HTML を生成する例です。

また、出力先は output プロパティにより dist/js に指定されているため、path.resolve() を使って dist/html に変更しています。

webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  //2つのエントリポイントを設定
  entry: {
    one: './src/one.js',
    two: './src/two.js',
  },
  //出力先の設定
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist/js'),
  },
  //プラグインの設定(plugins の配列)
  plugins: [
    //one.bundle.js のみを読み込む HTML 
    new HtmlWebpackPlugin({
      chunks: ['one'],
      filename: path.resolve(__dirname, 'dist/html/one.html'),
    }),
    //two.bundle.js のみを読み込む HTML 
    new HtmlWebpackPlugin({
      chunks: ['two'],
      filename: path.resolve(__dirname, 'dist/html/two.html'),
    })
  ],
};

上記の場合以下のような2つの HTML が生成されます。

dist/html/one.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Webpack App</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
  <body>
  <!-- 指定した1つの JavaScritp が読み込まれる -->
  <script src="../js/one.bundle.js"></script>
  </body>
</html>
dist/html/two.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Webpack App</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
  <body>
  <!-- 指定した1つの JavaScritp が読み込まれる -->
  <script src="../js/two.bundle.js"></script>
  </body>
</html>

webpack guides: HtmlWebpackPlugin

splitChunks

複数のエントリポイントで共通のモジュールを使っている場合、その共通のモジュールをそれぞれのファイルにバンドルすると、全体のファイルサイズが大きくなってしまいます。

optimization プロパティの splitChunks プロパティは、複数のエントリポイントで共通のモジュールを使っている場合に、その共通のモジュールだけを別のファイル(チャンク/chunk)として分離するための設定です(split:分割するという意味なので split Chunks はチャンクを分割するというような意味)。

webpack の version 4 以上では、この機能を使うための SplitChunksPlugin が提供されていてます。

以下は2つのエントリポイントで共通のモジュール(jquery)を使用する例です。

以下の例では myProject2 という名前のディレクトリを作成し、webpack をインストールします。

プロジェクトのディレクトリ(myProject2)に移動して、共通のモジュールとして使う jquery をインストールします。

jQuery のインストール
$ npm install jquery  return

+ jquery@3.5.1
added 1 package from 1 contributor and audited 415 packages in 1.544s

また、表示用の HTML を自動的に生成するために HtmlWebpackPlugin もインストールします。

$ npm install --save-dev html-webpack-plugin  return
        
+ html-webpack-plugin@4.3.0
added 63 packages from 130 contributors and audited 478 packages in 23.982s

package.json の scripts フィールドに webpack を実行する npm script を設定しておきます。この時点での package.json は以下のようになっています。

package.json
{
  "name": "myProject2",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "build": "webpack --mode production",
    "dev": "webpack --mode development"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "jquery": "^3.5.1"
  },
  "devDependencies": {
    "html-webpack-plugin": "^4.3.0",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11"
  }
}

以下のような構成を用意します。

ファイル構成
myProject2
├── node_modules    
├── package-lock.json
├── package.json
├── src
│   ├── modules
│   │   ├── bar.js
│   │   └── foo.js
│   ├── one.js
│   └── two.js
└── webpack.config.js

以下の JavaScript ファイルを作成します。

src/modules/bar.js(エントリポイントで読み込むモジュール)
// export 文を使って文字列を返す関数 heading を定義
export const heading = ()  => 'This is the heading by bar.js!';
src/modules/foo.js(エントリポイントで読み込むモジュール)
// export 文を使って文字列を関数 content を定義
export const content = ()  => 'This is the content by foo.js!';
src/one.js(エントリポイント)
//jquery をインポート(読み込んで $ として使用)
import $ from 'jquery';
//foo.js をインポート
import { content } from './modules/foo.js';
//bar.js をインポート
import { heading } from './modules/bar.js';
 
function component() {
  //div 要素を生成
  const element = document.createElement('div');
  // インポートした関数の実行結果を使って div 要素の HTML を作成
  element.innerHTML = `<h1>ONE: ${heading()}</h1>
<p>${content()}</p>`
 
  return element;
}
// jQuery を使って body 要素に component() の実行結果を設定
$('body').html(component());
// jQuery を使って div 要素をアニメーション表示
$('div').fadeOut(1000).fadeIn(1000);
src/two.js(エントリポイント)
//内容は one.js とほとんど同じ(違いは11行目の文字 TWO のみ)
//jquery をインポート(読み込んで $ として使用)
import $ from 'jquery';
//foo.js の関数 content をインポート
import { content } from './modules/foo.js';
//bar.js の関数 heading をインポート
import { heading } from './modules/bar.js';
 
function component() {
  const element = document.createElement('div');
  element.innerHTML = `<h1>TWO: ${heading()}</h1>
<p>${content()}</p>`
  return element;
}
 
$('body').html(component());
$('div').fadeOut(1000).fadeIn(1000);

以下のような webpack.config.js を作成します。output プロパティでは、[name] を使ってファイル名を設定しています([name] にはエントリポイントのファイル名が入ります)。

webpack.config.js
const path = require('path');
//html-webpack-plugin の読み込み
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  //2つのエントリポイントを設定
  entry: {
    one: './src/one.js',
    two: './src/two.js',
  },
  //出力先の設定
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist/js'),
  },
  //プラグインの設定
  plugins: [
    //one.bundle.js のみを読み込む HTML を自動的に生成
    new HtmlWebpackPlugin({
      chunks: ['one'],
      //出力先を output で指定した dist/js から dist/html に変更及びファイル名を指定
      filename: path.resolve(__dirname, 'dist/html/one.html'),
    }),
    //two.bundle.js のみを読み込む HTML を自動的に生成
    new HtmlWebpackPlugin({
      chunks: ['two'],
      filename: path.resolve(__dirname, 'dist/html/two.html'),
    })
  ],
};

この状態で webpack コマンドを実行すると、それぞれのエントリポイントに対しバンドルされた JavaScript が生成されます。また HtmlWebpackPlugin により2つの HTML ファイルが生成されます。

$ npm run dev  return //webpack コマンドを実行
// または npx webpack --mode=development

> myProject2@1.0.0 dev /Applications/MAMP/htdocs/sample/myProject2
> webpack --mode development

Hash: 9bfa90fb0715bb9222c7
Version: webpack 4.43.0
Time: 203ms
Built at: 2020/06/08 13:08:00
           Asset       Size  Chunks             Chunk Names
../html/one.html  237 bytes          [emitted]      // 自動生成された HTML
../html/two.html  237 bytes          [emitted]      // 自動生成された HTML
   one.bundle.js    324 KiB     one  [emitted]  one  //バンドルされた JavaScript
   two.bundle.js    324 KiB     two  [emitted]  two  //バンドルされた JavaScript
Entrypoint one = one.bundle.js
Entrypoint two = two.bundle.js
[./src/modules/bar.js] 150 bytes {one} {two} [built]
[./src/modules/foo.js] 200 bytes {one} {two} [built]
[./src/one.js] 518 bytes {one} [built]
[./src/two.js] 521 bytes {two} [built]
    + 1 hidden module
Child HtmlWebpackCompiler:
     1 asset
    Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
       1 module

バンドルされた2つのファイル(one.bundle.js と two.bundle.js)を確認すると、共通のモジュールがそれぞれにバンドルされていてファイルサイズが大きくなっています。

myProject2 
├── dist
│   ├── html
│   │   ├── one.html  //HtmlWebpackPlugin により生成されたファイル
│   │   └── two.html  //HtmlWebpackPlugin により生成されたファイル
│   └── js
│       ├── one.bundle.js  //バンドルされたファイル
│       └── two.bundle.js  //バンドルされたファイル
├── package-lock.json
├── package.json
├── src
│   ├── modules
│   │   ├── bar.js
│   │   └── foo.js
│   ├── one.js
│   └── two.js
└── webpack.config.js

splitChunks を設定して共通のモジュールを分離

以下のように webpack.config.js に optimization.splitChunks の設定(31〜39行目)を追加して、エントリポイントで共通のモジュールを別のファイル(チャンク)として分離すると、合計のファイルサイズを小さくすることができます。

以下では、共通のモジュールを分離したファイル(chunk)の名前 name を vendor としているので、分離して出力されるチャンクのファイル名は vendor.bundle.js になります。

chunks は対象とするチャンク(chunk)に含めるモジュールの種類を指定する項目で、all, async, initial を指定することができます。

webpack.config.js
const path = require('path');
//html-webpack-plugin の読み込み
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  //2つのエントリポイントを設定
  entry: {
    one: './src/one.js',
    two: './src/two.js',
  },
  //出力先の設定
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist/js'),
  },
  //プラグインの設定
  plugins: [
    //one.bundle.js のみを読み込む HTML を自動的に生成
    new HtmlWebpackPlugin({
      chunks: ['one'],
      //出力先を output で指定した dist/js から dist/html に変更及びファイル名を指定
      filename: path.resolve(__dirname, 'dist/html/one.html'),
    }),
    //two.bundle.js のみを読み込む HTML を自動的に生成
    new HtmlWebpackPlugin({
      chunks: ['two'],
      filename: path.resolve(__dirname, 'dist/html/two.html'),
    })
  ],
  //optimization の設定を追加
  optimization: {
    //optimization.splitChunks の設定
    splitChunks: {
      // 分離されて生成される chunk の名前(任意の名前)
      name: 'vendor',
      // 対象とするチャンク(chunk)に含めるモジュールの種類
      chunks: 'initial',   // または 'all'
    }
  },
};

.splitChunks の設定を追加して webpack コマンドを実行すると、共通のモジュールが別のチャンクに分割されて出力されます。

name を vendor と設定したので、分割して出力されるチャンクのファイル名は vendor.bundle.js になっていて、one.bundle.js と two.bundle.js のファイルサイズは小さくなっています。

$ npm run dev return //webpack コマンドを実行
// または npx webpack --mode=development

> myProject2@1.0.0 dev /Applications/MAMP/htdocs/sample/myProject2
> webpack --mode development

Hash: a5142f644a12d2333d35
Version: webpack 4.43.0
Time: 200ms
Built at: 2020/06/08 14:56:37
           Asset       Size  Chunks             Chunk Names
../html/one.html  283 bytes          [emitted]  
../html/two.html  283 bytes          [emitted]  
   one.bundle.js      9 KiB     one  [emitted]  one  //バンドルされた JavaScript
   two.bundle.js      9 KiB     two  [emitted]  two  //バンドルされた JavaScript
vendor.bundle.js    318 KiB  vendor  [emitted]  vendor  // 共通のモジュールのバンドル
Entrypoint one = vendor.bundle.js one.bundle.js //エントリーポイント one にバンドルされたファイル
Entrypoint two = vendor.bundle.js two.bundle.js //エントリーポイント two にバンドルされたファイル
[./src/modules/bar.js] 150 bytes {one} {two} [built]
[./src/modules/foo.js] 200 bytes {one} {two} [built]
[./src/one.js] 518 bytes {one} [built]
[./src/two.js] 521 bytes {two} [built]
    + 1 hidden module
Child HtmlWebpackCompiler:
     1 asset
    Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
       1 module

また、HtmlWebpackPlugin により自動的に生成される HTML の script 要素は以下のように2つのチャンク(vendor.bundle.js と one.bundle.js)を読み込むように変更されています。

one.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Webpack App</title>
  <meta name="viewport" content="width=device-width, initial-scale=1"></head>
  <body>
  <script src="../js/vendor.bundle.js"></script><!-- 分離されたチャンク --> 
  <script src="../js/one.bundle.js"></script>
  </body>
</html>
two.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Webpack App</title>
  <meta name="viewport" content="width=device-width, initial-scale=1"></head>
  <body>
  <script src="../js/vendor.bundle.js"></script><!-- 分離されたチャンク --> 
  <script src="../js/two.bundle.js"></script>
  </body>
</html>
myProject2
├── dist
│   ├── html
│   │   ├── one.html
│   │   └── two.html
│   └── js
│       ├── one.bundle.js
│       ├── two.bundle.js
│       └── vendor.bundle.js  //共通のモジュールが分割されたバンドルファイル(チャンク)
├── package-lock.json
├── package.json
├── src
│   ├── modules
│   │   ├── bar.js
│   │   └── foo.js
│   ├── one.js
│   └── two.js
└── webpack.config.js

オプション

前述の splitChunks の設定は以下のようなものでしたが、この場合、共通のモジュールは全て分離して出力されるので(サイズなどのデフォルトの設定の条件にもよりますが)、特定のファイルを分離したい場合などは、cacheGroups というオプションを指定することができます。

webpack.config.js 抜粋
optimization: {
  splitChunks: {
    // 分割されて生成される chunk の名前(任意の名前)
    name: 'vendor',
    //対象とする chunk に含めるモジュールの種類
    chunks: 'initial',
  }
},

cacheGroups を使用すると、条件に基づいてチャンクを作成することができます。cacheGroups はオブジェクトで、キー(key)はチャンクの名前、値(value)はそのチャンクの構成を指定します。

初期状態(デフォルト)では、defaultVendors と default というキーの cacheGroups が設定されています。

以下は cacheGroups を設定して、node_modules ディレクトリ配下の jquery モジュールを分離する場合の例です。

test は対象のファイルを正規表現で指定することができます。パスセパレータはクロスプラットフォームに対応するため [\\/] を使用しています。

webpack.config.js 抜粋
optimization: {
    //optimization.splitChunks の設定
    splitChunks: {
      //特定のファイルを分離する場合などに設定
      cacheGroups: {
        // vendor 以外の任意の名前を設定可能(cacheGroups のキー)
        vendor: {
          // node_modules 配下の jquery モジュールを分離する対象とする
          test: /[\\/]node_modules[\\/]jquery[\\/]/,
          // 分離されて生成される chunk の名前(cacheGroups のキーを上書き)
          name: 'vendor',
          //対象とする chunk に含めるモジュールの種類 
          chunks: 'initial'  // または 'all'
        }
      }
    }
  },

以下は SplitChunksPlugin に記載されているサンプルで、react と react-dom を別のチャンクに分離します。

webpack.config.js 抜粋
module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'vendor',
          chunks: 'all',
        }
      }
    }
  }
};
デフォルトの設定

以下が SplitChunksPlugin のデフォルトの設定です。必要に応じて変更することができます。

webpack.config.js
//SplitChunksPlugin のデフォルトの設定(初期値)
module.exports = {
  //...
  optimization: {
    splitChunks: {  //default behavior of the SplitChunksPlugin
      chunks: 'async',
      minSize: 30000,
      minRemainingSize: 0,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 6,
      maxInitialRequests: 4,
      automaticNameDelimiter: '~',
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};

※上記 cacheGroup の test フィールド(15行目)でパスセパレータを表すのに [\\/] を使用しているのはクロスプラットフォームに対応するためです(ファイルパスが webpack によって処理される場合、Unix 系では常に /、Windowsでは \ が含まれるため)。

上記のデフォルトの設定があるため、前述の例の場合、以下のように設定すると jquery のモジュールは minSize の 30k より大きく、エントリーポイントが2つなので minChunks を満たすので、共通のモジュールのチャンクとして分離されて出力されます。

optimization: {
  splitChunks: {
    //対象とする chunk に含めるモジュールの種類
    chunks: 'initial',    // または 'all'
  }
},

vendors~one~two.bundle.js という名前で分離されて出力されているのが確認できます。

$ npm run dev return //webpack コマンドを実行
// または npx webpack --mode=development

> myProject2@1.0.0 dev /Applications/MAMP/htdocs/sample/myProject2
> webpack --mode development

Hash: 7bd7f421f4a1ddc20748
Version: webpack 4.43.0
Time: 202ms
Built at: 2020/06/08 17:13:46
                    Asset       Size           Chunks             Chunk Names
         ../html/one.html  292 bytes                   [emitted]  
         ../html/two.html  292 bytes                   [emitted]  
            one.bundle.js   9.01 KiB              one  [emitted]  one
            two.bundle.js   9.01 KiB              two  [emitted]  two
vendors~one~two.bundle.js    318 KiB  vendors~one~two  [emitted]  vendors~one~two
Entrypoint one = vendors~one~two.bundle.js one.bundle.js
Entrypoint two = vendors~one~two.bundle.js two.bundle.js
[./src/modules/bar.js] 150 bytes {one} {two} [built]
[./src/modules/foo.js] 200 bytes {one} {two} [built]
[./src/one.js] 518 bytes {one} [built]
[./src/two.js] 521 bytes {two} [built]
    + 1 hidden module
Child HtmlWebpackCompiler:
     1 asset
    Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0

以下はオプションの一部抜粋です。

オプション 初期値 説明
chunks 'async' 対象とする chunk に含めるモジュールの種類。指定可能な値は all, async, initial または関数で指定。
  • all:全てのモジュール
  • async:webpack の import() でインポートされたモジュール
  • initial:静的にインポートされたモジュール
minSize 30000 生成されるチャンクの最小サイズをバイト単位で指定
minChunks 1 分割する場合に必要な共有されているエントリーポイントの最小数
name 分割されたチャンクの名前。true を指定すると、chunk と cacheGroups のキーに基づいて名前が自動的に生成されます。
enforce false true を指定すると、minSize, minChunks, maxAsyncRequests, maxInitialRequests の設定を無視します。

webpack の import()

webpack の import() 関数はモジュールを非同期(動的)に読み込みます。

以下は import() を使ってインポートする例です。

以下を src/modules/ に作成します。

src/modules/async.js
// async.js
export default 'I am ASYNC!';
export const HELLO = 'Hello World!';

エントリポイント(one.js)で上記の async.js を import() を使ってインポートします。import()関数はモジュールを非同期に読み込み promise を返し、成功すればそのモジュールがエクスポートしているものを返します。

src/one.js(エントリポイント)
// import async.js (追加)
import( './modules/async.js' ).then( ( data ) => {
    console.log( data );
} );

//jquery をインポート(読み込んで $ として使用)
import $ from 'jquery';
//foo.js の関数 content をインポート
import { content } from './modules/foo.js';
//bar.js の関数 heading をインポート
import { heading } from './modules/bar.js';

function component() {

  const element = document.createElement('div');
 
  element.innerHTML = `<h1>ONE: ${heading()}</h1>
<p>${content()}</p>`
 
  return element;
}
 
$('body').html(component());
$('div').fadeOut(1000).fadeIn(1000);

import() を使うには babel をインストールします。また babel はそのままでは webpack の import() を理解できないので @babel/plugin-syntax-dynamic-import もインストールする必要があります。

$ npm install babel-loader @babel/core @babel/preset-env --save-dev

$ npm install  @babel/plugin-syntax-dynamic-import --save-dev

以下が package.json の例です。

package.json
{
  "name": "myProject2",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "build": "webpack --mode production",
    "dev": "webpack --mode development"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "jquery": "^3.5.1"
  },
  "devDependencies": {
    "@babel/core": "^7.10.2",
    "@babel/plugin-syntax-dynamic-import": "^7.8.3",
    "@babel/preset-env": "^7.10.2",
    "babel-loader": "^8.1.0",
    "html-webpack-plugin": "^4.3.0",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11"
  }
}

webpack.config.js を以下のように編集します(ローダーの追加:49〜77行目)。

webpack.config.js
const path = require('path');
//html-webpack-plugin の読み込み
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  //2つのエントリポイントを設定
  entry: {
    one: './src/one.js',
    two: './src/two.js',
  },
  //出力先の設定
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist/js'),
  },
  //プラグインの設定
  plugins: [
    //one.bundle.js のみを読み込む HTML を自動的に生成
    new HtmlWebpackPlugin({
      chunks: ['one'],
      //出力先を output で指定した dist/js から dist/html に変更及びファイル名を指定
      filename: path.resolve(__dirname, 'dist/html/one.html'),
    }),
    //two.bundle.js のみを読み込む HTML を自動的に生成
    new HtmlWebpackPlugin({
      chunks: ['two'],
      filename: path.resolve(__dirname, 'dist/html/two.html'),
    })
  ],

  //optimization の設定を追加
  optimization: {
    //optimization.splitChunks の設定
    splitChunks: {
      cacheGroups: {
        // vendor 以外の任意の名前を設定可能
        vendor: {
          // node_modules 配下のモジュールを分離する対象とする
          test: /[\\/]node_modules[\\/]/,
          // 分離されて生成される chunk の名前(任意の名前)
          name: 'vendor',
          // 対象とする chunk に含めるモジュールの種類  
          chunks: 'initial'  // または 'all'
        }
      }
    }
  },
  
  module: {
    rules: [
      {
        // ローダーの処理対象ファイル(拡張子 .js のファイルを対象)
        test: /\.js$/,
        // ローダーの処理対象から外すディレクトリ
        exclude: /node_modules/,
        // 処理対象のファイルに対する処理を指定
        use: [
          {
            // 利用するローダーを指定
            loader: 'babel-loader',
            // ローダー(babel-loader)のオプションを指定
            options: {
              // プリセットを指定
              presets: [
                // targets を指定していないので、一律に ES5 の構文に変換
                '@babel/preset-env',
              ],
              //plugin-syntax-dynamic-import を使用するための設定
              plugins: [
                '@babel/plugin-syntax-dynamic-import'
              ]
            }
          }
        ]
      }
    ]
  }
};

webpack コマンドを実行すると、chunks が async のチャンク 0.bundle.js が生成されているのが確認できます。

$ npm run dev return //webpack コマンドを実行
// または npx webpack --mode=development

> myProject2@1.0.0 dev /Applications/MAMP/htdocs/sample/myProject2
> webpack --mode development

Hash: f75aabf7ff11595cbb01
Version: webpack 4.43.0
Time: 704ms
Built at: 2020/06/09 10:46:12
           Asset       Size  Chunks             Chunk Names
../html/one.html  283 bytes          [emitted]  
../html/two.html  283 bytes          [emitted]  
     0.bundle.js  692 bytes       0  [emitted]  //新たに生成(分割)されたチャンク
   one.bundle.js   12.1 KiB     one  [emitted]  one
   two.bundle.js   9.07 KiB     two  [emitted]  two
vendor.bundle.js    318 KiB  vendor  [emitted]  vendor
Entrypoint one = vendor.bundle.js one.bundle.js
Entrypoint two = vendor.bundle.js two.bundle.js
[./src/modules/async.js] 76 bytes {0} [built]
[./src/modules/bar.js] 175 bytes {one} {two} [built]
[./src/modules/foo.js] 225 bytes {one} {two} [built]
[./src/one.js] 643 bytes {one} [built]
[./src/two.js] 533 bytes {two} [built]
    + 1 hidden module
Child HtmlWebpackCompiler:
     1 asset
    Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
       1 module
0.bundle.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{

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

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"HELLO\", function() { return HELLO; });\n// async.js\n/* harmony default export */ __webpack_exports__[\"default\"] = ('I am ASYNC!');\nvar HELLO = 'Hello World!';\n\n//# sourceURL=webpack:///./src/modules/async.js?");

/***/ })

}]);

one.html のコンソールの出力を確認するため script タグを追加して 0.bundle.js を読み込みます。

dist/html/one.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Webpack App</title>
  <meta name="viewport" content="width=device-width, initial-scale=1"></head>
  <body>
    <script src="../js/vendor.bundle.js"></script>
    <script src="../js/one.bundle.js"></script>
    <script src="../js/0.bundle.js"></script><!-- 追加 -->
  </body>
</html>

以下は dist/html/one.html のコンソールの出力です。

参考サイト

webpack-dev-server

webpack では webpack-dev-server を使って簡単に開発用サーバを立ち上げることができます。

以下は、基本的な webpack-dev-server の設定方法と使い方です。すでに webpack はインストールされていることを前提にしています。(webpack のインストール

この例のディレクトリ構成とそれぞれのファイルは以下のようになっています。

myProject4
├── dist
│   ├── html
│   │   └── index.html
│   └── main.js  //出力ファイル
├── package-lock.json
├── package.json
├── src
│   ├── index.js  //エントリポイント
│   └── modules
│       └── foo.js
└── webpack.config.js
foo.js
export const greet = ()  => 'Hello webpack';
index.js
import { greet } from './modules/foo.js';

const element = document.createElement('div');
element.innerHTML = '<p>' + greet() + '!</p>';

document.body.appendChild(element);
index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Hello Webpack</title>
  </head>
  <body>
    <script src="../main.js"></script>
  </body>
</html>
package.json
{
  "name": "myProject4",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production",
    "dev": "webpack --mode development"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.12"
  }
}
webpack.config.js
const path = require('path');  
 
module.exports = {
  entry: './src/index.js',  //エントリポイント
  output: {  //出力先
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

この場合、ファイルに変更をしてそれを確認するには webpack コマンドを実行してブラウザで確認する必要があります。

または、以下のように実行して watch オプションを利用すればファイルが変更されると自動的に webpack コマンドを実行するようにはできますが、ブラウザには反映されません。

$ npx webpack --watch --mode development  //watch モードで実行

インストール

プロジェクトのディレクトリで以下を実行して webpack-dev-server をローカルインストールします。

--save-dev(または -D)は開発環境で使うパッケージに指定するオプションです。

$ npm install --save-dev webpack-dev-server   return
・・・中略・・・
+ webpack-dev-server@3.11.0
added 181 packages from 166 contributors and audited 577 packages in 8.129s

取り敢えず何も設定をしないまま webpack-dev-server を npx コマンドで起動してみます。

$ npx webpack-dev-server   return
ℹ 「wds」: Project is running at http://localhost:8080/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from /Applications/MAMP/htdocs/sample/myProject4
ℹ 「wdm」: Hash: 7181d51ec937fc8308a4
Version: webpack 4.43.0
Time: 236ms
Built at: 2020/06/22 10:55:28
  Asset     Size  Chunks             Chunk Names
main.js  363 KiB    main  [emitted]  main
Entrypoint main = main.js
[0] multi (webpack)-dev-server/client?http://localhost:8080 ./src/index.js 40 bytes {main} [built]
[./node_modules/ansi-html/index.js] 4.16 KiB {main} [built]
[./node_modules/html-entities/lib/index.js] 449 bytes {main} [built]

・・・中略・・・

[./src/index.js] 673 bytes {main} [built]
[./src/modules/foo.js] 45 bytes {main} [built]
    + 19 hidden modules
ℹ 「wdm」: Compiled successfully.

上記の2行目に「Project is running at http://localhost:8080/」と表示されるので、その URL にアクセスすると以下のようにプロジェクトのディレクトリのリストが表示されます。

この例の場合、index.html は dist/html/ にあるのでアドレスバーに http://localhost:8080/dist/html/ と入力するか dist フォルダのアイコンをクリックしてたどると index.html が表示されます。

このままではファイルを変更しても反映されず、また使い勝手が悪いのでオプションを設定する必要があります。

control + c でサーバを終了します。

webpack-dev-server

オプション

webpack-dev-server のオプションを設定する前に、コマンドラインで簡単にサーバを起動できるように package.json の scripts フィールドnpm scripts を設定します(必須ではありません)。

以下の例では start:dev に webpack-dev-server を設定(10行目)しているので、webpack-dev-server の起動は $ npm run start:dev と入力して実行できます。

package.json
{
  "name": "myProject4",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production",
    "dev": "webpack --mode development",
    "start:dev": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.0"
  }
}

オプションはコマンド実行時に --option のように指定するか、webpack の設定ファイル webpack.config.js に記述します。

オプションの一部抜粋
オプション 説明
contentBase 公開するリソースのルートディレクトリ(ドキュメントルート)を指定(最低限このオプションを指定)
open ブラウザを自動的に起動
port デフォルトのポート番号を変更
watchContentBase ルートディレクトリのファイルを監視
writeToDisk バンドルされたファイルを出力

webpack guides: DevServer

contentBase

少なくとも contentBase オプションで公開するリソースのルートディレクトリ(ドキュメントルート)を指定します。このオプションを指定することで、バンドル対象のファイルの更新が即座に反映されるようになります。

この例の場合、公開するファイル index.html は dist/html/ にあるので、contentBase に dist/html を指定します。

コマンド実行時に指定する場合は、以下のように指定することができます。

$ npx webpack-dev-server --content-base dist/html
myProject4
├── dist
│   ├── html
│   │   └── index.html  //公開するファイル 
│   └── main.js  //バンドルされて出力されるファイル
├── package-lock.json
├── package.json
├── src
│   ├── index.js  //エントリポイント
│   └── modules
│       └── foo.js
└── webpack.config.js

以下は webpack.config.js に設定を記述する例です。

webpack-dev-server の設定は devServer に記述します。公開するファイルが dist 直下の場合は dist/html の代わりに dist と指定します。環境に合わせて設定します。

webpack.config.js
const path = require('path');  //path モジュールの読み込み
 
module.exports = {
  entry: './src/index.js',  //エントリポイント
  output: {  //出力先
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  //webpack-dev-server の設定
  devServer: {
    //ルートディレクトリの指定
    contentBase: path.join(__dirname, 'dist/html'),
  }
};

webpack.config.js に上記の設定を追加して 以下を実行してブラウザで確認すると、例えば index.js や foo.js を編集して保存した時点でその変更がブラウザに自動的に反映されます。

$ npm run start:dev   return  //npm scripts
//または $ npx webpack-dev-server 

> myProject4@1.0.0 start:dev /Applications/MAMP/htdocs/sample/myProject4
> webpack-dev-server

ℹ 「wds」: Project is running at http://localhost:8080/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from /Applications/MAMP/htdocs/sample/myProject4/dist/html
ℹ 「wdm」: wait until bundle finished: /
ℹ 「wdm」: Hash: 3f5f44830819b8618e80
Version: webpack 4.43.0
Time: 275ms
Built at: 2020/06/22 16:07:22
  Asset     Size  Chunks             Chunk Names
main.js  363 KiB    main  [emitted]  main
Entrypoint main = main.js
・・・以下省略・・・

ターミナルの出力にある http://localhost:8080/ で公開ファイルにアクセスできます。

open ブラウザを自動的に起動

open オプションを指定すると、サーバー起動時にブラウザを自動的に起動することができます。

以下はコマンドラインで指定して実行する例です。

$ npx webpack-dev-server --open   return

以下は webpack.config.js に設定する例で、open: true を指定します。

webpack.config.js
const path = require('path');  //path モジュールの読み込み
 
module.exports = {
  entry: './src/index.js',  //エントリポイント
  output: {  //出力先
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  //webpack-dev-server の設定
  devServer: {
    //ルートディレクトリの指定
    contentBase: path.join(__dirname, 'dist/html'),
    //サーバー起動時にブラウザを自動的に起動
    open: true
  }
};

port ポート番号の変更

port オプションを指定するとデフォルトのポート(8080)を変更することができます

以下は webpack.config.js でポート番号を 3000 に変更する例です。以下の場合、アクセスする URL は「http://localhost:3000/」のようになります。

webpack.config.js
const path = require('path');  //path モジュールの読み込み
 
module.exports = {
  entry: './src/index.js',  //エントリポイント
  output: {  //出力先
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  //webpack-dev-server の設定
  devServer: {
    //ルートディレクトリの指定
    contentBase: path.join(__dirname, 'dist/html'),
    //サーバー起動時にブラウザを自動的に起動
    open: true,
    // ポート番号を変更
    port: 3000
  }
};

watchContentBase ルートディレクトリのファイルを監視

watchContentBase オプションを使うと、ルートディレクトリに配置した HTML や CSS を監視して更新があれば自動的にブラウザをリロードして変更を反映させることができます。

以下のように watchContentBase: true を指定すると、dist/html/index.html を変更するとのブラウザがリロードされ変更が自動的に反映されるようになります。

webpack.config.js
const path = require('path');  //path モジュールの読み込み
 
module.exports = {
  entry: './src/index.js',  //エントリポイント
  output: {  //出力先
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  //webpack-dev-server の設定
  devServer: {
    //ルートディレクトリの指定
    contentBase: path.join(__dirname, 'dist/html'),
    //サーバー起動時にブラウザを自動的に起動
    open: true,
    // ルートディレクトリのファイルを監視
    watchContentBase: true
  }
};

バンドルされたファイルは出力されない

webpack-dev-server を実行してファイルを編集して保存すると、サーバ上でビルドされてブラウザにその変更が即座に反映されます。

但し、webpack-dev-server で生成された(バンドルされた)ファイルはメモリ上に保存されていて、デフォルトでは実際には出力されません。

例えば、以下の構成で webpack-dev-server を実行して foo.js を編集して保存するとその変更はブラウザに反映されますが、直接 index.html を開いて見ると変更は反映されていません。これはバンドルされて出力されるファイル main.js が実際には書き出されていないためです。

myProject4
├── dist
│   ├── html
│   │   └── index.html  //公開するファイル 
│   └── main.js  //バンドルされて出力されるファイル
├── package-lock.json
├── package.json
├── src
│   ├── index.js  //エントリポイント
│   └── modules
│       └── foo.js
└── webpack.config.js

実際にバンドルされるファイルを出力するには、webpack を実行してビルドするか writeToDisk オプションを設定する必要があります。

webpack-dev-server を終了して、webpack コマンドを実行すると実際に main.js が出力され、index.html を直接開いても変更を確認することができます。

$ npm run dev   return
// または npx webpack --mode=development(モードはどちらでも)

> myProject4@1.0.0 dev /Applications/MAMP/htdocs/sample/myProject4
> webpack --mode development

Hash: af75b608d374ce859ac6
Version: webpack 4.43.0
Time: 43ms
Built at: 2020/06/22 15:45:50
  Asset     Size  Chunks             Chunk Names
main.js  5.2 KiB    main  [emitted]  main 
Entrypoint main = main.js
[./src/index.js] 673 bytes {main} [built]
[./src/modules/foo.js] 49 bytes {main} [built]

webpack コマンドで watch オプションを指定して watch モードで実行した場合は、ファイルの変更は監視されて自動的にバンドルされたファイルが書き出されますが、ブラウザには反映されないので、手動で再読み込みをして確認する必要があります。

writeToDisk

デフォルトでは webpack-dev-server はバンドルされたファイルを出力しません(ディスクに書き込みません)が、writeToDisk オプションを設定することで生成されたファイルをディスクに書き込むように指示することができます。出力先は output で指定したディレクトリになります。

以下は writeToDisk: true を指定してバンドルされたファイルを出力するようにする例です(18行目)。

webpack.config.js
const path = require('path');  //path モジュールの読み込み
 
module.exports = {
  entry: './src/index.js',  //エントリポイント
  output: {  //出力先
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  //webpack-dev-server の設定
  devServer: {
    //ルートディレクトリの指定
    contentBase: path.join(__dirname, 'dist/html'),
    //サーバー起動時にブラウザを自動的に起動
    open: true,
    // ルートディレクトリのファイルを監視
    watchContentBase: true,
    //バンドルされたファイルを出力する(実際に書き出す)
    writeToDisk: true
  }
};

webpack guides: devServer.writeToDisk