Node.js
development
Web
webpack
frontend

Webpack3でWebページ制作環境を構築する。基本編 - 前編

un-T factory! XA Advent Calendar 9日目担当の@untspringkです。

普段はWeb制作とは少し違ったことを業務で担当しているのですが、最近フロントエンド関連技術を扱う機会があったので、一度Web制作やフロントエンド開発環境構築についてまとめてみたいと思い執筆のテーマにWebpackを採用してみました。Qiita初投稿で慣れてないこともあって、いきなり長文記事になってしまいましたが、どうぞよろしくお願いいたします。

この記事について

長い記事になってしまいましたので、前編と後編に分けております。前編と後編で以下のような内容が含まれております。

【前編】 準備から、ローダーとプラグインの導入(※この記事です)

  • Webpackを使用し制作環境構築をはじめるまでの準備
  • Webpackの基本的な挙動(バンドル)について
  • Webpack-dev-serverを導入して、ローカルサーバーを立ち上げる
  • Webpackで画像などの素材を扱う(Webpackローダーとプラグインの導入)

【後編】 CSS、JavaScriptの扱いから、PRODUCTION用の最適化設定(※後日公開予定です)

  • WebpackでCSSを扱う(PostCSSによるプリ・ポストプロセシング)
  • WebpackのJavaScriptを扱う(Babelによるトランスパイル)
  • DevelopmentビルドとProductionビルド(公開バージョン用に最適化設定)

以下に記載する記事は、【前編】の内容になります。

はじめに

この記事は、モジュールバンドラ「Webpack」を使用して、シンプルなWebページ制作環境を構築する過程を記述したものです。社内向けのものでもあるので、Webページ制作に関して多少知見のある方を対象としているため、関連する技術の基本的なことは解説を省かせてもらっています。

また記事中は、Webpackの設定ファイル(webpack.config.js)を、Javascript ES2015(ES6)のシンタックス(※構文規則)で記述していますので、Nodeのバージョンを6.4.0以降で準備してください。Node.jsのバージョンとES2015以降のシンタックスのサポート状況については下記のサイトを参考にしてみてください。
Node.js ES2015/ES6, ES2016 and ES2017 support

この記事で説明していること

  • Webpackで制作環境構築を始めるまでの準備作業工程(Node、Yarnなどのインストール)
  • Webpackを使用して、Webページ制作環境を構築するまでの一通りの流れ
  • 上記一連の流れから派生する関連技術の基本的な説明(Webpackプラグインについてなど)

この記事で説明していないこと

  • Webpackの仕組み(どのような流れでバンドルされているのか?など)
  • Webpackの詳細な使い方
  • 他の代表的なモジュールバンドラ(Rollup.js、Browserifyなど)との比較

執筆時(2017年11月現在)の環境

  • MacOSX High Sierra(10.13.1)
  • Node.js v8.4.0 (6.4.0以上推奨)
  • Webpack v3.8.1

1. 準備

Webページ制作環境と言っても様々なケースが考えられますが、ここでは以下のような要件を満たすように環境を整えていきたいと思います。

Webページ制作の際に準備したい作業環境の一例

  • Sass(SCSS)や、PostCSSのコンパイル環境
  • JavaScript ES2015以降のトランスパイル環境
  • 簡易的なローカルサーバー立ち上げと、ブラウザの自動リロード機能
  • 公開時のデータ圧縮などの最適化(※開発時データとの棲み分け)

上記のような要件を満たすように制作環境を整えていきたいと思います。

Homebrew、Nodebrew、Node.js、Yarnのインストール

この項では、webpackの導入や設定をおこなう前に準備しておきたい基本的な制作ツール(Homebrew、Nodebrew、Node.js、Yarn)の導入手順について記述します。それぞれのツールについては概要にとどめ、詳細に解説はしません。すでにNode.jsとYarnが利用できる環境であれば、この項は飛ばして次項へ進んでください。

homebrewおよび、yarnのインストール

homebrewは、Mac OS上でソフトウェア導入をサポートしてくれるパッケージマネージャーです。YarnをHomebrewパッケージでインストールするために使用します。

console
$> /usr/bin/ruby -e "$(curl -fsSL
$> https://raw.githubusercontent.com/Homebrew/install/master/install)"

Yarnは、Nodeパッケージマネージャーの一つです。NPM上に公開されている様々なNodeパッケージを扱うことができます。Node.jsに同梱されている標準のパッケージマネージャー「NPM(Node Package Manager)」でも問題ありませんが、今回はYarnを使用します。

console
$> brew update /* homebrewを最新の状態に */
$> brew install yarn /* homebrewでyarnをインスール */
$> yarn -v /* yarnが正常にインストールされているかを調べるため、バージョン確認。 */
1.3.2

nodebrewおよび、nodeのインストール

nodebrewは複数のバージョンのNode.jsを切り替えて利用できるようにするツールですが、必須ではありません。すでにNode.jsがインストール済みの環境であれば、Node.jsのバージョンが6.4.0以上であることを確認しておいてください。nodebrewのインストールは公式ページにあるように、ターミナル上で以下のコマンドを実行します。

console
$> curl -L git.io/nodebrew | perl - setup

次に、パスを通します。

.bashrc
export PATH=$HOME/.nodebrew/current/bin:$PATH

設定を有効化します。terminalを再起動するか、下記コマンドを実行します。

console
$> source ~/.bashrc

以上でnodebrewのインストールは完了です。

nodebrewを使用してNode.jsをインストールする手順は下記になりますが、詳しい利用方法については公式ページを参考にしてください。

console
`Node.js、バージョン8.4.0をインストール`
$> nodebrew install-binary v8.4.0

`グローバルのNode.jsとしてバージョン8.4.0を使用する`
$> nodebrew use v8.4.0

`Node.jsのバージョンを調べる。上記useしたバージョンと同じであればOK`
$> node -v
v8.4.0

2. Webpackモジュールをプロジェクトに導入する

この項ではプロジェクト(制作環境)内に、Webpackモジュールを導入するまでの流れを説明していきますので、あらかじめ作業を行うディレクトリ(フォルダ)を準備して、そのディレクトリに移動しておいてください(※このディレクトリを以後「プロジェクト」、または「プロジェクトルート」と表記します)。

Nodeモジュールとpackage.json

「Nodeモジュール」とは大雑把に言うと、ある機能を実現するためのプログラム(※Javascriptファイル。以下、jsファイル)のことです。ただ多くの場合は、その機能を実現するために単一のjsファイル(Nodeモジュール)だけでなく、複数のjsファイルで構成されており、それら複数のjsファイルの構成情報やメタ情報などを「package.json」というjsonファイルに記載して、ひとまとめにパッケージすることから「Nodeパッケージ」ともいいます。(以下、Nodeモジュールで統一していきます)

Nodeモジュールのなかには、今回のWebpackように制作環境を整えるための裏方さん、縁の下の力持ちみたいな役割をしてくれたりするモジュールもあれば、Webページ上でjsライブラリとして利用する表舞台向きのモジュールもあります。

例えば、Webサイト制作に使用されることが多い、代表的なjsライブラリ「jQuery」もNodeモジュールとして登録されているので、jQueryをNodeモジュール(パッケージ)として自分たちのプロジェクトに追加して、適切にファイルを読むことでライブラリとして利用することができます。

「Nodeモジュール」のプロジェクトへの追加(インストール)は、通常はNode.jsと一緒に同梱されているパッケージマネージャー「NPM(Node Package Manager)」を使用しますが、今回はNPMとほぼ同じ感覚で利用ができて、処理が高速なパッケージマネージャー「Yarn」を使用します。(※Yarnの導入方法については、前項を参照)

Package.jsonの生成と、Webpackモジュールの追加

まずは、Nodeパッケージ情報を初期化して、package.jsonを生成します。

console
$> yarn init

いくつかの質問されますが、後ほど修正も可能ですので、現時点では各項目を全て[Enter]キーでスキップしても問題はありません。修正したい場合は生成されたpackage.jsonを直接編集するか、再度yarn initを実行してpackage.jsonを作り直すことができます。詳しくは、Yarn公式ページを参考にしてください。

