最近JavaScript関連の開発で採用が進んでいる「webpack」は、JavaScriptファイルの変換や結合といった操作をコマンド1つで実行できるツールだ。本記事では、webpackとは何かという基本的な概念から導入方法、実際の利用例などを紹介する。

モジュール管理機構の不足や未サポート機能の問題をツールで解決する「webpack」

今日のWeb開発においてJavaScriptの利用は避けることができないが、JavaScriptのソースコードをどのように管理・デプロイすべきかという問題に対しては、まだ決定的な解決策が生まれていない状況が続いている。その根本的な原因の1つには、最近までJavaScriptにおいて普遍的に利用できるモジュール管理システムが存在しなかったことがある。

一般的なプログラミング言語では、大規模なプログラムを実装する際に作業性やメンテナンス性を高めるためプログラムを複数のファイル(モジュール)に分割して実装する。これはJavaScriptにおいても例外ではない。しかし、JavaScriptではモジュール内から別のモジュールをロードする機能が長らく標準化されていなかった。そのため、Webブラウザで複数のモジュールに分割されたJavaScriptプログラムを実行させたい場合、HTML内でscriptタグを使って必要なモジュールを個別にロードさせる必要があった。

しかし、このようにscriptタグを使ってJavaScriptをロードする場合、使用するモジュールが増減した場合いちいちHTMLを変更する必要があり、メンテナンス性が悪くなってしまう。そのため、あらかじめ使用するモジュールを1つのファイルに結合しておくといった手法が使われていた。ただ、どちらの場合もモジュール間の依存性は自前で管理しなければならず、たとえばあるモジュールのロードを忘れたためにプログラム全体が動作しなくなる、といった不具合が発生しやすいという問題もあった。

一方でNode.jsなどのサーバーサイドJavaScriptでは独自にモジュール管理機構を実装し、それによってモジュール内から別モジュールのオブジェクトや関数を利用する仕組みを実現していた。以前より独自のモジュール管理機構というのは考案されていたが、Node.jsの普及とともにモジュール管理機構の便利さが広まり、それを受けてWebブラウザ上でモジュール管理機構を実現するライブラリも注目されるようになった。

このようなJavaScriptコードの結合やモジュール管理機構を実現するためのツールの1つが、今回紹介するwebpackだ(図1)。

図1 webpackの公式Webサイト
図1 webpackの公式Webサイト

webpackでは適切な順序でコードを結合できる

webpackはさまざまな機能を備えているが、その中核となっているのが「CommonJS」や「AMD(Asynchronous Module Definition)」といったモジュール規格に沿って実装されたJavaScriptモジュールを適切に結合して1ファイルにまとめるという機能(アセットバンドル)だ。

かつては複数のJavaScriptファイルを1つに結合する場合、ファイルを指定された順番に結合していく、といったシンプルな手法が多く使われていた。しかし、この手法では手動で対象とするファイルを指定しておく必要があり、必要なファイルを追加し忘れたり、不適切な順序で結合したりするといった設定ミスによる問題が発生しやすい。

webpackではモジュールを参照・ロードするための「require()」や「define()」といった関数を自動で検出し、結合対象とするファイルを自動的かつ適切な順序で追加するようになっている。そのため、最小限の設定だけで処理を実行でき、また依存関係が変わった場合でも設定ファイル等の変更は最小限で済むようになっている。

JavaScriptコードの変換もサポート

また、webpackではJavaScriptモジュールを結合する前後でコードの変換処理を実行することも可能だ。

ここ数年、JavaScriptでは言語的な機能強化が活発に行われている。しかし、まだ一部のWebブラウザではこういった機能強化に対応できておらず、最新の機能を使ったコードは実行できない。そのため、最新の機能を使って記述したコードを古いWebブラウザでも実行できるように変換するツールが登場している。また、TypeScriptなどの「JavaScriptコードにコンパイルできるプログラミング言語」の採用例も増えている。webpackではこういった変換処理を自動かつ容易に実行でき、コマンド1つでコードの変換/コンパイルから結合までをまとめて処理できるようになっている。

JavaScript以外のリソースも扱える

webpackでは、JavaScriptだけでなくCSSや画像といったリソースを扱うことも可能だ。今日では「Sass」や「Less」といったCSSにコンパイルできるスタイルシート言語を活用する例も増えているが、webpackはこういったCSSファイルの変換についてもサポートされている。

ただし、webpackは基本的にはJavaScriptを処理するためのツールとして設計されているため、JavaScript以外のリソースを扱う場合の設定や挙動にクセがある。設定によってはCSSや画像といったリソースをJavaScriptファイル内に埋め込んですべてJavaScriptで操作するということも可能だが、これについてはメリットとデメリットの両方があるため注意したい。もちろん、設定次第で別ファイルとしてこれらを出力させることもできる。

開発支援のためのサーバー機能も利用可能

WebサイトやWebアプリケーションのデバッグ時には、頻繁にJavaScriptコードやCSS、各種リソースなどを書き換えることになる。その場合、変更が発生するたびに手動でそれらのバンドル処理を実行するのは手間だ。そのためwebpackではファイルへの変更を検出して自動でバンドル処理を再実行する機能が用意されているほか、リソースのリクエストに応じて動的に変換・バンドル処理を実行できる開発用サーバー機能も用意されている。

webpackのインストールと使い方

