Edited at

今時のフロントエンド開発2017 (3. webpack編)

More than 1 year has passed since last update.


はじめに

本編では今時のフロントエンド開発2017 (2. 構築編)に続きwebpackを使っていきます。


おしながき


webpack


webpack.config.js

webpackでバンドルするファイルの設定をします。

プロジェクトディレクトリにwebpack.config.jsを作ります。

modern-front-end


+-- node_modules
| `-- *
|
+-- src
|
+-- package.json
`-- webpack.config.js <

webpack.config.jsに次のように記述します。


webpack.config.js

var path = require('path');

module.exports = [{
entry: ['./src/app.js'],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public')
}
}];


各項目の意味


entry


バンドルのエントリポイントとなるファイルのパスを指定します。

この場合は`modern-front-end/src/app.js`がバンドルのエントリポイントになります。

output


ビルド時の出力に関するオプションを指定します。

output.filename


出力するファイル名を指定します。

output.path


出力するディレクトリを絶対パスで指定します。

この場合は`modern-front-end/public/`にビルドしたファイルが出力されます。


webpackのエントリポイントを作成する

modern-front-end/src/app.jsとなるようにファイルを作成します。

modern-front-end


+-- node_modules
| `-- *
|
+-- src
| `-- app.js <
|
+-- package.json
`-- webpack.config.js

次にapp.jsに以下のように処理を記述します。

今回は標準出力をしてみます。


app.js

console.log('hello, world');



HTMLを書く

modern-front-end/public/index.htmlとなるようにHTMLを作成します。

modern-front-end

|
+-- node_modules
| `-- *
|
+-- public <
| `-- index.html <
|
+-- src
| `-- app.js
|
+-- package.json
`-- webpack.config.js

作成したらサクッとHTMLを書いてbundle.jsを読み込みましょう。

Emmetを使うと今後も楽にマークアップできるのでオススメです。


index.html

<!DOCTYPE html>

<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>modern-front-end</title>
<script src="./bundle.js"></script>
</head>
<body>
<p>hello, world</p>
</body>
</html>


npm-scriptsを設定する

npm-scriptsを通してwebpackを実行する準備をします。

package.jsonのscripts"build": "webpack"を追記します。


package.json

{

"scripts": {
"webpack": "webpack"
}
}

npm-scriptsはnpm runで実行できます。


webpackでビルドしてみる

それでは実際にwebpackでビルドしてみます。

プロジェクトディレクトリで次のコマンドを実行してみましょう。

npm run webpack

以下のようなログが出力されれば成功です。

Version: webpack 2.3.2

Child
Hash: fedbbe24aa69ef7f1937
Time: 43ms
Asset Size Chunks Chunk Names
bundle.js 2.78 kB 0 [emitted] main
[0] ./src/app.js 29 bytes {0} [built]
[1] multi ./src/app.js 28 bytes {0} [built]

すると,modern-front-end/public/bundle.jsというファイルができあがっているのが確認できます。

modern-front-end

|
+-- node_modules
| `-- *
|
+-- public
| +-- bundle.js <
| `-- index.html
|
+-- src
| `-- app.js
|
+-- package.json
`-- webpack.config.js

ブラウザでmodern-front-end/public/index.htmlを開き,デベロッパーツールのコンソールで標準出力できているか確認します。


バンドルファイルを圧縮する

modern-front-end/public/bundle.jsを開いてみると長々と何か書いてあります。

しかし,改行やらインデントやらでこれでは冒頭に問題としてあげた”無駄の多いデータ”そのものなので圧縮します。

bundle.jsの圧縮は--optimize-minimizeを指定することできます。

次のコマンドを実行しましょう。

npm runでオプションを付けるには--を間に挟みます。

npm run webpack -- --optimize-minimize

ビルド後もう一度bundle.jsを確認すると無駄な改行やインデントが削除されファイルサイズが小さくなったことがわかります。


デバッグできるようにする

ブラウザのコンソールに表示されているログからconsole.log('hello, world');した箇所をデバッグ画面に表示してみます。

JavaScriptを圧縮してしまうと次の画像のようにすべて1行で出力されるのでデバッグができなくなってしまいます。

そこで,--devtool source-mapオプションを付けてソースマップを作成できるようにします。

次のコマンドを実行しましょう。

npm run webpack -- --optimize-minimize --devtool source-map

ビルドが終わるとmodern-front-end/public/bundle.js.mapというファイルができます。

modern-front-end

|
+-- node_modules
| `-- *
|
+-- public <
| +-- bundle.js
| +-- bundle.js.map <
| `-- index.html
|
+-- src
| `-- app.js
|
+-- package.json
`-- webpack.config.js

デベロッパーツールのSourcesタブの左ペインからwebpack:// ./src app.jsの順で開いて確認してみます。

とても見やすくなりました。


毎回ビルドするのが面倒くさい

毎回ビルドするのが面倒なのでファイルの変更を監視してビルドできるようにします。

--watchオプションを付けることで監視状態になります。

npm run webpack -- --optimize-minimize --devtool source-map --watch

これでapp.jsを更新するたびに自動的にビルドされるようになりました。

抜け出すにはCLIでCtrl-Cを押します。

--watchはキャッシュを利用した差分ビルドによって,更新のあったファイルのみ変更が加えられるので高速にビルドすることができます。


コマンドが長くて面倒くさい

コマンドが長いのでnpm-scriptsを使って工夫します。

package.jsonのscriptsを次のように書き直します。


package.json

{

"scripts": {
"start": "webpack --progress --devtool source-map --watch",
"build": "webpack --progress --optimize-minimize",
}
}

先程までのコマンドとは少し違うので気をつけてください。

--progressはビルドの経過を表示するオプションです。

startは開発用の設定でソースマップの生成や監視状態でビルドします。

buildはプロダクション用で圧縮を有効化しています。

startは特別でnpm startと記述するだけで実行できます。

プロダクション用にビルドする場合はnpm run buildを実行します。


CSSをバンドルする準備

CSSをバンドルするためにwebpack.config.jsにローダーを登録します。


webpack.config.js

var path = require('path');

module.exports = [{
entry: ['./src/app.js'],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public')
},
module: {
rules: [{
test: /\.(css|sass|scss)$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
}]
}
}];



SCSSを書く

CSSをバンドルするためにSassを書いていきます。

SassにはSASS記法とSCSS記法がありますが今回はSCSS記法で書いていきます。

ここらへんはお好みでどうぞ。

modern-front-end/src/scss/style.scssを作成します。

modern-front-end

|
+-- node_modules
| `-- *
|
+-- public
| +-- bundle.js
| +-- bundle.js.map
| `-- index.html
|
+-- src
| +-- scss <
| | `-- style.scss <
| |
| `-- app.js
|
+-- package.json
`-- webpack.config.js