package.jsonを生成したら、早速「Webpack」をプロジェクトに追加しましょう。yarnの場合、モージュルの追加するコマンドは「yarn add」です。追加するモジュールを開発中のみ利用するモジュール(devDependencies)としてpackage.jsonに登録するため、オプションとして「-D」または「--dev」をあわせて指定します。

console
`webpackモジュールをdevDependenciesとして追加`
$> yarn add -D webpack
`または`
$> yarn add --dev webpack

追加するモジュールのdependency(依存性)の指定については、今回のようにNodeモジュールパッケージと配布することが目的ではなく、自分たちだけが使用する制作環境構築を目的としている場合は、あまり厳密に考えなくても良いでしょう。気になる方はこちらの記事でわかりやすく解説してくださっています。

今後モジュール追加の際、開発中に必要な裏方さんモジュールであれば「devDependencies」、公開時に必須の機能を有した表舞台モジュールであれば「dependencies」として棲み分けていきます。上記コマンド(yarn add)を実行して、無事にモジュールが追加されると、「node_modules」というフォルダが生成されます。先ほど追加したWebpackモジュールはこのフォルダ内に格納されています。

3-1. webpack設定ファイルの作成

Webpackの設定ファイル「webpack.config.js」に設定を記述していきます。
まずはプロジェクトルートに「webpack.config.js」ファイルを作成しましょう。このファイルは、Webpackの設定をObject型か、または関数(Function)型ので記述し、そのデータをエクスポート(module.exports)するように記述します。Nodeモジュールのエクスポートについては、こちらの記事でわかりやすく解説くださっています。

この設定データの型の違いは、Webpack実行時に「--env」オプションを通して、任意の値を設定に渡せるかどうかの違いでしかないので、現時点ではオプションを使用しないとしても、あとで使用する可能性もあるので、とりあえず関数型で設定を記述しておけば良いでしょう。

最小限の設定で、2つのjsモジュールをバンドルする

Webpackで2つjsファイル(jsモジュール)をバンドルして、1つのjsファイルを出力する最小限の設定を行なっていきます。

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

/* あとでWebpack実行時にオプションを渡したいので関数型のデータをexportする書き方にします */
module.exports = (options = {}) =>
{
    const config =
    {
        entry: path.resolve('src', 'App.js'),   /* エントリーポイント。起点となるモジュール(jsファイル)のパス */
        output: {
            path: path.resolve('dist'), /* バンドルしたファイルの出力先(絶対パス) */
            filename: 'bundle.js'
        }
    };

    return config; /* 関数の最後でobject型のデータをreturnします */
}

ディレクトリ構造は下図のようになります。dist/bundle.jsはWebpack実行後に生成されます。

screenshot-2017-11-16-211509.png

バンドルする2つのjsの「App.js」と「Sub.js」は、シンプルにそれぞれのファイル名をコンソールに出力するものです。App.jsを起点に、Sub.jsをimportして取り込むことで、2つのjsモジュールをバンドルします。

App.js
/* 同じディレクトリにあるSub.jsをimportして、App.jsに取り込みます */
import Sub from './Sub';
console.log('App.js');
Sub.js
console.log('Sub.js');

pathモジュールについて

webpack.config.js(※一部抜粋)
const path = require('path');

Path | Node.js Documentation

Webpackの設定ファイルは、ファイルやディレクトリまでのパスを記述することが多いので、パスに関する扱いが得意な「pathモジュール」を使用します。webpack.config.jsの1行目のrequire()は、pathという名前でpathモジュールの持つAPIを使用するためです。

pathモジュールは、Node.jsをインストールした際に本体と一緒に組み込まれているので、Nodeパッケージマネージャーでモジュールを追加(yarn add)しなくても利用することができます。

webpack.config.jsの基本構造について

webpack.config.js
/* あとでWebpack実行時にオプションを渡したいので関数型のデータをexportする書き方にします */
module.exports = (options = {}) =>
{
    const config = {

/* (設定を記述します) */

    };
    return config; /* 関数の最後でobject型のデータをreturnします */
}

上記コードは、関数型のデータをexportsしています。この関数内で実際の設定内容をobject型で記述して、最後にreturnするようにします。

entry

バンドルしたいモジュール(jsファイル)のパスを記述します。「エントリーポイント」とも言います。パスの値は、string型(短縮記述)、array型、object型に対応していて、相対パス、絶対パスどちらでも大丈夫です。相対パスの場合は、プロジェクトルート(第一階層)からのパスになります。

今回は「src」ディレクトリにある「App.js」をエントリーポイントに指定しますが、文字列で「./src/App.js」と設定するのではなく、後ほどメンテナンスしやすいようにpathモジュールの持つresolve()関数を使用して、絶対パスで指定することにします。この「entry」プロパティは目的や用途に応じて書き方がいろいろありますので、下記コードや公式ページを参考にしてください。

webpack.config.js [entryプロパティの書き方いろいろ]
entry: './src/App.js' /* 短縮記述 + 相対パス */
entry: path.resolve('src', 'App.js') /* 短縮記述 + 絶対パス */
entry: ['./src/App.js', './src/Sub.js'] /* Array型。複数エントリーを1つにまとめる時 */
entry: {'bundle': './src/App.js' } /* Object型。出力ファイル名を指定可能(outputプロパティの調整が必要。後述) */

/* Object型複数。コード分割(code spliting)したい時などに */
entry: {
  'bundle': './src/App.js',
  'libs/vendor': ['jquery', 'lodash', 'velocity-animate'],
}

resolve()関数の引数に絶対パスを求めたいディレクトリまたはファイル名を指定しますが、引数は可変長引数になっているので、1つの文字列でパスを指定するだけでなく、複数の文字列に分けて指定することもできます。

webpack.config.js [path.resolve()について]
/* 下記2つの指定方法は、同じ結果(絶対パス)が得られます */

path.resolve('./src/App.js') /* 1つの文字列で指定 */
path.resolve('src', 'App.js') /* 複数の文字列で指定 */

output

webpack.config.js(※抜粋)
output: {
  path: path.resolve('dist'), /* バンドルしたファイルの出力先(絶対パス) */
  filename: 'bundle.js'
}

「output」プロパティは、その子プロパティに値を設定していきます。
非常に多くの設定項目がありますが、ここでは「path」プロパティと、「filename」プロパティを設定しましょう。

output.path
バンドルしたファイル(jsファイル)の出力先のパス(ディレクトリ)を記述します。ディレクトリの値はstring型で絶対パスで指定する必要がありますので、pathモジュールのresolve()関数を使用しています。

output.filename
バンドルしたファイルのファイル名のルールを、string型で指定します。指定できるルールにはいくつかありますが、ここではシンプルに「bundle.js」というファイル名で書き出されるように設定しています。ファイル名のルールに関しては、公式ページを参考にしてください。

3-2. Webpackの実行

設定が済んだらWebpackを実行してみましょう。yarn addで追加したWebpackは、「node_modules」フォルダに格納されますが、その実行ファイルは「node_modules/.bin/」フォルダに「webpack」というファイル名で格納されます。

このファイルを実行すると、(デフォルトでは)webpack.config.jsに記述した設定にしたがってWebpackが実行されますが、その実行の仕方にいくつか方法があります。

Webpackの実行方法いろいろ

console [Webpackの実行方法いろいろ]
$> ./node_modules/.bin/webpack /* ローカルの実行ファイルまでのパスを指定 */

$> yarn webpack /* yarn run + 実行ファイル名を使用して実行 */

$> yarn dev /* yarn run用のスクリプトを用意して実行(※今回はコチラのやり方で) */

1番目の方法は少し冗長で、何回もタイプするのが辛いですね。2番目の方法はyarnのとても便利な機能の1つで、前述のnode_modules/.binに格納されている実行ファイルは、「yarn run [実行ファイル名]」、または「yarn [実行ファイル名]」で実行することができます。

3番目の方法は、コマンド(スクリプト)名と実行内容(シェルスクリプト)を、あらかじめpackage.jsonに登録して実行しています。1つのコマンドでWebpackの実行だけでなく、他の設定や処理を同時に実行させたい時などに便利な方法です。この方法は、「yarn run」や「NPMスクリプト(npm-scripts)」と呼ばれています。