それでは、実際にwebpackを利用するための環境構築や使い方について紹介していこう。なお、本記事では原則として記事執筆時の最新安定版であるwebpack v4.32.2をベースに解説を行っているが、一部環境向けにその1つ前のバージョンであるwebpack v3系を利用する場合の例も紹介していく。

ディストリビューション公式パッケージマネージャ経由でのインストール

まずwebpackのインストールだが、Linux環境の場合、ディストリビューションがパッケージを提供している場合がある。たとえばDebianの場合、testing(buster)およびunstable(sid)リポジトリにおいて、「webpack」パッケージとしてバージョン3.5.6が提供されている。また、Ubuntuでは18.04 LTS(bionic)以降で同じく「webpack」パッケージとしてバージョン3.5.6が提供されており、aptコマンドでインストールできる。

# apt install webpack

この場合、「webpack」という名称でwebpackを実行できるようになる。ただし、提供されているwebpackは最新安定版の1つ前の安定版であるバージョン3系である点に注意したい。

npmでのインストール

webpackはJavaScriptで実装されており、Node.jsのパッケージマネージャであるnpm経由でもインストールできる。この場合、インストールや実行には別途Node.jsが必要となる。記事執筆時の最新版であるwebpack v4.32.0ではNode.js 6.11.5以降が必要となっているが、できるだけ新しいバージョンのNode.jsを利用することが推奨されている。Node.jsにおいては長期サポートが提供されるLTS版が用意されており、現在であれば最新のLTS版であるバージョン10系を利用するのが良いだろう。

npm経由でインストールする場合、次のように「npm install」コマンドで最新版のwebpackをインストールできる。ここで指定している「-D」(「–save-dev」)オプションは開発時に依存するパッケージも同時にインストールすることを指示するものだ。通常この作業は、webpackで処理したいソースコードが格納されているディレクトリの親ディレクトリで実行する。

npm install -D webpack

最新安定版以外を利用したい場合、次のようにしてバージョンを指定してインストールすることもできる。

npm install -D webpack@<バージョン>

なお、webpack 4.0以降ではコマンドラインインターフェイス(CLI)は「webpack-cli」という別パッケージで提供されているので、こちらも別途インストールしておく必要がある。

$ npm install -D webpack-cli

いずれの場合も、webpackはnpmコマンドを実行したディレクトリ以下の「node_modules」ディレクトリ以下にインストールされる。このとき、webpackを実行するためのコマンドは下記のようになる。

$ ./node_modules/webpack-cli/bin/cli.js

ディストリビューションのパッケージマネージャとnpmどちらを選択すべきか

ディストリビューションのパッケージマネージャ経由でのインストールとnpm経由でのインストールはどちらもメリット・デメリットがある。ディストリビューションのパッケージマネージャ経由の場合、自動アップデート機能が利用できるほか、そのディストリビューション環境で安定的に動作することがある程度保証されている。一方で、提供されているバージョンが古いことも多い。また、webpackではプラグインによって機能を追加できるが、使用したいプラグインがディストリビューション公式のパッケージでは提供されていない場合もある。そのため、特に事情がない限りは最新版が利用できるnpm経由でのインストールをおすすめしたい。

webpackでJavaScriptのバンドル処理を実行する

続いては、webpackを使って実際に複数ファイルから構成されるソースコードを1ファイルにまとめる流れを紹介しよう。ここではサンプルとして、「index.js」と「foo.js」という2つのファイルを用意した。index.jsは、次のように「foo.js」で定義されているrun()関数を実行するだけのものだ。

// index.js
const foo = require('./foo.js');

foo.run('hello, world!');

また、foo.jsでは次のようにrun()関数を定義してエクスポートしている。

// foo.js
module.exports.run = function run(msg) {
    console.log(msg);
}

このindex.jsをNode.jsで実行すると、次のように「hello, world!」というメッセージが表示される。

$ node index.js
hello, world!

webpackでは「webpack.config.js」という設定ファイルで対象のファイルなどを指定する。この設定ファイルは、拡張子が「.js」となっていることからも分かるように、JavaScriptファイルとして処理され、このファイル中でmodule.exportsオブジェクトにプロパティを定義することで設定を行う仕組みとなっている。たとえば次の例は「./src/index.js」を処理対象とし、実行結果を「./dist/main.js」というファイルに保存する場合の設定だ。

// webpack.config.js

module.exports = {
    entry: './src/index.js',
    output: {
        path: __dirname + '/dist',
        filename: 'main.js',
    }
};

ここでは「entry」プロパティが処理対象のファイル、「output」プロパティが出力先を示している。ちなみに「__dirname」はこのファイルが格納されているディレクトリを示す変数だ。

なお、webpackを利用する場合、ソースコードを「src」ディレクトリ以下に、出力ファイルは「dist」ディレクトリ以下に配置し、webpack.config.jsファイルはこれらディレクトリの親ディレクトリに配置することが慣例となっている。

さて、index.jsとfoo.jsという2つのファイルをsrcディレクトリ内に作成し、その親ディレクトリにwebpack.config.jsファイルを作成したら、webpack.config.jsファイルのあるディレクトリでwebpackを実行してみよう。

npm経由でインストールしたwebpackを利用したい場合、webpack.config.jsファイルのあるディレクトリにwebpackやwebpack-cliをインストールし、そのうえで./node_modules/webpack-cli/bin/cli.jsを実行する。