style.scssに以下のように記述します。


style.scss

body {

color: darkcyan;
}

このままではstyle.scssはどこからも参照されていないので,エントリポイントであるapp.jsからstyle.scssを参照します。


app.js

var css = {

style: require('./scss/style.scss')
};

console.log('hello, world');


監視状態のままwebpack.config.jsを変更した場合は反映されないのでビルドし直してください。

ビルドされたらブラウザで確認してみます。

style.scssに書いた内容が<style>で展開されているのがわかります。

画面内のhello, worldの文字色が変わっていることも確認してください。


ベンダープレフィックスを付ける

postcss-loaderautoprefixerを使ってビルド時にベンダープレフィックスを付けるようにします。


webpack.config.js

var path = require('path');

module.exports = [{
entry: ['./src/app.js'],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public')
},
module: {
rules: [{
test: /\.(css|sass|scss)$/,
use: [
'style-loader',
'css-loader',
'sass-loader',
{
loader: 'postcss-loader',
options: {
plugins: function () {
return [
require('autoprefixer')
];
}
}
}
]
}]
}
}];


ベンダープレフィックスを付けてくれるか確認するためにstyle.scssも変更します。

※ブラウザのCSS3対応が進み,以下の例ではプレフィックスがつかないのでその下の 2017/09/20 追記分 を試してください。



style.scss

body {

color: darkcyan;
transition: all .1s ease;
}

ビルドしなおしたらブラウザで確認してみます。

ベンダープレフィックスが自動で付与されていることがわかります。



2017/09/20 追記分


style.scss

.example {

display: flex;
transition: all .5s;
user-select: none;
background: linear-gradient(to bottom, white, black);
}

ビルドしなおしてブラウザで確認すると<head>タグ内の<style>にプレフィックスが付いていることが確認できます。