上手に活用すれば、NPMスクリプトだけでも開発環境を整えることはできますが、今回のメインテーマはWebpackですので、あくまで補助的に使用していきたいと思います。詳しい解説や活用例については、こちらの記事で詳しく解説してくださっています。

今回は3番目の方法で進めていきたいと思いますので、まず最初に「package.json」に、webpack(node_modules/.bin/webpack)を実行するコマンドを「dev」という名前で登録します。

package.json
  "scripts": {
    "dev": "webpack"
  },

次に実行ですが、npmの場合はスクリプトを実行するには「npm run [コマンド名]」と入力する必要がありますが、yarnの場合はコマンド名がyarnの他のAPI名と衝突しない場合は、runを省略して「yarn [コマンド名]」で実行することができます。詳しくは公式ページを参考にしてみてください。

今回は「dev」というコマンド名で「webpack」を実行するように記述しましたので、コンソール上で「yarn dev」と入力するとWebpackが実行されます。

console
$> yarn dev

実行結果(bundle.js)を確認

「yarn dev」でwebpackを実行すると、distフォルダの中にbundle.jsが生成されます。内容を確認してみましょう。

screenshot-2017-11-20-214715.png

少しわかりづらいかもしれませんが、entryでエントリーポイントに指定した「App.js」内で、「Sub.js」をimportし、「bundle.js」内で一緒(バンドル)になっているのがわかると思います。

Webpackモジュールの追加、yarn(npm)スクリプトの追加によって、package.jsonの内容は以下のようになりました。

package.json
{
  "name": "dev_env_web_basic1",
  "version": "0.1.0",
  "main": "index.js",
  "author": "Takayasu Beharu",
  "license": "MIT",
  "description": "Constructuring a development environment of webpages by Webpack",
  "scripts": {
    "dev": "webpack"
  },
  "devDependencies": {
    "webpack": "^3.8.1"
  }
}

以上が、Webpackの複数のファイルを結合(バンドル)するという基本的な挙動です。この基本挙動を元に様々な設定を追加していくことで、Webページ制作に必要な環境を整えていくことになります。次項では、Webpack用ローカルサーバー「webpack-dev-server」モジュールを導入して、ローカルサーバーを立ち上げてみましょう。

3-3. webpack-dev-serverを導入して、ローカルサーバーを立ち上げる

webpack-dev-serverは、webpackを使用した開発用のローカルサーバーを簡単に立ち上げることができるモジュールで、以下のような特徴を持っています。

  • 特定のディレクトリをルートに指定して、ローカルサーバーを立ち上げることができる
  • Webpack設定ファイルに則って対象ファイルを監視し、対象ファイルに変更があった場合、自動でWebpackを実行(ビルド)してブラウザの表示を更新する
  • ビルド後、ファイルは実際には生成されずにメモリ上に展開される
  • ページ全体ではなく変更された部分だけが更新されるHMR(Hot Module Replacement)を利用できる(※今回の記事ではHMRは使用しません)

webpack-dev-serverは、Expressサーバーのミドルウェアとして使用する方法もありますが、今回はシンプルにwebpack-dev-serverモジュール自体をサーバーとして使用しますので、その設定をwebpack.config.jsに記述していきます。

まずはyarn addでwebpack-dev-serverモジュールをプロジェクトに追加してださい。

webpack.config.js
$> yarn add -D webpack-dev-server

webpack.config.js

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

/* あとでWebpack実行時にオプションを渡したいので関数型のデータをexportする書き方にします */
module.exports = (options = {}) =>
{
    const config =
    {
        entry: path.resolve('src', '_scripts', 'App.js'),   /* エントリーポイント。_scriptsディレクトリ内にApp.jsを移動したので、resolve()関数の引数追加 */
        output: {
            path: path.resolve('src'), /* バンドルしたファイルの出力先(絶対パス)。一旦、distからsrcにディレクトリを変更 */
            filename: path.join('assets', 'bundle.js'), /* bundle.jsをassetsディレクトリ内に格納。path.join()関数でパスを求める */
            publicPath: '/' /* webpackでビルド(バンドル)されたデータや、生成されたjscssなどのファイルが外部素材を参照するとき、どういったパスで読み込むか(分かりづらい、、) */
        },
        devServer: {
            contentBase: path.resolve('src'), /* ローカルサーバーのルートをsrcディレクトリに指定 */
            publicPath: '/', /* 上記outputプロパティで書き出したファイルの読み込み先ディレクトリ */
            host: '0.0.0.0', /* 同じネットワークの外部PCから閲覧できるようにするためのダミープライベートIP */
            port: 9000, /* ポート番号を9000に設定。数字はお好みで */
        },
        devtool: 'source-map' /* ソースマップの書き出し可否と、そのスタイル指定 */
    };

    return config; /* 関数の最後でobject型のデータをreturnします */
}

src/index.html(新規)

サーバーを立ち上げた後に表示を確認するため、index.htmlを新規で作成します。

index.html
<!DOCTYPE html>
<html lang="ja-jp">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Making Development Environment by Webpack3</title>
</head>
<body>
    <h1>Development Environment by Webpack3</h1>
    <script src="/assets/bundle.js"></script>
</body>
</html>

ディレクトリ構造

Screen Shot 2017-11-27 at 21.55.28.png

前項から少しディレクトリ構造を変更したので、webpack.config.jsの「entry」のパスと「output.path」のパスの指定も変更しています。

path.join()と、output.publicPathについて

webpack.config.js(※抜粋)
        output: {
            path: path.resolve('src'), /* バンドルしたファイルの出力先(絶対パス)。一旦、distからsrcにディレクトリを変更 */
            filename: path.join('assets', 'bundle.js'), /* bundle.jsをassetsディレクトリ内に格納。path.join()関数でパスを求める */
            publicPath: '/' /* webpackでビルド(バンドル)されたデータや、生成されたjscssなどのファイルが外部素材を参照するとき、どういったパスで読み込むか(分かりづらい、、) */
        },

「output」プロパティは、「path」と「filename」プロパティの指定の変更と、「publicPath」プロパティを追加しました。

output.path(※変更)

バンドルファイルの出力先(ルート)ディレクトリを絶対パスで指定するプロパティですが、先ほどはこちらを「path.resolve('dist')」としていたところを、「path.resolve('src')」に変更し、プロジェクトルート直下の「src」フォルダを出力先ディレクトリに設定しなおしました。この設定は後半で、開発環境(Development)と公開環境(Production)によって出力先を切り替えられるようにします。

output.filename(※変更)

先ほど単に「bundle.js」という書き出しファイル名を文字列で設定しましたが、今度はpath.join()関数を使用して、「ディレクトリ+ファイル名」を設定しています。

path.join()関数は引数に指定した複数の文字列(ディレクトリ名)の間にスラッシュ(/)を補完してパスとして繋げた文字列を返してくれるので、自力でパスの文字列を作成するよりも手軽で便利です。今回は「output.filename」に「path.join('assets', 'bundle.js')」と「assets」と「bundle.js」を引数に指定しているので、結果として文字列「assets/bundle.js」が設定されます。

前述のバンドルファイルの出力先ディレクトリを指定する「output.path」に「src」を絶対パスで指定しているので、結果としてバンドルされた「bundle.js」は、「(プロジェクトルート)/src/assets/」ディレクトリに書き出されます。

output.publicPath(※追加)

このプロパティはその役割が少し分かりづらいかもしれません。コード中のコメントに書いてあるように、素材ファイル(画像やチャンクになったjs(※コード分割されたjs)など)を読み込む際のルート(スタート地点)を指定することになります。

このプロパティを設定をしない場合は、ファイルの参照パスのルートが、現在表示しているhtmlと同じ階層からになるだけなので、大抵の場合はこのプロパティを指定しなくても問題はありません。

このプロパティを指定する必要がある場合の1つとして、現在作業しているディレクトリの階層よりも上位の階層のディレクトリにファイルを参照したい場合、、例えばcommon.jsなどのサイト共通ファイルは、ルート階層の「shared」ディレクトリから読み込みたいなどのケースの場合は、このプロパティの値を「/」(ルート)にしておいて、「entry」や「output.filename」プロパティの方でパスを調整する必要があります。今回は「/」(ルート)に設定しました。

コード分割(code splitting)などの最適化の際に重要となってくるので、後半に改めて説明する予定です。