npm install -D webpack webpack-cli

webpack 4系では実行時に「–mode」オプションを指定することが推奨されている。このオプションの引数には「development」もしくは「production」、「none」が指定可能で、これによってwebpack実行時の挙動が変化する。ここでは開発時向けのオプションである「development」を指定している。

$ ./node_modules/webpack-cli/bin/cli.js --mode=development
Hash: 3d510931611f4cf3cfcc
Version: webpack 4.32.0
Time: 88ms
Built at: 05/21/2019 10:36:01 AM
  Asset      Size  Chunks             Chunk Names
main.js  4.18 KiB    main  [emitted]  main
Entrypoint main = main.js
[./src/foo.js] 66 bytes {main} [built]
[./src/index.js] 61 bytes {main} [built]

ちなみに、webpack 4系では設定ファイル(webpack.config.js)を作成せずにコマンドを実行しても同様の結果が得られる。webpack 4系ではデフォルトで入力ファイルとして./src/index.jsを、出力先として./dist/main.jsを使用するよう暗黙的に設定されているためだ。

また、aptなどのパッケージマネージャでwebpackをインストールしていた場合は、そのままこのディレクトリでwebpackコマンドを実行すれば良い。

$ webpack
Hash: a4f9b6be5b8c89b7815f
Version: webpack 3.5.6
Time: 58ms
         Asset     Size  Chunks             Chunk Names
./dist/main.js  2.68 kB       0  [emitted]  main
   [0] ./src/index.js 61 bytes {0} [built]
   [1] ./src/foo.js 66 bytes {0} [built]

どちらの場合も「-w」オプション付きで実行することで、処理対象とするファイルを監視する「watch」モードになり、処理を実行したあとに待機状態となる。この場合、処理対象ファイルが変更されたら自動的に再度変換処理が実行される。

さて、webpackを実行すると、設定ファイルには記述されていないfoo.jsについても自動的に処理対象に加わっていることが分かる。これは、index.js内でrequire()関数を使ってfoo.jsを読み込むよう指定しているためだ。最終的にはこの2つのファイルが結合され、その結果がdistディレクトリ以下の「main.js」というファイルに出力される。

ここで出力されたmain.jsを実行すると、index.jsを実行した場合と同様に「hello, world!」という文字列が表示されることが分かる。

$ node dist/main.js
hello, world!

生成されたソースコードを見てみると、次のようにファイルの先頭にwebpackによっていくつかのユーティリティ関数が追加され、続いて元々のソースコードを変形したものが結合されていることが分かる。