<style>

.example {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
background: linear-gradient(to bottom, white, black);
transition: all .5s;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>


url-loaderを使ってみる

url-loaderはCSS中で使用するアセットをBase64エンコードしたdata URIとしてバンドルできるようにします。

指定したファイルサイズより大きい場合は外部ファイルとして読み込みます。

なんでも良いですが例として次の画像をバンドルしてみます。

ファイル名はqiita.pngとします。

Qiita

qiita.png

qiita.pngmodern-front-end/src/img/qiita.pngとなるように配置します。

modern-front-end

|
+-- node_modules
| `-- *
|
+-- public
| +-- bundle.js
| +-- bundle.js.map
| `-- index.html
|
+-- src
| +-- img <
| | `-- qiita.png <
| |
| +-- scss
| | `-- style.scss
| |
| `-- app.js
|
+-- package.json
`-- webpack.config.js


webpackの設定をする

webpack.config.jsを以下のように修正します。


webpack.config.js

var path = require('path');

module.exports = [{
entry: ['./src/app.js'],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public')
},
module: {
rules: [{
test: /\.(css|sass|scss)$/,
use: [
'style-loader',
'css-loader',
'sass-loader',
{
loader: 'postcss-loader',
options: {
plugins: function () {
return [
require('autoprefixer')
];
}
}
}
]
}, {
test: /\.(jpe?g|png|gif|svg|ico)(\?.+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 8192,
name: './img/[name].[ext]'
}
}
}]
}
}];


各項目の意味は次のとおりです。


test


`.jpg` `.jpeg` `.png` `.gif` `.svg` `.ico`を対象として指定しています。

`(\?.+)?`はクエリパラメータが付いていた場合でもファイルを対象にするために付けています。

use.loader


指定したファイルに対して`url-loader`を使うように設定しています。

use.options.limit


指定したバイト数以内のファイルをバンドルするように設定しています。

指定したバイト数を上回った場合はfile-loaderの機能を利用して外部ファイルとして参照します。

この場合8KB以上のファイルは除外されます。

use.options.name


`use.options.limit`を超えた場合はBase64エンコードされずにリンクを保ったまま出力用のディレクトリにコピーされます。

`use.options.name`はコピーされる場合のファイルパスを指定します。

[name]と[ext]を使うことで元のファイル名と拡張子を維持できます。


SCSSを修正する

style.scssを次のように修正します。

body {

color: darkcyan;
transition: all .1s ease;
}

p:before {
display: inline-block;
width: 1em;
height: 1em;
background-image: url("../img/qiita.png");
background-size: contain;
content: "";
}


確認する

ビルドしたらブラウザをリロードしてと表示されていることを確認します。

正しく表示されていることを確認したらデベロッパーツールのNetworkを確認します。

data:image/png;から始まる名前のリクエストが確認できます。

よく見てみるとSize(from memory cache)になり,Time0 msになっています。

ちなみに,data URIによるリクエストを除外して外部へのリクエストだけを表示するには赤枠で囲ったHide data URLsをチェックします。

次に<style>に展開されたCSSも確認してみます。

qiita.pngへの参照がurl-loaderによってBase64エンコードされていることが確認できます。

Base64エンコードされた画像はブラウザがデコードして画面上に表示します。

ですので,新しくネットワークを介したリクエストが発生しなくなり,Sizeはメモリキャッシュから読み込むためTimeは0ミリ秒になります。

もう少し細かく追ってみます。

デベロッパーツールのSourcesからqiita.pngを確認してみます。

画像はbundle.jsに含まれているため実際には存在しませんが,ソースマップを有効にしている場合はqiita.pngがどのように扱われているかを確認することができます。


file-loaderを使ってみる

file-loaderはファイルをバンドルせずに外部ファイルの参照を保つためのローダーです。


Font Awesomeで試す

Font AwesomeはAwesomeなアイコンフォントです。

file-loaderを使ってFont Awesomeを使ってみましょう。

まずは次のコマンドでFont Awesomeをインストールします。

$ npm install -save font-awesome

ソースコードから利用するパッケージなので--saveオプションにしました。

package.jsonを見るとfont-awesomeの依存関係がdependenciesになっているのがわかります。


package.json