devServerプロパティの設定

webpack-dev-serverは特になにも設定しなくても、ローカルサーバーを立ち上げることが可能ですが、目的に応じて詳細に設定を行いたい場合は「devServer」プロパティにその設定を記述することになります。基本的なWebページ制作目的であれば、次の4つのプロパティとついでにSourceMapの設定をすれば良いかと思います。

webpack.config.js
        devServer: {
            contentBase: path.resolve('src'), /* ローカルサーバーのルートをsrcディレクトリに指定 */
            publicPath: '/', /* 上記outputプロパティで書き出したファイルの読み込み先ディレクトリ */
            host: '0.0.0.0', /* 同じネットワークの外部PCから閲覧できるようにするためのダミープライベートIP */
            port: 9000, /* ポート番号を9000に設定 */
        },
        devtool: 'source-map' /* ソースマップの書き出し可否と、そのスタイル指定 */

devServer.contentBase

ウェブルートのディレクトリを指定します。正確には、サーバーが静的ファイルを参照するときのルートディレクトリを絶対パスで指定するということになりますが、少しわかりづらいので単純にトップページで表示させたいディレクトリを指定すると覚えておけば良いと思います。今回は「src」ディレクトリに設定しているので、トップページのURL(/)にアクセスがあったときは「src/index.html」を参照し、表示します。

このプロパティを設定しない場合、プロジェクトルート(ワーキングディレクトリ。webpack.config.jsがあるディレクトリ)に設定されてしまうため、表示させたいファイル(html)が存在する適切なディレクトリを設定しておきましょう。

devServer.publicPath

ローカルサーバー(webpack-dev-server)起動中にビルド(バンドル)されて、メモリ上に展開されたファイルを読み込む際、そのパスをどこから読み込むようにするかを設定します。

前述の「output.publicPath」プロパティと同様に少し役割が分かりづらいプロパティですが、基本的には「ouput.publicPath」と用途は一緒です。違いは「output.publicPath」が実体のあるファイルへの参照のためであるのに対し、「devServer.publicPath」はwebpack-dev-serverが動的に仮想上(メモリ上)に生成するファイルへの参照のためのプロパティであることです。うーん、やっぱりわかりづらいですね。

ただ特に特別な理由がない限り、これら2つのプロパティの値を別々にする必要はないので、基本的に前述の「output.publicPath」プロパティと同じ値で問題ありません。ここでは同様にルート('/')に設定しました。

devServer.host

webpack-dev-serverでローカルサーバーを立ち上げた際のホストを指定できます。デフォルトの値は「localhost」ですが、そのままでは自分のPC以外のデバイスから自分のローカルサーバー(webpack-dev-server)上のWebページを参照することができないため、自分のプライベートIPを指定するか、「0.0.0.0」を設定すると良いでしょう。

devServer.port

webpack-dev-serverでローカルサーバーを立ち上げた際のポートを指定できます。デフォルトは8080番です。プロキシなどの設定する際に設定しておいた方が良いと思いますが、通常は必ずしも変更する必要はありません。今回は好みで9000番に設定しています。

webpack-dev-serverの設定は以上ですが、ついでにバンドルしたjsのSourceMap(ソースマップ)の書き出し設定も行いましょう。

devtool

バンドルするjsモジュール(ファイル)のソースマップの書き出しの有無、および書き出しの品質を設定できます。値はソースマップを利用しないfalseか、書き出し方法の名称を文字列で指定します。書き出し仕方は公式ページでまとめられています。スピードや品質を検証して自分の用途にあった方法を選択するようにしましょう。

ソースマップ適用前
Screen Shot 2017-12-01 at 18.30.21.png

ソースマップ適用後
Screen Shot 2017-12-01 at 18.30.47.png

ソースマップ適用すると、バンドル前のどのjsモジュール(ファイル)のどこの行で実行されているのか?エラーが出ているのか?が分かりやすくなりますね。

webpack-dev-serverの実行(ローカルサーバーの立ち上げ)

webpack-dev-serverを実行するのに、yarnスクリプトを使用したいので、package.jsonにサーバー立ち上げ用に以下の「serve」という名前のスクリプトを追加します。

package.json [追加したスクリプト抜粋]
  "scripts": {
    .....
    "serve": "webpack-dev-server"
  }

yarn runでスクリプトを実行してみましょう。

console [webpack-dev-server立ち上げ]
$> yarn serve

.....
Project is running at http://0.0.0.0:9000/

/*(略)*/

うまく設定できていれば、0.0.0.0の9000ポートでサーバーが立ち上がるので、ブラウザのアドレスバーにURLを入力して、index.htmlがちゃんと表示されるか確認してみましょう。

Screen Shot 2018-02-23 at 20.09.52.png

3-4. Webpackで画像などの素材ファイルを扱う(Webpack loaderとpluginの導入)

この項から、いよいよWebpackのローダー(loader)とプラグイン(plugin)を導入します。ローダーとプラグインのそれぞれの役割ですが、大まかに分けると以下のような棲み分けになります。

Webpackローダー(webpack loaders)

Webpackでcssや画像といったjs以外のデータを扱うとき、バンドルしたいファイルのデータ形式に応じて適切な設定をおこなう必要があります。Webpackローダーは、様々なデータ形式のファイルをバンドルできるように取り計らってくれる仲介人みたいな役割をしてくれます。ロード(load)は荷物を積み込むといった意味合いがあるので、ローダー(loader)は積み込み請負人みたいな感じでしょうか。

ただいろいろなファイルを1つのjsファイルにどんどんバンドルしていくと、最終的に膨大なサイズのjsファイルになってしまうので、一般的にはバンドルしたデータの一部を抽出して、最適化しつつ、別ファイルとして出力するように設定します。そんなときにローダーだけでなく、次に説明するWebpackプラグインを使用したりします。

Webpackプラグイン(webpack plugins)

webpackに便利な機能を追加することができる機能拡張モジュールパッケージです。最初にプラグインを導入するきっかけとなるのは、前述のようにローダーを通して組み込んだデータを分割して別ファイルに出力したい時ではないかな?と思います。プラグインはそれ以外にも様々な機能を追加できるので、一通り基本的なプラグインを導入した後に、いろいろなものを試してみると良いと思います。

プラグインを使用しなくても、ローダーでも組み込んだデータを抽出し、別ファイルに分割、生成することもできますが(file-loader, extract-loaderなど)、現時点ではローダーとプラグインの役割を明確に分けた方が理解しやすいと思います。

ですので、ローダーは色々なデータをバンドルするときに必要なもの。プラグインは組み込んだデータを有効活用したり、その他の便利な機能をwebpackに追加するもの。といった感じで現状は認識してもらえれば良いかと思います。

この項を終えると以下のような状態になります。

Screen Shot 2017-12-01 at 20.09.21.png

package.json
{
  "name": "dev_env_web_basic1",
  "version": "0.1.0",
  "main": "index.js",
  "author": "Takayasu Beharu",
  "license": "MIT",
  "description": "Constructuring a development environment of webpages by Webpack",
  "scripts": {
    "dev": "webpack",
    "serve": "webpack-dev-server"
  },
  "devDependencies": {
    "file-loader": "^1.1.5",
    "html-loader": "^0.5.1",
    "html-webpack-plugin": "^2.30.1",
    "url-loader": "^0.6.2",
    "webpack": "^3.8.1",
    "webpack-dev-server": "^2.9.4"
  }
}
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