/******/ (function(modules) { // webpackBootstrap
/******/        // The module cache
/******/        var installedModules = {};
/******/
/******/        // The require function
/******/        function __webpack_require__(moduleId) {


/***/ "./src/foo.js":
/*!********************!*\
  !*** ./src/foo.js ***!
  \********************/
/*! no static exports found */
/***/ (function(module, exports) {

eval("module.exports.run = function run(msg) {\n    console.log(msg);\n}\n\n\n\n//# sourceURL=webpack:///./src/foo.js?");

/***/ }),

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

eval("const foo = __webpack_require__(/*! ./foo.js */ \"./src/foo.js\");\n\nfoo.run('hello, world!');\n\n\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

/******/ });

ファイルをminifyした上で出力する

このようにwebpackで出力したコードはそのままでは若干冗長なものになっているが、この問題は出力ファイルをminifyするように設定することで解決できる。minifyとは、不要なスペースやインデントなどを削除したり、関数名や変数名を短いものに置き換えたりすることでソースコードのサイズを圧縮する処理のことだ。minifyされたソースコードは人間には読みにくくなってしまうが、プログラムとしては問題なく実行できる。

webpackではいくつかのminify用プラグインが用意されているが、webpack 4系では前述の「–mode」オプションで「production」を指定するだけで自動的に出力コードがminifyされる。

$ ./node_modules/webpack-cli/bin/cli.js --mode=production

この場合、出力されるmain.jsファイルは以下のようになる。

!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="",t(t.s=0)}([function(e,n,t){t(1).run("hello, world!")},function(e,n){e.exports.run=function(e){console.log(e)}}]);

ちなみに、minifyを行うかどうかの設定はwebpack.config.jsの「optimization」プロパティでも設定できる。たとえば常にminifyを無効にするには次のように「minimize」プロパティをfalseに設定すれば良い。

// webpack.config.js

module.exports = {
    entry: './src/index.js',
    output: {
        path: __dirname + '/dist',
        filename: 'main.js',
    },
    optimization: {
        minimize: false,
    }
};

このminimizeプロパティをtrueにすれば「–mode=development」指定時にもminifyが実行されるようになるが、「–mode=production」指定をした場合には単にminifyが有効になるだけでなく、複数のプラグインが同時に有効になる。そのため出力結果は異なるものになる点に注意したい。その辺りの挙動については、ドキュメントの「mode」ページに記載されている。

ちなみにwebpack 3系では「mode」オプションは用意されていないが、次のように「UglifyJsPlugin」を利用するようwebpack.config.jsに記述することでminifyを行える。

// webpack.config.js
const webpack = require("webpack");

module.exports = {
    entry: './src/index.js',
    output: {
        path: __dirname + '/dist',
        filename: 'main.js',
    },
    plugins: [ new webpack.optimize.UglifyJsPlugin(), ],
};

SourceMapを生成する

minifyしたソースコードは人間の目には読みにくくデバッグしにくいというデメリットがある。この問題は、minifyしたソースコードとオリジナルのソースコードとの対応付けを定義した「SourceMap」というファイルを用意することで解決できる。SourceMapファイルをJavaScriptデバッガに読み込ませると、デバッガはこの情報を利用してminifyしたソースコードと変換元のソースコードを対応付けて表示できるようになる。

webpackでは、webpack.config.jsの「devtool」プロパティでSourceMapの生成を指定できる。

// webpack.config.js

module.exports = {
    entry: './src/index.js',
    output: {
        path: __dirname + '/dist',
        filename: 'main.js',
    },
    devtool: "source-map"
};

SourceMapにはいくつかの形式があるが、ここでは結合・minify前のソースコードすべてを1ファイルにまとめた「source-map」形式を指定している。これ以外の利用できる形式についてはdevtoolオプションのドキュメントを参照してほしい。

この設定を追加してwebpackを実行すると、出力ファイルと同じディレクトリ内に、出力ファイルと同じファイル名で拡張子が「.js.map」のファイルとしてSourceMapが作成される(今回の例では「./dist/main.js.map」)。

$ ./node_modules/webpack-cli/bin/cli.js --mode=production
Hash: 532ced80e4cc6849ce05
Version: webpack 4.32.0
Time: 584ms
Built at: 05/22/2019 11:02:36 AM
      Asset      Size  Chunks             Chunk Names
    main.js  1.02 KiB       0  [emitted]  main
main.js.map  4.71 KiB       0  [emitted]  main
Entrypoint main = main.js main.js.map
[0] ./src/index.js 61 bytes {0} [built]
[1] ./src/foo.js 66 bytes {0} [built]

なお、webpack 3系の場合は次のようにUglifyJsPluginの「sourceMap」オプションでsourceMapを生成するかどうかを指定できる(ドキュメント)。

// webpack.config.js
const webpack = require("webpack");

module.exports = {
    entry: './src/index.js',
    output: {
        path: __dirname + '/dist',
        filename: 'main.js',
    },
        plugins: [ new webpack.optimize.UglifyJsPlugin({ sourceMap: true }), ]
};

Babelを使ってJavaScript(ECMAScript)の最新機能を使ったコードを変換する

JavaScriptはECMAScriptという名称で標準規格化されており、2015年に約6年ぶりのメジャーアップデート版となるECMAScript 6(ECMAScript 2015)がリリースされた。ECMAScript 6ではさまざまな新機能や新しい文法が追加され、また、2016年以降もECMAScript規格はアップデートが続けられている。こういった新機能を利用することでより簡潔にコードを記述できる一方で、新機能をサポートしていないWebブラウザもまだ存在している。そこで、こういったECMAScriptの新機能を使ったコードを、それらをサポートしていないWebブラウザで利用できるようなコードに変換するツールが登場している。その中でも有名なのが「Babel」というものだ(図2)。

図2 Babelの公式Webサイト
図2 Babelの公式Webサイト

webpackでは、こういったツールを利用してJavaScriptの変換処理を実行した後にバンドル処理を実行することができる。たとえば、ECMAScript 6で追加されたものの、Internet Explorerでは利用できない「アロー関数(Arrow Functions)」を使うコードを変換してみよう。

アロー関数は関数オブジェクトを定義するもので、次のように関数をプロパティや変数に代入する際に利用される。

// foo.js
module.exports.run = msg => {
    console.log(msg);
};

webpackでBabelを利用する場合、Babel(「@babel/core」パッケージ)のほか「babel-loader」というモジュールが必要になる。これらはnpmでインストールできる。

npm install -D babel-loader @babel/core @babel/preset-env

なお、「@babel/preset-env」パッケージは変換設定(プリセット)を指定する際に使われる支援パッケージだ。

これらを利用するためのwebpack.config.jsファイルは次のようになる。

// webpack.config.js

module.exports = {
    entry: './src/index.js',
    output: {
        path: __dirname + '/dist',
        filename: 'main.js',
    },
    module: {
        rules: [
            {
                test: /\.js$/, // .jsファイルを処理対象として指定
                use: { // testプロパティにマッチしたファイルに対する処理を指定
                    loader: 'babel-loader',  // 「babel-loader」を利用することを指定
                    options: { // babel-loaderのオプションを指定
                        presets: [ // プリセットを選択する
                            [
                                '@babel/preset-env',  // プリセットとして「@babel/preset-env」を使用する
                                {
                                    targets: { ie: 11 } // ターゲットとしてIE11を指定
                                }
                            ]
                        ]
                    }
                }
            }
        ]
    }
};

webpack.config.jsでは、「module」という単位でファイルをどのように処理するかを設定できる仕組みになっている。具体的な処理ルールは「rules」プロパティで指定する。今回の例では、処理対象のファイル名を正規表現で指定する「test」プロパティに「/\.js$/」を設定することで、ファイル名が「.js」で終わるファイルを処理対象にするよう指定している。

また、どのような処理を実行するかは「use」プロパティで指定する。ここでは「loader」として「babel-loader」を指定し、さらに「option」プロパティで使用するプリセットに関する設定を行っている。Babelのプリセットに関して詳しくはドキュメントを参照して欲しいが、@babel/preset-envではブラウザを指定することで、そのブラウザで実行できるコードを出力できるようになっている。今回は対象のブラウザとして「ie: 11」、つまりInternet Explorer 11を指定している。

さて、このように指定した状態で、developmentモードでwebpackを実行してみよう。

$ ./node_modules/webpack-cli/bin/cli.js --mode=development
Hash: 6e14d460051dd7d4c266
Version: webpack 4.32.0
Time: 430ms
Built at: 05/24/2019 11:26:45 AM
  Asset      Size  Chunks             Chunk Names
main.js  4.22 KiB    main  [emitted]  main
Entrypoint main = main.js
[./src/foo.js] 77 bytes {main} [built]
[./src/index.js] 72 bytes {main} [built]

この場合の出力コードを見ると、「foo.js」の部分はアロー関数ではなく、次のようにfunctionキーワードを使ったものに変換されていることが確認できる。

/***/ "./src/foo.js":
/*!********************!*\
  !*** ./src/foo.js ***!
  \********************/
/*! no static exports found */
/***/ (function(module, exports) {

eval("module.exports.run = function (msg) {\n  console.log(msg);\n};\n\n//# sourceURL=webpack:///./src/foo.js?");

/***/ }),

また、ここでは変換後のコードがそのまま出力されているが、modeとして「production」を指定するとminifyされたコードが出力される。

なお、webpack 3の場合も基本的な設定方法は変わらない。また、Babelやbabel-loaderについてはUbuntuの場合次のようにapt経由でもインストールできる。

# apt install node-babel-loader node-babel-preset-env

ただし、このようにapt経由でインストールしたモジュールを利用する場合はresolveLoader.modulesプロパティでそのインストール先(/usr/lib/nodejs)を指定しておく必要がある。

// webpack.config.js
const babelLoader = require('babel-loader');

module.exports = {
    entry: './src/index.js',
    output: {
        path: __dirname + '/dist',
        filename: 'main.js',
    },
    resolveLoader: {
        modules: [
            '/usr/lib/nodejs',
            'node_modules',
        ],
    },
    module: {
        rules: [
            {
                test: /\.js/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            [
                                'env',
                                {
                                    targets: { ie: 11 }
                                }
                            ]
                        ]
                    }
                }
            }
        ]
    }
};

