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

  • 30
    いいね
  • 0
    コメント

はじめに

本編では今時のフロントエンド開発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, 'dist')
    }
}];

各項目の意味

entry
バンドルのエントリポイントとなるファイルのパスを指定します。
この場合は`modern-frontend/src/app.js`がバンドルのエントリポイントになります。
output
ビルド時の出力に関するオプションを指定します。
output.filename
出力するファイル名を指定します。
output.path
出力するディレクトリを絶対パスで指定します。
この場合は`modern-frontend/dist/`にビルドしたファイルが出力されます。

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

modern-frontend/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-frontend/dist/index.htmlとなるようにHTMLを作成します。

modern-front-end
|
+-- dist <
| `-- index.html <
|
+-- node_modules
| `-- *
|
+-- 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"webpack": "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-frontend/dist/bundle.jsというファイルができあがっているのが確認できます。

modern-front-end
|
+-- dist
| +-- bundle.js <
| `-- index.html
|
+-- node_modules
| `-- *
|
+-- src
| `-- app.js
|
+-- package.json
`-- webpack.config.js

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

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

modern-frontend/dist/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/dist/bundle.js.mapというファイルができます。

modern-front-end
|
+-- dist <
| +-- bundle.js
| +-- bundle.js.map <
| `-- index.html
|
+-- node_modules
| `-- *
|
+-- src
| `-- app.js
|
+-- package.json
`-- webpack.config.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 --optimize-minimize --devtool source-map --watch",
    "build": "webpack --optimize-minimize --devtool source-map"
  }
}

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

startは特別でnpm startと記述するだけで実行できます。
通常どおりビルドする場合はnpm run buildを実行します。
監視状態にするにはnpm startを実行します。

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, 'dist')
    },
    module: {
        rules: [{
            test: /\.(css|sass|scss)$/,
            use: [
                'style-loader',
                'css-loader',
                'sass-loader'
            ]
        }]
    }
}];

SCSSを書く

CSSをバンドルするためにSassを書いていきます。
SassにはSASS記法とSCSS記法がありますが今回はSCSS記法で書いていきます。
ここらへんはお好みでどうぞ。

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

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

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

style.scss
body {
    color: darkcyan;
}

modern-frontend/src/app.jsstyle.scssを読み込みます。

app.js
var css = {
    style: require('./scss/style.scss')
};

console.log('hello, world');

npm startを実行していれば自動的にビルドされているのでブラウザで確認してみます。

style.scssに書いた内容が<style>によって<head>内で展開されているのがわかります。
画面内の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, 'dist')
    },
    module: {
        rules: [{
            test: /\.(css|sass|scss)$/,
            use: [
                'style-loader',
                'css-loader',
                'sass-loader',
                {
                    loader: 'postcss-loader',
                    options: {
                        plugins: function () {
                            return [
                                require('autoprefixer')
                            ];
                        }
                    }
                }
            ]
        }]
    }
}];

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

style.scss
body {
    color: darkcyan;
    transition: all .1s ease;
}

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

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

url-loaderを使ってみる

url-loaderはCSS中で使用するアセットをdata URIに変換してバンドルできるようにします。
指定したファイルサイズより大きい場合は外部ファイルとして読み込みます。

Font Awesomeで試す

Font AwesomeはAwesomeなアイコンフォントです。
url-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, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.(css|sass|scss)$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'sass-loader',
                    {
                        loader: 'postcss-loader',
                        options: {
                            plugins: function () {
                                return [
                                    require('autoprefixer')
                                ];
                            }
                        }
                    }
                ]
            },
            {
                test: /\.(eot|otf|ttf|woff|woff2|svg)(\?.+)?$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        limit: 8192,
                        name: './fonts/[name].[ext]'
                    }
                }
            }
        ]
    }
}];

.eot .otf .ttf .woff .woff2 .svgファイルに対してurl-loaderを使うようにしています。
後ほど書きますがurl-loaderじゃなくてfile-loaderでも良いです。

testの部分はカッコよく書こうとしたら/\.(eot|[ot]tf|woff2?|svg)(\?.+)?$/でしょうか。
見づらいので丁寧に/\.(eot|otf|ttf|woff|woff2|svg)(\?.+)?$/で良さそうです。
(\?.+)?は拡張子のあとにクエリパラメータを付ける場合を考慮して付けています。

use.options.limit
指定したバイト数以上のファイルはバンドルしないようにできます。
この場合8KB以上のファイルは除外するということになります。
use.options.name
バンドルしない場合コピー先のファイルパスを指定します。
[name]と[ext]を使うことで元のファイル名と拡張子を維持できます。

次に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/dist/fonts/にフォントファイルがコピーされます。

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

画面を確認して3.hello_awesome.pngと表示されていれば読み込めています。
続けてデベロッパーツールの方も確認してみます。

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

ちょっと考察

バンドルするファイル

url-loaderでバンドルするファイルですが,Font Awesomeなどのフォントファイルはbundle.jsに含めないほうが良いです。
フォントファイルはブラウザによって読み込める種類が異なるので.eot.woffなどが用意されています。
つまり,フォントファイルは1回のセッションにつきすべてが読み込まれるわけではないので,すべて含めてしまうと読み込まれることのないファイルの分だけbundle.jsのファイルサイズが大きくなってしまいます。

こういったバンドルしないものに限っては次のようにfile-loaderを指定してしまった方が良いです。
url-loaderはuse.options.limitを超えた場合file-loaderの機能を使っているのでuse.options.nameは同じ書き方ができます。

{
    test: /\.(eot|otf|ttf|woff|woff2|svg)(\?.+)?$/,
    use: {
        loader: 'file-loader',
        options: {
            name: './fonts/[name].[ext]'
        }
    }
}

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

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

MIMEタイプ

url-loaderにはMIMEタイプを指定できます。
サーバーレスポンスをgzipなどで圧縮する場合MIMEタイプを見て区別していることがあります。
このようにサーバーがMIMEタイプによってあらゆる動作を決めていることがあるのでMIMEタイプは付けたほうが望ましいです。

しかし,ファイルをバンドルしない場合は通常通りCSSから外部のファイルを読み込んでいるのと変わりませんので不要であることがわかります。

ファイルサイズがurl-loaderのlimitオプションに収まる場合(もしくはその可能性がある場合)はMIMEタイプを付けておきましょう。
data URIに変換されるとブラウザが元のデータの種類を区別できなくなるからです。

ベースURL問題

執筆中...