/* あとでWebpack実行時にオプションを渡したいので関数型のデータをexportする書き方にします */
module.exports = (options = {}) =>
{
    const config =
    {
        entry: path.resolve('src', '_scripts', 'App.js'),   /* エントリーポイント。_scriptsディレクトリ内にApp.jsを移動したので、resolve()関数の引数追加 */
        output: {
            path: path.resolve('src'), /* バンドルしたファイルの出力先(絶対パス)。一旦、distからsrcにディレクトリを変更 */
            filename: path.join('assets', 'bundle.js'), /* bundle.jsをassetsディレクトリ内に格納。path.join()関数でパスを求める */
            publicPath: '/' /* webpackでビルド(バンドル)されたデータや、生成されたjscssなどのファイルが外部素材を参照するとき、どういったパスで読み込むか(分かりづらい、、) */
        },
        devServer: {
            contentBase: path.resolve('src'), /* ローカルサーバーのルートをsrcディレクトリに指定 */
            publicPath: '/', /* 上記outputプロパティで書き出したファイルの読み込み先ディレクトリ */
            host: '0.0.0.0', /* 同じネットワークの外部PCから閲覧できるようにするためのダミープライベートIP */
            port: 9000, /* ポート番号を9000に設定。数字はお好みで */
        },
        devtool: 'cheap-module-eval-source-map', /* ソースマップの書き出し可否と、そのスタイル指定 */
        module: {
            rules: [
                {
                    test: /\.(png|jpe?g|gif)$/, /* これらの拡張子にマッチしたファイルをローダーの対象 */
                    exclude: /node_modules/, /* node_modulesディレクトリをローダーの対象外に */
                    use: [{
                        loader: 'url-loader', /* 画像などをURLエンコードするローダー。実は同時にfile-loaderと連携し、容量リミットを超えた場合の処理も実行します */
                        options: {
                            limit: 8192, /* 画像の容量が約8KB以下の場合は、URLエンコードする */
                            // name: '[name].[ext]', /* ファイルを書き出す際のファイル名(文字列で指定) */
                            name(file) { /* ファイルを書き出す際のファイル名(関数で指定) */
                              return file.slice(file.indexOf('assets') + 'assets'.length);
                            },
                            outputPath: path.join('assets') /* 容量リミットを超えた場合はこのディレクトリにファイルを書き出す */
                        }
                    }]
                },
                {
                    test: /\.html$/, /* 拡張子htmlのファイルをローダーの対象に */
                    exclude: /node_modules/,
                    use: 'html-loader' /* htmlファイルをjsに組み込む(ロード)するために必要なローダー。使用するローダーが1つであれば、「use」プロパティを文字列で指定可能 */
                }

            ]
        },
        plugins: [
            new HtmlWebpackPlugin({ /* 高機能なプラグインです。いろいろなローダーやプラグインと連携して、あらたなHTMLを生成できます */
                template: path.resolve('src', 'index.tpl.html'),  /* テンプレートとして使用するファイルを指定 */
                filename: path.resolve('src', 'index.html') /* 書き出し先とファイル名 */
            })

        ]
    };

    return config; /* 関数の最後でobject型のデータをreturnします */
}

src/index.tpl.html(※新規)

index.tpl.html
<!DOCTYPE html>
<html lang="ja-jp">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Making Development Environment by Webpack3</title>
</head>
<body>
    <h1>Making Development Environment by Webpack3</h1>
    <img src="./_assets/images/cat.jpg" />
</body>
</html>

ローダーやパッケージなど、yarnで追加するモジュールパッケージは以下です。

console [パッケージの追加]
$> yarn add -D url-loader file-loader html-loader html-webpack-plugin

それぞれの簡単な機能説明です。

url-loader

画像などのファイルをエントリーポイント(js)に組み込むローダーの1つ。対象のデータのサイズがオプションで設定した制限容量(options.limit)内であれば、データのソース(例、img要素src属性)内容をBase64形式にDataURIエンコードされた文字列に変換してくれます。デフォルトでは制限容量を超えた時のフォールバック(例外)処理として後述の「file-loader」モジュールを使用するため、あらかじめプロジェクトに「file-loader」モジュールを組み込んでおく(yarn add)必要があります。

file-loader

画像などのファイルをエントリーポイント(js)に組み込むローダーの1つ。デフォルトでは、組み込んだデータを特定のディレクトリに、特定のファイル名で書き出し、戻り値として、その画像までのパス(文字列)を返してくれます。画像本体(バイナリ)ではなく、パスを返すという点がポイントです。

後述の「html-loader」モジュールと連携し、アセットの参照元(htmlのimgタグなど)のパスを、このローダーの返り値のパスで書き換えた後、「html-webpack-plugin」モジュールと連携して、納品用の新たなhtmlファイルを生成するといった使い方をします。後ほど詳細を説明します。

html-loader

htmlデータをjsに組み込むローダー。htmlを文字列として扱いパースし、他のローダーやプラグインと連携できるようにします。例えばパースの結果、html上で画像などの外部アセットファイルが読み込まれていることが認識されると、前述のurl-loaderやfile-loaderと連携して適切に処理してもらうことができます。

それ以外にもHTMLのミニファイやパスの書き換えなど納品データ向けの処理もできますが、このローダーだけでは新たなファイルを生成できないため、納品データ向けに新たなHTMLファイルを生成したい場合は、「file-loader」や後述のhtml-webpack-pluginと連携して使用する場合が多いです。

html-webpack-plugin

とても高機能なプラグインで、様々なモジュールと連携して機能拡張できますが、主な利用用途は、元となるhtml(またはejsなどのHTMLメタ言語)をもとに、最適化された新たなhtmlを生成したいときに利用します。前述のhtml-loaderとの連携はこの生成機能を利用します。

今回導入した3つのローダーと1つのプラグインについては以上です。

文章だけだとわかりづらいし、webpack.config.jsの記述も少し複雑になってきたので、このあたりの設定は最初の難関に感じるかもしれませんね。ですので、手始めに素材を扱うための代表的なローダー「file-loader」と「url-loader」の挙動を見ていきながら、設定の「コツ」みたいなものが掴んでいきましょう。

ローダー(loader)の基本的な設定について

ローダー(以下、loader)の設定は、object型の値を持つ「module」プロパティから始まり、配列の値をとる「rules」プロパティに、それぞれの設定をobject型で記述していくことになります。
つまり、「module」プロパティと「rules」プロパティまでの書き方は毎回一緒で、「rules」プロパティの配列にどんどん設定を追加していくことになります。上記の場合は「url-loader」の設定です。設定に必要なプロパティは基本的に以下のような項目になります。

rule.test
loaderの対象となるデータ(ファイル)名を指定する。文字列や配列等でも指定できますが、正規表現で拡張子を指定するケースが多いです。

rule.exclude
loaderの対象外とするディレクトリやファイルを指定する。文字列や配列等でも指定できますが、こちらも正規表現で「node_modules」を指定する場合が多いです。反対に、rule.includeプロパティがあり、こちらは対象となるディレクトリを絞り込むことができます。

rule.include
loaderの対象となる(絞り込む)ディレクトリやファイルを指定する。対象の絞り込みが主な目的になりますが、「同じloaderを使用するが、対象となるデータと書き出し先を変えたいとき」などにも使用します。指定しない場合はプロジェクト内のデータ全体が対象となるため、指定しなくても問題ありませんが、データの種類が増えてきたり、処理分けを行いたい時などに使用する機会が出てくると思います。

rule.use
対象となるデータをバンドル(読み込む、ロードする)際に使用するloaderとその設定(options)を指定します。loaderが一つであればloader名を文字列。使用するloaderが複数あれば配列。使用するloaderに設定が必要であればオブジェクト型で記述します。

webpack.config.js(Webpackローダー設定「use」記述パターンいろいろ)
use: 'file-loader' /* ローダー1つ、オプションなし */
loader: 'file-loader' /* ローダー1つ、オプションなし(useじゃなくても、OK */
use: ['file-loader', 'url-loader'] /* ローダー2つ、オプションなし */

/* ローダー1つ、オプションあり */
use: {
  loader: 'file-loader',
  options: {
    name: '[name].[ext]'
  }
}

/* ローダー2つ、1つのローダーにオプションあり */
use: [
  {
    loader: 'file-loader',
    options: { name: '[name].[ext]' }
  },
  'url-loader'
]