TypeScriptを使ったコードを利用する

昨今ではTypeScriptという、JavaScriptに静的型付けなどの機能を追加した言語を利用する例が増えている。たとえば、先に紹介した「index.js」や「foo.js」をTypeScriptで記述すると、次のようになる。

// index.ts

import * as foo from './foo';

foo.run('hello, world!');
// foo.ts

export function run(msg: string) {
    console.log(msg);
}

webpackでは、Babelを使った場合と同様に「ts-loader」を利用することでTypeScriptで記述したコードをJavaScriptに変換してからバンドルできる。これを利用するには、まずnpmコマンドで「typescript」および「ts-loader」モジュールをインストールする。

$ npm install -D typescript ts-loader

ts-loaderを利用するには、webpack.config.jsファイルと同じディレクトリにTypeScriptコンパイラ(tsc)向けの設定ファイルである「tsconfig.json」が必要だ。これは次のようにして生成できる。

./node_modules/typescript/bin/tsc --init

このtsconfig.jsonファイルを編集することでtscの挙動をカスタマイズできるが、今回は生成されたものをそのまま使用している。

webpack.config.jsは次のようになる。ここではbabel-loaderを利用したのと同様に「module」プロパティでルールの設定を追加したほか、「resolve」プロパティの設定を行っている。このプロパティは依存するファイルを探索する際の拡張子を指定するもので、ここではTypeScriptファイルの拡張子である「.ts」を追加している。

// webpack.config.js

module.exports = {
    entry: './src/index.ts',
    output: {
        path: __dirname + '/dist',
        filename: 'main.js',
    },
    module: {
        rules: [
            {
                test: /\.ts$/,
                use: 'ts-loader'
            }
        ]
    },
    resolve: {
        extensions: [ '.ts', '.js' ]
    },
};

先ほどと同様、modeとして「development」を指定してwebpackを実行すると、「foo.ts」と「index.ts」がwebpackによって処理されていることが分かる。

$ ./node_modules/webpack-cli/bin/cli.js --mode=development
Hash: e70b96cb37e3527863e5
Version: webpack 4.32.0
Time: 872ms
Built at: 05/24/2019 11:41:20 AM
  Asset      Size  Chunks             Chunk Names
main.js  4.67 KiB    main  [emitted]  main
Entrypoint main = main.js
[./src/foo.ts] 150 bytes {main} [built]
[./src/index.ts] 439 bytes {main} [built]

また、出力コードもTypeScriptからJavaScriptに変換されたものになっていることが確認できる。

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

"use strict";
eval("\n// foo.ts\nObject.defineProperty(exports, \"__esModule\", { value: true });\nfunction run(msg) {\n    console.log(msg);\n}\nexports.run = run;\n\n\n//# sourceURL=webpack:///./src/foo.ts?");

/***/ }),

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