{

"name": "modern-front-end",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack --optimize-minimize --devtool source-map --watch",
"build": "webpack --optimize-minimize --devtool source-map"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"font-awesome": "^4.7.0"
},
"devDependencies": {
"autoprefixer": "^6.7.7",
"css-loader": "^0.27.3",
"file-loader": "^0.10.1",
"node-sass": "^4.5.1",
"postcss-loader": "^1.3.3",
"sass-loader": "^6.0.3",
"style-loader": "^0.16.0",
"ts-loader": "^2.0.3",
"url-loader": "^0.5.8",
"webpack": "^2.3.2"
}
}


Font Awesomeを読み込む

webpack.config.jsを以下のように修正します。


webpack.config.js

var path = require('path');

module.exports = [{
entry: ['./src/app.js'],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public')
},
module: {
rules: [{
test: /\.(css|sass|scss)$/,
use: [
'style-loader',
'css-loader',
'sass-loader',
{
loader: 'postcss-loader',
options: {
plugins: function () {
return [
require('autoprefixer')
];
}
}
}
]
}, {
test: /\.(jpe?g|png|gif|svg|ico)(\?.+)?$/,
include: [
path.resolve(__dirname, 'src', 'img')
],
use: {
loader: 'url-loader',
options: {
limit: 8192,
name: './img/[name].[ext]'
}
}
}, {
test: /\.(eot|otf|ttf|woff2?|svg)(\?.+)?$/,
include: [
path.resolve(__dirname, 'node_modules')
],
use: {
loader: 'file-loader',
options: {
name: './fonts/[name].[ext]'
}
}
}]
}
}];


Font Awesomeのスタイルシートを参照するためにapp.jsを次のように修正します。


app.js

var css = {

fontAwesome: require('font-awesome/scss/font-awesome.scss'),
style: require('./scss/style.scss')
};

console.log('hello, world');


ここで書いているfont-awesomeは相対パスではありません。

npmによってfont-awesomeに対してパスが通っているのでパッケージ名として記述しています。

区別するために相対パスは常に./を付けておくと良いでしょう。

続けてindex.htmlでFont Awesomeを使ってみます。


index.html

<!DOCTYPE html>

<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>modern-front-end</title>
<script src="./bundle.js"></script>
</head>
<body>
<p>
<i class="fa fa-font-awesome" aria-hidden="true"></i>hello, world
</p>
</body>
</html>

ビルドするとmodern-front-end/public/fonts/にフォントファイルがコピーされます。

modern-front-end

|
+-- node_modules
| `-- *
|
+-- public
| +-- fonts <
| | +-- fontawesome-webfont.eot <
| | +-- fontawesome-webfont.svg <
| | +-- fontawesome-webfont.ttf <
| | +-- fontawesome-webfont.woff <
| | `-- fontawesome-webfont.woff2 <
| |
| +-- bundle.js
| +-- bundle.js.map
| `-- index.html
|
+-- src
| +-- scss
| | `-- style.scss
| |
| `-- app.js
|
+-- package.json
`-- webpack.config.js

画面を確認してと表示されていれば読み込めています。

続けてデベロッパーツールの方も確認してみます。

fontawesome-webfont.woff2が読み込まれているのが確認できます。


同じ拡張子のファイルが存在する場合

Font Awesomeを使う際にwebpack.config.jsで画像を指定しているtest: /\.(jpe?g|png|gif|svg|ico)(\?.+)?$/とフォントを指定しているtest: /\.(eot|otf|ttf|woff2?|svg)(\?.+)?$/.svgファイルの指定が重複しています。

指定した内容が重複すると片方の設定しか反映されず意図した動作をしません。

例えば,この場合は画像として用意した.svgファイルもmodern-front-end/public/fonts/にコピーされてしまいます。

このような場合はurl-loaderやfile-loaderを使う際にincludeという項目を追加します。

includetestで指定するファイルの場所を限定する場合に使用します。

画像を指定している側のincludemodern-front-end/src/img/内から参照し,フォントを指定しているincludemodern-front-end/node_modules内から参照しています。

こうすることでmodern-front-end/src/img/に置いた.svgmodern-front-end/public/img/にコピーされ,Font Awesomeで使用するフォントはmodern-front-end/public/fonts/にコピーされます。


webpack-dev-serverを使ってみる

webpack-dev-serverはローカルサーバーを使ってビルドと同時にブラウザの画面更新を行ってくれるパッケージです。

