Webpack Level 3: loaderを追加してjs以外のファイル以外を結合する

こんにちは！高野です。
Webpackレベルアップシリーズの第3弾はloaderです。これは `.js` ファイル以外も最終的に `.js` ファイルとして結合するためのパーサーのような機能を提供するもので、今やWebpackを利用する主目的と言っても過言ではありません。
loaderの設定方法を理解して、CSSファイルでも画像ファイルでもTypeScriptファイルでも、何でもバンドルできるようになってしまいましょう！

## コンテンツ
- [Webpack Level 1: 裸のWebpackのデフォルト動作を理解する](https://qiita.com/takano-h/items/3240181a83ef5e6f26a6)
- [Webpack Level 2: 設定ファイルをカスタマイズする](https://qiita.com/takano-h/items/a0c6783c79fa38c50673)
- [Webpack Level 3: loadersを追加して.js以外のファイルを結合する](https://qiita.com/takano-h/items/1a6a5a0b9d25a677f7d2)
- [Webpack Level 4: Webpack Pluginsを用いて静的サイトをS3に自動デプロイ](https://qiita.com/takano-h/items/abd3fca1514767468878) <- 今ここ


## 事前準備
まずは webpack が動作する環境を整えます。

```bash
$ npm init -y
$ npm install -g webpack webpack-dev-server webpack-cli
```

設定ファイルを下記の通り配置します

```javascript:webpack.config.js
module.exports = {
  mode: 'production',
};
```

HTMLファイルをデフォルト位置に配置します。

```html:index.html
<!DOCTYPE html>
<html>
<head>
  <title>Webpack Level Up</title>
</head>
<body>
  <h1>Webpack Level Up</h1>
  <script type="text/javascript" src="./main.js"></script>
</body>
</html>
```

## CSSファイルをバンドルする
まずはCSSファイルをバンドルしてみましょう。下記のファイルを用意します。

```css:src/main.css
h1 {
  color: red;
}
```

次にこのCSSファイルをJSファイルの中で読み込みます。

```javascript:src/index.js
import css from './main.css';

console.log(css);
```

このままwebpackを実行すると、下記のようなエラーが発生します。

```bash
$ webpack
Hash: 93310d27d1fdb082600f
Version: webpack 4.16.3
Time: 302ms
Built at: 2018-08-05 15:23:11
 1 asset
Entrypoint main = main.js
[0] ./src/index.js 53 bytes {0} [built]
[1] ./src/main.css 157 bytes {0} [built] [failed] [1 error]

ERROR in ./src/main.css 1:5
Module parse failed: Unexpected token (1:5)
You may need an appropriate loader to handle this file type.
> body {
|   color: red;
| }
 @ ./src/index.js 1:0-20
```

CSSの文法で書かれたコードを読み込むことができていないようですね。当たり前です。
そこで、WebpackがCSSをJavaScriptに変換するためのloaderを導入します。

```bash
$ npm install --save-dev css-loader
```

インストールしたloaderを設定ファイルに追加します。

```js:webpack.config.js
module.exports = {
  mode: 'production',
  module: {
    rules: [
      { test: /\.css$/, use: 'css-loader' },
    ],
  },
};
```

`module` を追加し、中に `rules` という配列で利用するloaderを指定しています。
`test` に正規表現オブジェクトを与え、ファイル名がmatchした場合に利用するloaderを `use` で指定しています。

この状態で `webpack-dev-server` を実行してみましょう。

```bash
$ webpack-dev-server --open # --open を指定することで規定のブラウザを開く
```

処理はエラーなしで完了しましたが、スタイルは適用されていませんね。
コンソールを見てみると、 `css` という変数の中身の一部にCSSファイルの中身が含まれていることは分かります。

<img width="339" alt="Webpack_Level_Up.png" src="https://qiita-image-store.s3.amazonaws.com/0/131942/7cbdc72f-faf3-cf10-bff5-4e1f253d18f2.png">


実は `css-loader` は、CSSファイルを読み込み可能な形式に変換するだけで、これをstyleタグに生成し直すにはまた別のloaderが必要なのです。次にそちらもインストールしましょう。

```bash
$ npm install --save-dev style-loader
```

上記を設定ファイルに追加します。

```js:webpack.config.js
module.exports = {
  mode: 'production',
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
    ],
  },
};
```

`use` の部分を配列にして、 `style-loader` , `css-loader` の順で指定しています。これは「 `css-loader` で読み込み可能にしたCSSファイルの内容を用いて `style-loader` でstyleタグに生成し直す」という処理を指定していることになります。
この状態で `webpack-dev-server` を再度実行してみます。

```bash
$ webpack-dev-server --open # --open を指定することで規定のブラウザを開く
```

<img width="446" alt="Webpack_Level_Up.png" src="https://qiita-image-store.s3.amazonaws.com/0/131942/806b997a-bcf5-1cec-38ef-9d6640dcd14f.png">

無事にスタイルが適用されていますね！

<img width="327" alt="Webpack_Level_Up.png" src="https://qiita-image-store.s3.amazonaws.com/0/131942/462a9c6f-c27d-ccc1-03b3-05afe3f612ea.png">

Chrome Developer ToolでElementsを見てみると、headerの中にstyleタグが挿入されていることが分かります。この部分が `style-loader` の仕事であるわけですね。

最後に、 `src/index.js` 内でCSSを `import` する箇所ですが、特に変数に代入する必要は無いので下記のように修正します。

```javascript:src/index.js
import './main.css';

console.log('Hello webpack.');
```

CSSファイルをWebpackで扱うためには、ここまでで十分です。とても簡単ですね！

## SCSSファイルをバンドルする
さて、Webpackの便利ポイントの中でも特に役割が大きいものがトランスパイラの導入です。今回はSCSSファイルをCSSファイルにトランスパイル後、それをstyleタグとしてDOM内に挿入するまでを試してみましょう。

まずは `src/main.css` を `src/main.scss` に変更します。

```scss:src/main.scss
$color: blue;

h1 {
  color: $color;
}
```

必要な loader をインストールします。

```bash
$ npm install --save-dev sass-loader node-sass
```

`node-sass` は `sass-loader` が依存しているライブラリで、インストールされていないとトランスパイル中に `Error: Cannot find module 'node-sass'` というエラーが発生します。

次に `webpack.config.js` を更新します。

```javascript:webpack.config.js
module.exports = {
  mode: 'production',
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, // 追加
    ],
  },
};
```

`.scss` で終わるファイルを読み込む場合は `sass-loader` でCSS形式で変換し、 `css-loader` でJavaScriptが読み込み可能な状態に変換し、最後に `style-loader` でstyleタグを生成してDOM内に挿入するという処理を設定しています。
実際に変換できるかどうか確認してみましょう。

```bash
$ webpack-dev-server --open
```

![Webpack_Level_Up.png](https://qiita-image-store.s3.amazonaws.com/0/131942/6bd9269d-9539-ba77-445f-f5eda466aa97.png)

無事に変数 `$color` が展開され、文字色が青になっていることが確認できます。

![Webpack_Level_Up.png](https://qiita-image-store.s3.amazonaws.com/0/131942/63147cc9-64b9-b926-35f1-eea34224a89e.png)

レンダリングされたDOMを見ても、無事に `color: blue;` が反映されていることを確認できます。

## 画像ファイルをバンドルする
ここまでで loader 追加の基本的な流れは分かりましたね。あとは同じ要領になりますが、次は画像ファイルを Base64 形式で読み込む `url-loader` を追加してみましょう。まずはライブラリをインストールします。

```bash
$ npm install --save-dev url-loader
```

設定ファイルに画像ファイルの扱いを追加します。

```javascript:webpack.config.js
module.exports = {
  mode: 'production',
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
      { test: /\.(gif|png|jpe?g|)$/, use: 'url-loader' }, // 追加
    ],
  },
};
```

`src/assets/` ディレクトリに適当な画像ファイルを追加して `src/index.js` で読み込みます。

```javascript:src/index.js
import url from './assets/matchlab.png';

const img = document.createElement('img');
img.src = url;
document.body.appendChild(img);
```

では `webpack-dev-server` を起動してみましょう。

```bash
$ webpack-dev-server --open
```

Chrome Developer ToolでElementsを見てみましょう。

<img width="506" alt="Webpack_Level_Up.png" src="https://qiita-image-store.s3.amazonaws.com/0/131942/7d2197d5-14b1-9f2d-43d8-bd45d8ef0a56.png">

画像ファイルが無事に Base64 形式にエンコードされて img タグに収まっていることが確認できます。

Base64 エンコードではなくファイルパスとして読み込む設定も可能です。その場合は `url-loader` の代わりに `file-loader` を使います。

```bash
$ npm install --save-dev file-loader
```

```javascript:webpack.config.js
module.exports = {
  mode: 'production',
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
      { test: /\.(gif|png|jpe?g|)$/, use: 'file-loader' }, // 変更
    ],
  },
};
```

```bash
$ webpack-dev-server --open
```

<img width="474" alt="Webpack_Level_Up.png" src="https://qiita-image-store.s3.amazonaws.com/0/131942/b39c3806-e910-59c0-a259-6b928266e630.png">

ファイル名が `ハッシュ値 + 拡張子` となってロードされ、そのパスから画像ファイルがサーブされていることが分かります。
ちなみに `webpack.config.js` 内で設定を書き足すことでファイル名の生成規則を操作できます。

```javascript:webpack.config.js
module.exports = {
  mode: 'production',
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
      { test: /\.(gif|png|jpe?g|)$/, use: 'file-loader?name=assets/[name]-[hash].[ext]' }, // 変更
    ],
  },
};
```

`?name=xxx` のように、クエリパラメータの形式で設定を付け足すことで操作できます。 `[name]` が元のファイル名、 `[hash]` がハッシュ値、 `[ext]` が拡張子を表します。

```bash
$ webpack-dev-server --open
```

<img width="425" alt="Webpack_Level_Up.png" src="https://qiita-image-store.s3.amazonaws.com/0/131942/2cd1f841-89ba-d688-1cc2-704b4ae42e37.png">

無事に `assets/[ファイル名]-[ハッシュ値].[拡張子]` 形式に変換されていることが分かります。

`webpack-dev-server` ではなく `webpack` を実行すると、 `dist/` 以下に `assets/image1-xxxxxxxxxx.png` のようなファイル名で画像ファイルがコピーされます。もちろんバンドル結果の `main.js` 内のパスも同じハッシュ値になっているので、デプロイ時は `dist/` 以下をそのままの構成でサーバーにアップロードすればデプロイが完了するという優れものですね。

### 本番環境サーバーの設定やデプロイフローとセットで考える
HTMLファイルやJSファイル、画像ファイルといった静的ファイルは、本番ではCDNを介して配信することが多いと思います。
その場合よく問題になるのがデプロイ時のキャッシュクリア（invalidation）です。webpackはこの問題に非常に良く対応してくれています。

例えば `file-loader` を用いてファイル名にハッシュ値を付与する場合、ビルドの度にハッシュ値が変わるので画像ファイル側のキャッシュクリアの手間が不要になります。
`url-loader` を使う場合も、画像ファイルは全てJSファイルの中にインクルードされているわけですから、キャッシュクリアを意識する必要はありません。
両者ともに画像ファイルのパスを指定したJSファイルだけキャッシュクリアすれば十分というわけです。これは便利ですね。

使い分けですが、個人的には、LP制作など1枚のHTMLファイルを作る場合は `url-loader` 、SPAなど本格的なフロントエンド開発の場合は `file-loader` を使うことが多いような印象がありますね。やはりLPの表示には速度が求められるため、極限までリクエスト回数を減らそうとするということなのでしょうか。

## TypeScriptファイルをトランスパイルしてバンドルする
さて、TypeScript等のトランスパイルが必要な言語も、webpackのloaderがあれば簡単に導入できます。まずは必要なライブラリをインストールします。

```bash
$ npm install --save-dev typescript ts-loader
```

TypeScriptには `tsconfig.json` が必要なので、とりあえず動かすために空の設定ファイルをプロジェクトルートに追加します。

```json:tsconfig.json
{}
```

`webpack.config.js` にTypeScript用の設定を追加します。

```javascript:webpack.config.js
module.exports = {
  mode: 'production',
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
      { test: /\.(gif|png|jpe?g|)$/, use: 'file-loader?name=assets/[name]-[hash].[ext]' },
      { test: /\.ts$/, use: 'ts-loader' }, // 追加
    ],
  },
};
```

バンドル対象のTypeScriptファイルを追加します。

```typescript:src/user.ts
export default class User {
  name: string;

  constructor(name) {
    this.name = name;
  }

  sayHi(): void {
    console.log('Hi, I am ' + this.name);
  }
}
```

`src/index.js` 内で上記のクラスを読み込みます。

```javascript:src/index.js
import User from './user.ts';

const user = new User('Tom');

user.sayHi();
```

`webpack-dev-server` を実行してみます。

```bash
$ webpack-dev-server --open
```

<img width="532" alt="Webpack_Level_Up.png" src="https://qiita-image-store.s3.amazonaws.com/0/131942/43da04d8-c189-f136-ea3a-da683ead1485.png">

無事にTypeScriptがトランスパイルされ、 `user.sayHi()` が実行されていることが分かります。
なお、エントリーポイントのJSファイルから部分的にTSファイルを読み込むといういびつな構成になってしまっていますが、もちろん `webpack.config.js` を修正してエントリーポイントから全てTSファイルにすることは可能です。

```javascript:webpack.config.js
module.exports = {
  mode: 'production',
  entry: './src/index.ts', // 追加
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
      { test: /\.(gif|png|jpe?g|)$/, use: 'file-loader?name=assets/[name]-[hash].[ext]' },
      { test: /\.ts$/, use: 'ts-loader' },
    ],
  },
};
```

```typescript:src/index.ts
import User from './user.ts';

const user: User = new User('Tom');

user.sayHi();
```

こんな感じですね。

## 設定ファイルの書き方のバリエーション
`webpack.config.js` の書き方は実は色々あって、同じ内容でも異なる書き方がいくつかあって混乱することがあります。loader関連でよくある別の書き方を1つ例として挙げます。

```javascript:webpack.config.js
module.exports = {
  mode: 'production',
  module: {
    rules: [
      { test: /\.css$/, use: [
        { loader: 'style-loader' },
        { loader: 'css-loader' },
      ] },
      { test: /\.scss$/, use: [
        { loader: 'style-loader' },
        { loader: 'css-loader' },
        { loader: 'sass-loader' },
      ] },
      { test: /\.(gif|png|jpe?g|)$/, use: {
        loader: 'file-loader',
        options: {
          name: 'assets/[name]-[hash].[ext]',
        },
      } },
    ],
  },
};
```

`use: 'xxx-loader'` という書き方は、実は `use: { loader: 'xxx-loader' }` の省略形だったりします。
また、 `use: 'xxx-loader?key=value'` のようなオプションの書き方も、本来的には `options: { key: 'value' } ` のような書き方があります。
この辺は説明するにも細かすぎるので、気になる方は [公式ドキュメント](https://webpack.js.org/concepts/loaders/) の参照をオススメします。

さて、loaderの設定についてはここまでにしましょう。今回利用したソースコードは [こちら](https://github.com/midwhite/webpack-level-up/tree/level-3) です。
次回はwebpackに様々な便利機能を提供してくれるpluginについてです！お楽しみに！