"use strict";
eval("\n// index.ts\nvar __importStar = (this && this.__importStar) || function (mod) {\n    if (mod && mod.__esModule) return mod;\n    var result = {};\n    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];\n    result[\"default\"] = mod;\n    return result;\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nvar foo = __importStar(__webpack_require__(/*! ./foo */ \"./src/foo.ts\"));\nfoo.run('hello, world!');\n\n\n//# sourceURL=webpack:///./src/index.ts?");

/***/ })

なお、webpack 3系の場合も設定方法は同じだ。ただし、ts-loaderの最新版はwebpack 4以上でしか利用できないので、次のようにバージョン3系を明示的に指定してインストールする必要がある。

 npm install typscript ts-loader@3.x

TypeScriptについてはUbuntuでは公式パッケージも提供されており、npmでインストールせずにそちらを利用することもできる。

# apt install node-typescript

CSSファイルの処理

webpackではCSSファイルの結合やminifyも行える。CSSの出力先としては単一のCSSファイルだけでなく、JavaScriptやHTML内などさまざまなものが指定できるが、今回はもっとも汎用的な例として複数のCSSファイルを1つのCSSファイルに結合する、という設定例を紹介する。

webpackではCSSを処理するためのモジュールとして「css-loader」が用意されている。また、CSSをファイルに出力するには「mini-css-extract-plugin」プラグインも必要となる。まずはこれらをインストールしておく。

$ npm install -D mini-css-extract-plugin css-loader

また、今回処理するCSSファイルは次の「index.css」と「reset.css」の2つだ。CSSではほかのCSSファイルをロードする「@import」というルールが用意されており、これを使ってindex.cssからreset.cssをロードするように指定している。

/* index.css */

@import "reset.css";

body {
    line-height: 180%;
}



/* reset.css */
* {
    margin: 0;
    padding: 0;
}

css-loaderおよびmini-css-extract-pluginを使用する設定は以下のようになる。

// webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    entry: {
        main: './src/js/index.js',
        css: './src/css/index.css',
    },
    output: {
        path: __dirname + '/dist',
        filename: '[name].js',
    },

    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].css',
        }),
    ],
    module: {
        rules: [
            {
                test: /\.css$/, // 拡張子が.cssのファイルを対象とする
                use: [
                    { // MiniCssExtractPlugin.loaderを指定
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            publicPath: '../',
                        },
                    },
                    'css-loader',
                ],
            },
        ],
    },
};

ここでは、まずentryプロパティとして「main」と「css」の2つを指定している。webpackでは最終的に出力されるファイル等の塊を「chunk」(チャンク)と呼び、このように指定することでindex.jsから「main」チャンクを、「index.css」から「css」チャンクを生成することを指示している。また、outputプロパティの「filename」プロパティで「[name]」という文字列を指定しているが、これはチャンク名(先の例では「main」および「css」)に置換される。

「plugins」プロパティは使用するプラグインを指定するもので、ここでMiniCssExtractPluginを使用するよう指示している。引数にはプラグインの挙動を指定するオプションを指定でき、ここでは出力ファイル名を指定する「filename」プロパティに「[name].css」を指定している。

moduleプロパティでは、拡張子が「.css」のファイルを対象にMiniCssExtractPlugin.loaderで処理を行うよう指定している。ここで注意したいのが、CSSをJavaScript形式で処理できるように変換する「css-loader」も一緒に指定する必要がある点だ。webpackでは、デフォルトでは処理したファイルを最終的にJavaScriptファイルに変換しようとするが、css-loaderを指定しないとCSSをそのままJavaScriptとして扱ってしまいエラーとなる。

さて、このような設定を用意したうえでwebpackを実行すると、次のようにindex.jsのほかindex.cssファイルも処理される。

$ ./node_modules/webpack-cli/bin/cli.js --mode=development
Hash: 79765080b8a99e141fe5
Version: webpack 4.32.0
Time: 178ms
Built at: 2019/06/14 7:16:39
  Asset       Size  Chunks             Chunk Names
css.css  297 bytes     css  [emitted]  css
 css.js   3.86 KiB     css  [emitted]  css
main.js   4.21 KiB    main  [emitted]  main
Entrypoint main = main.js
Entrypoint css = css.css css.js
[./src/css/index.css] 39 bytes {css} [built]
[./src/js/foo.js] 56 bytes {main} [built]
[./src/js/index.js] 61 bytes {main} [built]
    + 2 hidden modules
Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!src/css/index.css:
    Entrypoint mini-css-extract-plugin = *
    [./node_modules/css-loader/dist/cjs.js!./src/css/index.css] 504 bytes {mini-css-extract-plugin} [built]
    [./node_modules/css-loader

また、distディレクトリには次のように3つのファイルが出力される。main.jsはindex.jsから作られたJavaScriptファイル、css.cssはindex.cssから作られたCSSファイルだ。

$ ls dist
css.css css.js main.js

css.cssの中身を見てみると、次のように「@import」で指定したreset.cssとindex.cssが結合されていることが分かる。

/* reset.css */
* {
    margin: 0;
    padding: 0;
}

/* index.css */

body {
    line-height: 180%;
}



最後のcss.jsだが、こちらは本来であれば指定したCSSが格納されたJavaScriptコードが出力される。ただし、ここではMiniCssExtractPluginを使用しているため、次のように実質的には何も実行しないコードが出力されている。




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

eval("// extracted by mini-css-extract-plugin\n\n//# sourceURL=webpack:///./src/css/index.css?");

/***/ })