webpack-dev-server-を使うことでエディターとブラウザを行ったり来たりしては更新するといった手間が省けます。

さらに,Hot Module Replacementと呼ばれる機能を使って画面全体ではなくビルドした部分だけ更新することもできます。


webpack-dev-serverインストールする

次のコマンドでwebpack-dev-serverをインストールします。

$ npm install --save-dev webpack-dev-server


webpack.config.jsを書き換える

webpack.config.jsdevServerプロパティを追記してwebpack-dev-serverの設定をします。


webpack.config.js

var path = require('path');

module.exports = [{
entry: ['./src/app.js'],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public')
},
devServer: {
contentBase: path.resolve(__dirname, 'public')
},
module: {
rules: [{
test: /\.(css|sass|scss)$/,
use: [
'style-loader',
'css-loader',
'sass-loader',
{
loader: 'postcss-loader',
options: {
plugins: function () {
return [
require('autoprefixer')
];
}
}
}
]
}, {
test: /\.(jpe?g|png|gif|svg|ico)(\?.+)?$/,
include: [
path.resolve(__dirname, 'src', 'img')
],
use: {
loader: 'url-loader',
options: {
limit: 8192,
name: './img/[name].[ext]'
}
}
}, {
test: /\.(eot|otf|ttf|woff2?|svg)(\?.+)?$/,
include: [
path.resolve(__dirname, 'node_modules')
],
use: {
loader: 'file-loader',
options: {
name: './fonts/[name].[ext]'
}
}
}]
}
}];



npm-scriptsを書き換える

開発ビルドでwebpack-dev-serverを使うようにpackage.jsonを編集します。


package.json

{

"scripts": {
"start": "npm run serve",
"build": "webpack --progress --optimize-minimize",
"serve": "webpack-dev-server --hot --inline --devtool source-map"
}
}

Hot Module Replacementを無効にしたい場合はnpm-scriptsの--hotオプションを削除します。


確認する

npm startを実行してwebpack-dev-serverを立ち上げます。

ビルドできたらブラウザでhttp://localhost:8080/にアクセスします。

正しく表示されていれば,差分ビルドするたびにブラウザ上の画面も自動的に更新されます。

正しく表示されない場合はmodern-front-end/public/index.htmlが存在するか確認してください。

webpack.config.jsdevServer.contentBaseで指定した場所にHTMLが存在しないと表示することができません。


webpack-dev-serverの動き

webpack-dev-serverはビルドしたファイルはメモリ上に展開するので出力用ディレクトリにファイルは出力されません。

また,ビルド後のファイルが出力用ディレクトリに存在している必要もありません。

これらのことはindex.html以外のファイルをmodern-front-end/public/から削除しても正常に表示されることからわかります。

bundle.jsやその他アセットのURLについては,webpack.config.jsoutput.pathで指定した場所と相対的に同じ位置になります。

つまり,output.pathで指定したmodern-front-end/public/http://localhost:8080/が同じ位置になります。

bundle.jsですとmodern-front-end/public/bundle.jshttp://localhost:8080/bundle.jsになります。

詳細はドキュメントに書かれているので一度目を通してみてください。


ちょっと考察


バンドルするファイル

url-loaderでバンドルするファイルですが,Font Awesomeなどのフォントファイルはbundle.jsに含めないほうが良いです。

フォントファイルはブラウザによって読み込める種類が異なるので.eot.woffなどが用意されています。

つまり,フォントファイルは1回のセッションにつきすべてが読み込まれるわけではないので,すべて含めてしまうと読み込まれることのないファイルの分だけbundle.jsのファイルサイズが大きくなってしまいます。

こういったバンドルしないものはfile-loaderを指定してしまった方が良いです。

画像などのメディアをBase64エンコードすると元のデータに比べファイルサイズが30~40%ほど増加すると言われていますが,サーバー側でレスポンスをgzipで圧縮して転送するようになっていればこの増加分は2~3%まで減らすことができます。

容量の大きなファイルをバンドルしてしまうとバンドルファイル自身のファイルサイズが増えてしまうので,なんでもかんでもバンドルせずによく考えることを心がけましょう。

Hot Module Replacementやindex.htmlを開発用ディレクトリに含めるかどうかは予め開発方針を決めて選択すると良いでしょう。