/* ローダー2つ、2つのローダーともにオプションあり */
use: [
  {
    loader: 'file-loader',
    options: { name: '[name].[ext]', outputPath: path.join('assets', 'images' }
  },
  {
    loader: 'url-loader',
    options: { limit: 8194 }
  }
]

複数のローダーの実行と実行順序

Webpackローダーはエントリーポイント(js)にデータを組み込む(バンドル)際に、うまいこと取り計らってくれる仲介人みたいな役割と以前述べましたが、データの用途や目的によっては、「仲介」が複数必要になる時があります。

次に説明する「url-loader」「file-loader」が早速そのケースに該当するのですが、今回の場合は、対象データ「.png」「.jpg」「.gif」の拡張子を持つデータをエントリーポイントに組み込む時、まず「url-loader」が処理し、そのあとに「file-loader」を処理する。という流れに設定しています。(※具体的な処理の流れはこのあとに記載します)

ここで押さえておきたいのはローダー設定プロパティの1つ「use」の記述方法と、その実行順序です。複数のローダーの処理を挟みたい場合「use」プロパティに配列で複数のローダーの設定を記述しますが、その際は配列の後ろに設定したローダーが先に処理が実行されることを覚えておいてください。

webpack.config.js(ローダーの実行順の例)
/* ローダーは「use」プロパティの配列に指定したローダーが後ろから順番に実行されます。
下の例の場合は、「url-loader」の後に、「file-loaderが実行されます */

use: ['file-loader', 'url-loader']

3-4. file-loaderとurl-loaderで画像をバンドルする

file-loaderとurl-loaderの基本的な挙動については前述の通りです。設定ファイル(webpack.config.js)には以下の内容を追加しています。

webpack.config.js
/*(略)*/

module: {
  rules: [
    {
      test: /\.(png|jpe?g|gif)$/, /* この内容にマッチしたファイルをローダーの対象 */
      exclude: /node_modules/, /* node_modulesディレクトリをローダーの対象外に */
      use: [{
        loader: 'url-loader', /* 画像などをURLエンコードするローダー。実は同時にfile-loaderと連携し、容量リミットを超えた場合の処理も実行します */
        options: {
          limit: 8192, /* 画像の容量が約8KB以下の場合は、URLエンコードする */
          // name: '[name].[ext]', /* ファイルを書き出す際のファイル名(文字列で指定) */
          name(file) { /* ファイルを書き出す際のファイル名(関数で指定) */
            return file.slice(file.indexOf('assets') + 'assets'.length);
          },
          outputPath: path.join('assets') /* 容量リミットを超えた場合はこのディレクトリにファイルを書き出す */
        }
      }]
    },

/*(略)*/

「file-loader」の設定について

「file-loader」と「url-loader」を使用するということで、上記設定では確かに「url-loader」を「use」プロパティで指定して、そのオプションも設定しています。しかし「file-loader」が「use」に含まれていないし、その設定も記述されていないように見えますね。

実は上記記述は、「url-loader」と「file-loader」の設定を同時に行なっています(「url-loader」自体は、オプションに「name」プロパティや「outputPath」プロパティを持っていません。これらのプロパティは「file-loader」の持つオプションです)。

「url-loader」は、対象とする素材の容量が制限(options.limitプロパティ)の値を超えた場合、デフォルトでは自動的に「file-loader」をフォールバック(本来の処理が実行できなかったときの代替処理)として実行します。ですので、「url-loader」を使用する場合は必ず「file-loader」もプロジェクトに追加(yarn add)しておきましょう。

limit以外のプロパティは、file-loaderのオプションになります。いくつかのオプションが用意されていますが、下記のように、最低限ファイル出力先のディレクトリ(outputPath)とファイル名(name)が指定されていれば良いと思います。詳しくは公式のドキュメントを参考にしてみてください。

file-loader:ouputPath

出力先のディレクトリを文字列型(string)で指定します。注意すべきこととして、このプロパティのルートパスは、前述の「output.publicPath」の値を参照していることです。「output.publicPath」は、出力されたhtml、css、jsなどが他のリソースを参照するときのルートディレクトリを設定しているので、ここで設定する値は、そのルートディレクトリ(output.publicPathの設定)からのパスになります。

ここで設定の仕方に、絶対パスを求めるpath.resolve()関数を使用すると、「output.publicPath」で設定したパスと連結した文字列がパスとして設定されてしまうため、ここではパス用の文字列を取得するpath.join()関数を使用しています。今回のように「assets」だけであれば、単に文字列で指定しても問題はありません。

また、file-loaderにもオプション「publicPath」プロパティが用意されていて、これにパスを設定することで、output.publicPathの値を参照せずに、file-loader独自のアセット用ルートパスを持つこともできますが、これら2つの値が異なるのはトラブルの原因になりかねないので、なるべく値を変えないようにしましょう。

file-loader:name

出力ファイル名を文字列または、関数で指定します。上述の「outputPath」オプションと連結されて、出力ファイルまでの完全なパスになります。このnameプロパティを設定しないと、デフォルトでは[hash].[ext]が適用され、[hash](ハッシュ値。ランダムな英数字の羅列)と、[ext](extension。拡張子)でファイル名が設定されます(例 4fc7d7b6764e6e38761b4992bb8897b1.jpgなど)。

出力ファイル名を「文字列(String)」で指定する場合は、先述のようにカスタムファイル名テンプレート([]で囲まれたキーワードで、最終的に文字列に置き換えられるプレイスホルダーみたいなもの)で指定します。テンプレートの意味については公式ページを参考にしてみてください。開発中は認識しやすいファイル名で、公開版はファイルをハッシュ名で生成すると、画像キャッシュトラブルを避ける方法の1つになります。

文字列で指定する方法の欠点?(というほどのものではないですが、、)として、ファイルがすべて同じディレクトリに出力されてしまうことでしょう。[path]というテンプレートが用意されていますが、開発時に参照するアセット用のパスに変換されてしまうため実用的ではないかな?と思います。

開発環境のデータと納品データを分けることが当たり前になっている昨今、データの階層関係やファイル名はそれほど重要ではないケースも増えてきているかもしれませんが、納品後に先方の担当者が更新するなどの場合は、わかりやすいファイル構造にする必要もあるかもしれませんので、今回はファイル構成の構造を保つ出力方法として、「関数(Function)」で設定しています(※文字列で設定する方法も記述していますが、コメントアウトしています)

関数で指定する場合は、引数にファイルまでの絶対パスを受け取り、戻り値にパスの文字列を設定するようにします(この文字列にカスタムファイル名テンプレートが含まれていても大丈夫です)。

今回の指定の仕方は、引数として受け取った絶対パス(文字列)を、あらかじめ決めておいたアセット用のディレクトリ名を境界として文字列を分割(slice)することで、開発中のアセットディレクトリの階層構造を保ったまま、公開用のアセットディレクトリに出力できるようにしています。

例)name()関数の引数「file」が「/user/your_root/your_project/assets/images/cat.jpg」だった場合
1) indexOf()関数が「assets」の文字列を探して29文字目(インデックス値は28)を返す
2) そのままだとslice()関数が文字列を29文字目を境に分割するので、「assets/images/cat.jpg」となり、「assets」がちょっと余計
3) なので「assets」の文字数分('assets'.length)6を足して35文字目で分割してもらうと、意図したように「/images/cat.jpg」をファイル名として指定できる

String型が持つindexOf()関数と、slice()関数についてはリンクを参考にしてみてください。

webpack.config.js(抜粋) file-loaderオプション「name」を関数で指定
name(file) { /* ファイルを書き出す際のファイル名(関数で指定) */
  return file.slice(file.indexOf('assets') + 'assets'.length);
}

/* 上記コードは、es2015以降の書き方です。下記のような意味です */
name: function(file){
  return file.slice(file.indexOf('assets') + 'assets'.length);
}

url-loaderとfile-loaderの動作を確認する

loaderの主な設定項目は以上です。ただこの設定を記述しただけでは何も変化はありません。

loaderの設定は、基本的にWebpack設定(webpack.config.js)の「entry」プロパティに設定したエントリーポイント(js)に対象データ(ファイル)が取り込まれた(import)タイミングで実行するため、今回の場合は拡張子に「gif」「jpg」「png」を持ったファイルをエントリーポイント(※今回の場合は、App.js)に取り込む(import)必要があります。取り込む方法はいくつかの方法がありますが、まずはシンプルにjsファイル内からimportしてみましょう。

App.js
(略)

/* ▼ここから、追加 */
import catImage from './../_assets/images/cat.jpg'; 

const cat = document.createElement('img'); /* imgタグを生成し、srcに取り込んだ猫の画像を設定 */
cat.src = catImage;
document.body.appendChild(cat); /* bodyタグ子要素としてDOMツリーに追加して、画面上に表示 */
/* ▲追加、ここまで */

