はじめに
本編では今時のフロントエンド開発2017 (2. 構築編)に続きwebpackを使っていきます。
おしながき
- 今時のフロントエンド開発2017 (1. 愚痴編)
- 今時のフロントエンド開発2017 (2. 構築編)
- 今時のフロントエンド開発2017 (3. webpack編)
- 今時のフロントエンド開発2017 (4. TypeScript編)
- 今時のフロントエンド開発2017 (5. もっと効率よく編)
webpack
webpack.config.js
webpackでバンドルするファイルの設定をします。
プロジェクトディレクトリにwebpack.config.js
を作ります。
modern-front-end
│
+-- node_modules
| `-- *
|
+-- src
|
+-- package.json
`-- 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
に以下のように処理を記述します。
今回は標準出力をしてみます。
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を使うと今後も楽にマークアップできるのでオススメです。
<!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"
を追記します。
{
"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
を次のように書き直します。
{
"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
にローダーを登録します。
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
に以下のように記述します。
body {
color: darkcyan;
}
このままではstyle.scss
はどこからも参照されていないので,エントリポイントであるapp.js
からstyle.scss
を参照します。
var css = {
style: require('./scss/style.scss')
};
console.log('hello, world');
監視状態のままwebpack.config.js
を変更した場合は反映されないのでビルドし直してください。
ビルドされたらブラウザで確認してみます。
style.scss
に書いた内容が<style>
で展開されているのがわかります。
画面内のhello, world
の文字色が変わっていることも確認してください。
ベンダープレフィックスを付ける
postcss-loader
とautoprefixer
を使ってビルド時にベンダープレフィックスを付けるようにします。
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.scssbody { color: darkcyan; transition: all .1s ease; }
ビルドしなおしたらブラウザで確認してみます。
ベンダープレフィックスが自動で付与されていることがわかります。
2017/09/20 追記分
.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.png
をmodern-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
を以下のように修正します。
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)
になり,Time
が0 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
になっているのがわかります。
{
"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
を以下のように修正します。
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
を次のように修正します。
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を使ってみます。
<!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
という項目を追加します。
include
はtest
で指定するファイルの場所を限定する場合に使用します。
画像を指定している側のinclude
はmodern-front-end/src/img/
内から参照し,フォントを指定しているinclude
はmodern-front-end/node_modules
内から参照しています。
こうすることでmodern-front-end/src/img/
に置いた.svg
はmodern-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.js
にdevServer
プロパティを追記してwebpack-dev-serverの設定をします。
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
を編集します。
{
"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.js
のdevServer.contentBase
で指定した場所にHTMLが存在しないと表示することができません。
webpack-dev-serverの動き
webpack-dev-serverはビルドしたファイルはメモリ上に展開するので出力用ディレクトリにファイルは出力されません。
また,ビルド後のファイルが出力用ディレクトリに存在している必要もありません。
これらのことはindex.html
以外のファイルをmodern-front-end/public/
から削除しても正常に表示されることからわかります。
bundle.js
やその他アセットのURLについては,webpack.config.js
のoutput.path
で指定した場所と相対的に同じ位置になります。
つまり,output.path
で指定したmodern-front-end/public/
とhttp://localhost:8080/
が同じ位置になります。
bundle.js
ですとmodern-front-end/public/bundle.js
はhttp://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
を開発用ディレクトリに含めるかどうかは予め開発方針を決めて選択すると良いでしょう。