/******/ });

ちなみに、MiniCssExtractPluginを使用しないでcss-loaderのみを指定した場合、次のような内容になる。




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

eval("exports = module.exports = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\")(false);\n// Imports\nexports.i(__webpack_require__(/*! -!../../node_modules/css-loader/dist/cjs.js!./reset.css */ \"./node_modules/css-loader/dist/cjs.js!./src/css/reset.css\"), \"\");\n\n// Module\nexports.push([module.i, \"/* index.css */\\n\\nbody {\\n    line-height: 180%;\\n}\\n\\nh1, h2, h3, h4, h5, h6 {\\n    font-size: 100%;\\n}\\n\\nli {\\n    margin-left: 1.5em;\\n}\\n\\np {\\n    margin-bottom: 1em;\\n}\\n\\na {\\n    text-decoration: none;\\n}\\n\\na:hover {\\n    text-decoration: underline;\\n}\\n\\n\", \"\"]);\n\n\n\n//# sourceURL=webpack:///./src/css/index.css?");

/***/ })

/******/ });

なお、webpack 3系ではmini-css-extract-pluginではなく「extract-text-webpack-plugin」を使用する。

npm install -D extract-text-webpack-plugin css-loader

また、その場合設定ファイルは次のようになる。

// webpack.config.js

const ExtractTextPlugin = require("extract-text-webpack-plugin");
var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
    entry: {
        main: './src/js/index.js',
        css: './src/css/index.css',
    },
    output: {
        path: __dirname + '/dist',
        filename: '[name].js',
    },
    plugins: [
        new ExtractTextPlugin("main.css"),
        new OptimizeCssAssetsPlugin(),
    ],
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ExtractTextPlugin.extract({
                    use: [
                        {
                            loader: 'css-loader',
                        },
                    ]
                })
            }
        ]
    },
};

CSSをminifyする

CSSはJavaScriptの場合と異なり、明示的に指定しない限りmodeを「production」に指定してもそのままではCSSはminifyされない。CSSのminifyを行うには「optimize-css-assets-webpack-plugin」を使用する。

npm install -D optimize-css-assets-webpack-plugin

このプラグインを「optimization」プロパティの「minimizer」プロパティで指定すれば良い。具体的な設定ファイルは次のようになる。

// webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
    entry: {
        main: './src/js/index.js',
        css: './src/css/index.css',
    },
    output: {
        path: __dirname + '/dist',
        filename: '[name].js',
    },
    optimization: {
        minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].css',
        }),
    ],
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            publicPath: '../',
                        },
                    },
                    'css-loader',
                ],
            },
        ],
    },
};

なお、optimization.minimizerプロパティを指定すると、デフォルトでJavaScriptのminifyにも影響がある。そのため、ここではJavaScript向けのminifyプラグインである「TerserJSPlugin」も同時に指定している。

これらを指定した状態でwebpackをproductionモードで実行すると、CSSファイルについてもminifyされるようになる。

$ ./node_modules/webpack-cli/bin/cli.js --mode=production
Hash: 873437dd55283f73143e
Version: webpack 4.32.0
Time: 557ms
Built at: 05/23/2019 11:23:54 AM
   Asset        Size  Chunks             Chunk Names
  css.js   934 bytes       0  [emitted]  css
main.css   174 bytes       0  [emitted]  css
 main.js  1010 bytes       1  [emitted]  main
Entrypoint main = main.js
Entrypoint css = main.css css.js
[0] ./src/js/index.js 61 bytes {1} [built]
[1] ./src/js/foo.js 56 bytes {1} [built]
[2] ./src/css/index.css 39 bytes {0} [built]
    + 2 hidden modules
Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!src/css/index.css:
    Entrypoint mini-css-extract-plugin = *
    [1] ./node_modules/css-loader/dist/cjs.js!./src/css/index.css 504 bytes {0} [built]
    [2] ./node_modules/css-loader/dist/cjs.js!./src/css/reset.css 202 bytes {0} [built]
        + 1 hidden module

この例の場合、出力されるCSSは次のようになる。

*{margin:0;padding:0}body{line-height:180%}h1,h2,h3,h4,h5,h6{font-size:100%}li{margin-left:1.5em}p{margin-bottom:1em}a{text-decoration:none}a:hover{text-decoration:underline}

webpack 3系でもoptimize-css-assets-webpack-pluginは利用可能だ。ただし、webpack 3系で利用する場合はバージョン3系を明示的に指定してインストールする必要がある。

npm install -D optimize-css-assets-webpack-plugin@3.x

この場合の設定ファイルは次のようになる。

// webpack.config.js

const ExtractTextPlugin = require("extract-text-webpack-plugin");
var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
    entry: {
        main: './src/js/index.js',
        css: './src/css/index.less',
    },
    output: {
        path: __dirname + '/dist',
        filename: '[name].js',
    },
    plugins: [
        new ExtractTextPlugin("main.css"),
        new OptimizeCssAssetsPlugin(),
    ],
    module: {
        rules: [
            {
                test: /\.less$/,
                use: ExtractTextPlugin.extract({
                    use: [
                        {
                            loader: 'css-loader',
                            options: {},
                        },
                    ]
                })
            }
        ]
    },
};

「less」や「sass」などのCSS変換ツールを利用する