(略)

この段階で「yarn dev」してWebpackを実行してみましょう。バンドルされたbundle.jsを確認すると、コードの一部に以下のような記述があることを確認できると思います。

Screen Shot 2017-12-05 at 21.50.11.png

コードの中に「assets/images/cat.jpg」という猫の画像へのパスが記述されていると思います。もともとApp.js上では猫の画像を、「./../assets/images/cat.jpg」のパスで読み込んでいましたが、なぜパスが変化したのでしょうか?

これはApp.jsが猫の画像の読み込み(4行目のimport)を行ったことで、jpgの拡張子を対象ファイルの1つと設定した「url-loader」が設定にしたがって仕事をし始めますが、読み込もうとした猫の画像は約76KBと、DataURIエンコード処理を実行する制限サイズを超えてしまっているので処理を実行せずに、その仕事を「file-loader」に委ねるからです。

webpack.config.js url-loader設定抜粋
(略)
    loader: 'url-loader', /* 画像などをURLエンコードするローダー。実は同時にfile-loaderと連携し、容量リミットを超えた場合の処理も実行します */
    options: {
          limit: 8192, /* 画像の容量が約8KB以下の場合は、URLエンコードする */
          // name: '[name].[ext]', /* ファイルを書き出す際のファイル名(文字列で指定) */
          name(file) { /* ファイルを書き出す際のファイル名(関数で指定) */
            return file.slice(file.indexOf('assets') + 'assets'.length);
          },
          outputPath: path.join('assets') /* 容量リミットを超えた場合はこのディレクトリにファイルを書き出す */
        }
    }
(略)

「file-loader」は、url-loaderに設定したオプション(上記コード参照)に従い、結果として得られるパスは「assets/images/cat.jpg」となります。そして、猫の画像(cat.jpg)は「assets/images」フォルダに出力(コピー)され、bundle.js内に記述される猫の画像へのパスも自動的に書き換えられました。

まだこのままでは猫の画像を読み込んだだけですので、ブラウザ(画面)上に画像は表示されません。App.jsでは下記コードのようにimportした猫の画像(catImage)を、jsで動的に生成したimg要素のsrc属性にアタッチしてブラウザ上に表示しています。

App.js (img要素動的生成部分の抜粋)
import catImage from './../_assets/images/cat.jpg'; 
const cat = document.createElement('img'); /* imgタグを生成し、srcに取り込んだ猫の画像を設定 */
cat.src = catImage;
document.body.appendChild(cat); /* bodyタグ子要素としてDOMツリーに追加して、画面上に表示 */

yarn serveなどしてコンパイルして結果見ると、下の画像のように猫の画像が表示されると思います。

Screen Shot 2017-12-05 at 22.12.03.png

Chromeのディベロッパーツールで確認すると、imgタグが動的に生成されていることが確認できます。

htmlに静的に記述したimg要素をから画像を取り込むための準備

ここまででようやくWebpackを通してjsに画像を取り込み(バンドル)、画面上に表示することができました。しかし通常のウェブページ制作では、jsで動的にimg要素を生成するよりも、htmlドキュメント上にimg要素(タグ)を配置して画像を読み込む場合がほとんどだと思いますので、このやり方はあまり実用的ではないですね。(※画像の遅延読み込みの場合はjsからの動的読み込みになると思いますが、その話また別の機会に)

そこで、実際の現場では「url-loader」と「file-loader」だけでなく、次に説明する「html-loader」と、「html-webpack-plugin」を併用して利用するケースが多いと思います。

html-loaderで、htmlをjs内に取り込む(挙動確認のため)

html-loaderの挙動を確かめてみましょう。その役割は前述の通りですが、その名の通りhtmlをjsに組み込む(ロード)するためのloaderです。jsにhtmlを組み込むという感覚が不思議ですね。実際はそのように使用せずに後述の「html-webpack-plugin」プラグインと組み合わせて使用することが多いと思いますが、その挙動を確認する意味で、一度jsからhtmlファイルをimportして組み込んでみます。

組み込むhtml(index.html)は以下のような感じです。猫の画像をimg要素のsrc属性で「./_assets/images/cat.jpg」(※開発用アセットフォルダ「_assets」と公開用アセットフォルダ「assets」を分けています)のパスで読み込んでいます。

index.html
<!DOCTYPE html>
<html lang="ja-jp">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Making Development Environment by Webpack3</title>
</head>
<body>
<h1>Making Development Environment by Webpack3</h1>
<img src="./_assets/images/cat.jpg"> /* imgタグで静的に画像を配置 */
<script src="./assets/bundle.js"></script>
</body>
</html>

このindex.htmlをApp.jsからimportして組み込みます。先の例で猫の画像を読み込んだコードは削除しました。

App.js
/*(略)*/

import indexHtml from './../index.html'; /* index.htmlを組み込み */
document.body.innerHTML = indexHtml; /* 組み込み後パースされたhtml(文字列)をbodyタグ内にHTMLとしてレンダリングさせて、画面上に表示 */

yarn devでコンパイルされたbundle.jsを確認してみましょう。

Screen Shot 2017-12-06 at 17.21.37.png

ちゃんとindex.htmlのタグらしきものが組み込まれているのが分かりますね。このjs上で文字列化したindex.htmlはパースされて、loaderの対象ファイルが含まれていれば、その内容にしたがって処理が実行されます。つまり、index.htmlで配置した猫の画像cat.jpgは、url-loaderとfile-loaderの処理を経て、「/assets/images/cat.jpg」を参照するコードに書き換えられます。

次に「yarn serve」を実行して「webpack-dev-server」を立ち上げて、表示状態を確かめてみます。

Screen Shot 2017-12-06 at 17.51.58.png

index.html上でimgタグで静的に猫の画像を配置したので表示されるのは当たり前ですが、ディベロッパーツールでhtmlを確認してみると、App.jsによって動的にbodyタグ内の内容が一部書き換えられていることが確認できると思います。

index.html(一部抜粋。Chromeディベロッパーツールで確認)
/* 元のindex.html上でのimgタグの記述 */
<img src="./_assets/images/cat.jpg"> */

/* html-loaderindex.htmljsに組み込み、innerHTMLで動的に書き換えたときのimgタグの記述 */
<img src="/assets/images/cat.jpg">

前述の通り「file-loader」の働きによって、パスが書き換わってますね。

「url-loader」の挙動を確認する

このタイミングで、一向に仕事をしない「url-loader」の挙動も確認してみたいので、「url-loader」の処理制限を設定するオプションの「limit」プロパティの値を猫の画像の容量サイズ(76KB)よりも大きくし、約80KBに設定してみます。

webpack.config.js(※抜粋)
use: [{
    loader: 'url-loader', /* 画像などをURLエンコードするローダー。実は同時にfile-loaderと連携し、容量リミットを超えた場合の処理も実行します */
    options: {
          limit: 81920, /* 容量リミットを一時的にが約80KB以下に */

      ...()...

jsのコンパイルとバンドルのため「yarn dev」。その後、表示確認のため「yarn serve」してページを確認してみます。(※yarn serveだけ実行して、jsを保存してコンパイルを実行しても大丈夫です。)

Screen Shot 2017-12-06 at 18.16.09.png

ディベロッパーツールのhtmlを確認すると、imgタグのsrc属性の値がパスではなく、Base64エンコードされた文字列になっていることが確認できると思います。

この例では対象となっている猫の画像の容量サイズが大きすぎるので効果的とは言えません。「url-loader」の使いどころは、HTTPリクエスト数を減らす目的で、アイコンなどの比較的小さいサイズの画像やSVGなどをエンコードする時に、その実力を発揮すると思います。

この項の目的は「html-loader」と「url-loader」の挙動確認ためでしたので上記でおこなった設定は一時的なものでした。ですので、さきほど一時的に変更した「url-loader」の「limit」プロパティの値を元に戻し、「html-loader」の挙動確認のため、App.jsからindex.htmlをimportする部分のコードも削除して、次のhtml-webpack-pluginの導入を進めてください。

3-5. html-webpack-pluginを追加し、テンプレートから最適化されたhtmlを生成する

初めてのWebpackプラグインの導入として、「html-webpack-plugin」の設定を行います。

なぜこのプラグインを使用するかというと、前回の項では「index.html」上に静的に配置した画像を、「url-loader」の対象ファイルとして認識してもらうため、「画像を直接jsにimportする」や「index.htmlをjsにimportする」などの方法をご紹介しましたが、しかし通常のウェブサイト制作ではこれらの方法はあまり実用的ではない場合が多いと思います。

しかし、この「html-webpack-plugin」を使用すると、プラグイン用のテンプレー(HtmlWebpackPlugin:template)に設定したファイルは、エントリーポイント(entry)に設定していなくてもloaderの対象ファイルとして認識され、プラグインで設定した出力先(HtmlWebpackPlugin:filename)に最適化されたファイルを生成します。今回の場合は、テンプレートファイルが「index.tpl.html」なので、「html-loader」が機能し、「url-loader」「file-loader」が連携します(※詳細は後述)。

文章にすると少しわかりづらいのですが、つまり、いつものようにhtmlコーディングを行った後に、そのファイルを元にして最適化されたファイルを生成することができるという点が便利で、たとえばコーダーさんが通常のコーディングを行った後に、webpack設定に関して知見がある人が後から最適化作業をおこなうといった分業も可能だと思います。

html-webpack-pluginの基本的な挙動を確認

html-webpack-pluginの基本的な挙動を追って確認してみましょう。

今回の場合はhtmlファイル「index.tpl.html」をプラグインのテンプレート(HtmlWebpackPlugin:template)ファイルに設定するので、このファイルは「html-loader」の対象となります。「index.tpl.html」には、img要素のソースパス(src属性)「./_assets/images/cat.js」が記述されているので、「url-loader」と「file-loader」が連携し、設定どおりの挙動を実行します。

html-webpack-pluginは出力先設定(HtmlWebpackPlugin:filename)に従い、(今回の場合は)「index.html」を出力しますが、この生成された「index.html」にはWebpack設定(webpack.config.js)の「output」プロパティの設定に従って、バンドルされたjsファイルを読み込むscript要素(タグ)を、自動的にhtml内に挿入してくれます。

Webpackプラグインは、設定ファイル(webpack.config.js)の「plugins」プロパティのもつ配列に、それぞれのプラグイン設定を追加していきます。今回追加した「html-webpack-plugin」の設定内容は、以下のようになります。

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

/* (略) */

    plugins: [
        new HtmlWebpackPlugin({ /* 高機能なプラグインです。いろいろなローダーやプラグインと連携して、あらたなHTMLを生成できます */
            template: path.resolve('src', 'index.tpl.html'),  /* テンプレートとして使用するファイルを指定 */
            filename: path.resolve('src', 'index.html') /* 書き出し先とファイル名 */
        })
    ]

設定ファイルの冒頭で、あらかじめ「html-webpack-plugin」モジュールを読み込んで「HtmlWebpackPlugin」という変数名で扱えるようにした後、設定の「plugins」プロパティの持つ配列に、「html-webpack-plugin」の設定を格納しています。

Webpackプラグインは、基本的にrequireしたプラグインを「new演算子」を用いてインスタンスを作成するとともに、そのコンストラクタに渡す引数でオプションを設定するパターンが多いです。文章ではわかりづらいので設定例を見てみましょう。例でもわかりづらいかもしれませんが(笑)、今後色々なプラグインを扱っていくことになるので、だんだんと掴めてくると思います。

webpack.config.js(Webpackプラグインの基本設定例)
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const PATH_ROOT = '';
const PATH_SRC = 'src';
const PATH_DIST = 'dist';
const pageAbout = new HtmlWebpackPlugin({
  template: path.resolve(PATH_SRC, PATH_ROOT, 'about.tpl.html'),
  filename: path.resolve(PATH_DIST, PATH_ROOT, 'about.html')
});

/*(略)*/
  plugins: [ /* プラグインは配列で複数設定可能 */
    new HtmlWebpackPlugin({ /* new [requireしたプラグイン名]([オプション]) */
      template: path.resolve(PATH_SRC, PATH_ROOT, 'index.tpl.html'),
      filename: path.resolve(PATH_DIST, PATH_ROOT, 'index.html')
    }),
    pageAbout /* あらかじめインスタンスを作成 */
  ]
/*(略)*/

「html-webpack-plugin」のオプション設定では、最低限以下の2項目を設定しておけば良いでしょう。

html-webpack-plugin : template

書き出すファイルのテンプレート(もと)となるファイルを指定します。htmlファイルの他に、ejsやpug、handlebarsなどのHTML拡張も使用できますが、loaderの設定や挙動に少しコツや慣れが必要なため、慣れないうちはhtmlファイルをテンプレートに指定すると良いでしょう。

ここで注意したいのはテンプレートファイルと次に紹介する生成ファイルのディレクトリまたは名前を別にすることです。一緒の場所と名前にしてしまうと、コンパイルの際にテンプレートファイルが生成ファイルの内容で上書きされてしまいますのでご注意ください。(webpack-dev-server起動時のコンパイルであれば、メモリに展開されるため上書きされません。)

html-webpack-plugin : filename
上記テンプレートファイルをもとに、新たにファイルが生成されるファイルの書き出し先のパスとファイル名を指定します。ここで設定するパスは、相対パスの場合はtemplateで設定したファイルと同じディレクトリからの相対パスになります。またpath.resolve()を使用して絶対パスで指定することもできるので、ここでは絶対パス指定で「src」ディレクトリに「index.html」というファイル名で書き出すように設定しました。

以上で、「html-webpack-plugin」設定は終了です。設定通り「src」ディレクトリに「index.tpl.html」が用意してあれば、「yarn dev」でコンパイル後、同じディレクトリに「index.html」が生成(または上書き)されると思います。内容を確認すると以下のような記述になっていると思います。

index.html(html-webpack-pluginで生成)
<!DOCTYPE html>
<html lang="ja-jp">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Making Development Environment by Webpack3</title>
</head>
<body>
    <h1>Making Development Environment by Webpack3</h1>
    <img src="/assets/images/cat.jpg" />
<script type="text/javascript" src="/assets/bundle.js"></script></body>
</html>

「index.html」の雛形(テンプレート)となった、「index.tpl.html」を再度掲載します。

index.tpl.html
<!DOCTYPE html>
<html lang="ja-jp">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Making Development Environment by Webpack3</title>
</head>
<body>
    <h1>Making Development Environment by Webpack3</h1>
    <img src="./_assets/images/cat.jpg" />
</body>
</html>

index.tpl.htmlで静的に配置したimgタグの猫の画像のパスが、「file-loader」の設定通りにちゃんと書き換わっていることと、bundle.jsを読み込むscriptタグが自動でbodyタグの末尾に追記されていることがわかると思います。(※index.tpl.htmlではscriptタグを記述していません)

この後に続く、CSSの設定(PostCSSのプリ・ポストプロセッシング)や、jsの設定(Babelによるトランスパイル)などは、基本的にこれまで説明してきた流れと一緒です。

つまり、まず対象とするファイルの特徴(拡張子など)と、それをjsに組み込むために使用するloaderを設定する。そして次に、組み込んだ内容を抽出して別ファイルとして生成するなどloaderの基本的な機能の範疇を超える処理はpluginで導入してサポートする。という流れです。

とても長々とした記事になってしまいましたが、「Webpack3でWebページ制作環境を構築する。基本編 - 前編」は以上になります。最初はとっつきにくいWebpackですが、実は記述する内容は以外にシンプルなんだなと思っていただけたら幸いです。

「基本編 - 後編」では、PostCSSによるCSSの設定、Babelによるjsの設定、そして最後にWebpackビルド(バンドル)方法に、Development(開発)モードとProduction(公開)モードを用意し、コードやアセットの最適化について説明する予定です。

  • 3-1. webpack設定ファイルの作成
  • 3-2. Webpackの実行
  • 3-3. webpack-dev-serverを導入して、ローカルサーバーを立ち上げる
  • 3-4. Webpackで画像などの素材ファイルを扱う(Webpack loaderとpluginの導入)
  • 3-4. file-loaderとurl-loaderで画像をバンドルする
  • 3-5. html-webpack-pluginを追加し、テンプレートから最適化されたhtmlを生成する