昨今では「less」や「sass」といったスタイルシート言語を使用してCSSを記述することも多い。この場合、変換ツールを利用して最終的にこれらの言語で記述されたコードをCSSに変換して使用する形になるが、webpackでは「less-loader」や「sass-loader」を使用することで変換処理を実行できる。

たとえば、次のような「index.less」および「reset.less」というlessファイルを使用する例を見てみよう。

/* index.less */

@anchor-color: #634B2C;
@anchor-color-hover: #3b78e7;
@background-color: #fbf9f7;
@foreground-color: #1a150d;

@import "reset.less";

body {
    line-height: 180%;
    background-color: @background-color;
    color: @foreground-color;
}

h1, h2, h3, h4, h5, h6 {
    font-size: 100%;
}


/* reset.less */
* {
    margin: 0;
    padding: 0;
}

この場合、まずlessおよびless-loaderをインストールする。

npm install -D less-loader less

そして、設定ファイルではmodules.rulesの「test」プロパティで拡張子が.lessのファイルを対象にするよう指定したうえで「css-loader」の後に「less-loader」を追加すれば良い。

// webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
    entry: {
        main: './src/js/index.js',
        css: './src/css/index.less',
    },
    output: {
        path: __dirname + '/dist',
        filename: '[name].js',
    },
    optimization: {
        minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].css',
        }),
    ],
    module: {
        rules: [
            {
                test: /\.less$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            publicPath: '../',
                        },
                    },
                    'css-loader',
                    'less-loader',
                ],
            },
        ],
    },
};

これでwebpackを実行すると、自動的にlessでの処理が行われてその結果が出力される。

$ ./node_modules/webpack-cli/bin/cli.js --mode=development
Hash: fc1c2482591a6c8390ad
Version: webpack 4.32.0
Time: 204ms
Built at: 05/23/2019 12:23:03 PM
   Asset       Size  Chunks             Chunk Names
  css.js   3.86 KiB     css  [emitted]  css
main.css  357 bytes     css  [emitted]  css
 main.js   4.21 KiB    main  [emitted]  main
Entrypoint main = main.js
Entrypoint css = main.css css.js
[./src/css/index.less] 39 bytes {css} [built]
[./src/js/foo.js] 56 bytes {main} [built]
[./src/js/index.js] 61 bytes {main} [built]
    + 1 hidden module
Child mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!node_modules/less-loader/dist/cjs.js!src/css/index.less:
    Entrypoint mini-css-extract-plugin = *
    [./node_modules/css-loader/dist/cjs.js!./node_modules/less-loader/dist/cjs.js!./src/css/index.less] 531 bytes {mini-css-extract-plugin} [built]
        + 1 hidden module

出力結果は次のようになる。

/* index.less */
/* reset.less */
* {
  margin: 0;
  padding: 0;
}
body {
  line-height: 180%;
  background-color: #fbf9f7;
  color: #1a150d;
}
h1,
h2,
h3,
h4,
h5,
h6 {
  font-size: 100%;
}


ここではdevelopmentモードで実行しているが、もちろんproductionモードで実行すれば出力結果はminifyされたものになる。

なお、sassを利用する場合は「less-loader」ではなく「sass-loader」を指定すれば良い。

npm install -D sass-loader node-sass

また、webpack 3系の場合もほぼ同様にしてlessやsassは利用できる。この場合、設定ファイルは以下のようになる。

// webpack.config.js

const ExtractTextPlugin = require("extract-text-webpack-plugin");
var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
    entry: {
        main: './src/js/index.js',
        css: './src/css/index.less',
    },
    output: {
        path: __dirname + '/dist',
        filename: '[name].js',
    },
    plugins: [
        new ExtractTextPlugin("main.css"),
        new OptimizeCssAssetsPlugin(),
    ],
    module: {
        rules: [
            {
                test: /\.less$/,
                use: ExtractTextPlugin.extract({
                    use: [
                        {
                            loader: 'css-loader',
                            options: {},
                        },
                        {
                            loader: 'less-loader',
                            options: {},
                        },
                    ]
                })
            }
        ]
    },
};

webpackだけで完結させるのではなく、ほかのツールとの組み合わせも考慮しよう

webpackはプラグインでさまざまな機能を提供しており、また設定ファイルも複雑になりやすいため難しいような印象も受けやすいが、ここまでで説明した通り、単純なJavaScriptのバンドルや変換といった処理においては、必要な設定等も少なく、理解しやすいのではないだろうか。特にバージョン4系ではminifyなどの設定をmodeオプションで切り替えられるようになっており、シンプルな設定ファイルで完結するようになっている。

また、webpackではJavaScript以外を扱うことも可能で、今回紹介したようなCSSの処理だけでなく、HTMLファイルを処理することも可能だ。ただ、これらについてはやや設定が複雑になり、また使用ケースも限られるだろう。そのため無理にwebpackだけで完結させようとせず、適宜ほかのツールと組み合わせて利用することも検討するとよいだろう。

なお、2015年に標準化されたECMAScript 2015規格では、「export」や「import」というキーワードでモジュールを読み込む機能(通称「ES6 module」)が導入された。ただ、実装面での問題からWebブラウザにおけるサポートは遅れ、主要ブラウザがサポートするのは2017~2018年になってからという状況で、パフォーマンスなどの面でもまだ懸念点がある。そのため、まだしばらくはwebpackの存在意義はなくならないだろう。