webpackについてまとめる①

webpackについてまとめる①

はじめに

本記事では webpack を利用したフロントエンド開発環境の構築手順や、なぜ webpack を使うのか?などまとめています。

使用するのは webpack5 系を使用します。

webpack とはフロントエンド開発用のモジュールバンドラーです。モジュールバンドラーとは、複数のモジュールをまとめるツールのことを言います。

扱うモジュールは、

  • CSS
  • JavaScript
  • 画像ファイル

などがあり、webpack とはこれらの複数のファイルをまとめるツールです。

webpack-image

webpack の機能

ローダー

webpack は、読み込んだモジュールを JavaScript のファイルに変換します。そのため、CSS や画像ファイルなどはそのまま扱うことが出来ません。JavaScript のファイル以外も webpack で扱うようにするためにはローダー(Loader)を利用する必要があります。

変換するファイルによって使用するローダーも異なるので、その都度インストールする必要があります。

因みに、JavaScript 用のローダーのBabel ローダーなどもあります。Babel ローダーは最新の書き方の JavaScript を古い JavaScript の書き方に変換し、古いブラウザでも読み込めるようにするローダーです。

また、ESLint ローダーなどのコードを検証するローダーもあります。

プラグイン

プラグインとは webpack を拡張させるためのプログラムのことで、下記のような機能を持ったプラグインがあります

  • clean-webpack-plugin
    • 実行時に出力ディレクトリ内のファイルを一度クリーンアップしてから新しいファイルを出力する
  • html-webpack-plugin
    • HTML テンプレートからバンドルファイルを読み込んだ HTML ファイルを出力する
  • mini-css-extract-plugin
    • js ファイルと css ファイルを分割するためのプラグイン

プラグインの実行は、ファイルがバンドルされる前後で実行されます(プラグインによって異なる)

公式ページにプラグインの一覧が掲載されています。

なぜ webpack を使うのか?

これまで webpack の機能について簡単に説明してきましたが、なぜ webpack などのモジュールバンドラーを使用する必要があるのでしょうか。

モジュールバンドラーを使うメリットとして、

  1. ファイルを分割して開発ができる
  2. 外部モジュールを簡単に利用できる
  3. 依存関係を解決したファイルを出力できる
  4. 最適化したファイルを出力できる

などが挙げられます。

ファイルを分割して開発ができる

ファイルを分割する開発のメリット

  • 可読性の向上

    • 一つのファイルに複数の機能や、数百行・数千行のコードが書かれているより、ファイルが分割されていたほうが読みやすく理解しやすい
  • 開発作業の分担やテストがしやすくなる

    • 機能ごとにファイルを分割していれば作業分担やテストがし易い
    • 同じファイルを触らなくて済むので、Git などのバージョン管理システムを利用したときにコンフリクトが発生する可能性も減る
    • 誤って本来編集する箇所と別の箇所を更新してしまった、という事故も防げる
  • 名前空間を生成出来る

    • 変数の競合やグローバル汚染を防げる
    • 異なるファイル間で変数の名前が被ったり、予期せぬ変数の上書き発生しないように出来る
  • モジュールの再利用性を高められる

    • 汎用性の高いモジュールを開発することでその他のモジュールで使い回すことが出来る
    • モジュールを再利用することで複数のファイルに同じロジックをコピペする必要が無くなる
    • 修正が発生した場合も、一つのモジュールを修正すれば良いのでモジュールの読み込み先を更新する必要がない

外部モジュールを簡単に利用できる

フロントエンド開発では、Vue や React・jQuery など外部モジュールを利用して開発するのが殆どで、webpack を利用するれば外部モジュールも簡単に利用できます。

依存関係を解決したファイルを出力できる

webpack を使用せずにモジュールを読み込んだ場合、依存関係によっては正常に動作しないことがあります。

"webpackを使用しない場合"
<body>
  <script src="js/modules/module1.js"></script>
  <script src="js/modules/module2.js"></script>
  <script src="js/modules/jquery.js"></script>
  <script src="js/modules/module3.js"></script>
  <!--
      ↑app.jsはライブラリに依存していた場合、
      読み込む順番を間違えると正常に動作しない場合がある
  -->
  <script src="js/app.js"></script>
</body>

依存関係が増えれば増えるほどコードが分かりにくくなり、ライブラリ同士が依存している場合があるので迂闊にモジュールの読み込み順を変更したり削除したりすることが出来ません。その結果、メンテナンスに多大な労力が掛かってしまいます。

また、依存関係を知っている人はコードを書いた人であり、後からチームに参画したプログラマーが依存関係を理解するのは困難であり余計な工数が掛かってしまいます。

webpack を使用した場合、自動で依存関係を解決したファイルを出力してくれるので、モジュールの読み込み順などを気にせずに済みます。

"webpackを使用した場合"
<body>
  <!-- 依存関係を解決してまとめられたファイル -->
  <script src="js/bundle.js"></script>
</body>

最適化したファイルを出力できる

webpack を利用することで、ファイルを圧縮したり不要なコードを削除したりして無駄なファイルサイズを削減させたファイルを出力できます。

ファイルサイズが少ないほど読み込み速度が早くなったり、全体の通信量を削減できるので、最適化したファイルを利用することで Web サイトパフォーマンスの向上が期待できます。

実際に webpack を使ってみる

今回は以下の環境で動作させています。webpack はバージョン 5 系を使用しています。

  • OS: Windows11
  • WSL: Ubuntu-20.04 Version 2
  • Node.js: v16.17.0
  • Yarn : v1.22.19

最終的なディレクトリ構成です。

webpack-project
│
├── dist # distディレクトリ配下のファイルはビルド時に生成されます。手動で作成する必要はありません
│     ├── index.html
│     ├── css
│     │    └── style.min.css
│     └── js
│           └── bundle.mim.js
├── src
│     ├── html
│     │    └── index.html
│     ├── images
│     │    └── cat-image-01.jpg
│     ├── js
│     │    └── index.js
│     └── scss
│           └── style.scss
├── package.json
├── postcss.config.js
├── webpack.config.common.js
├── webpack.config.dev.js
├── webpack.config.prod.js
├── .browserslistrc
├── .eslintrc.js
├── .prettierignore
├── babel.config.js
├── postcss.config.js
└── yarn.lock

まずはpackege.jsonを作成します。packege.jsonとは npm や yarn を利用したプロジェクトの設定情報が記述されたファイルのことです。

プロジェクトディレクトリを作成して作業をしていきます。

$ mkdir webpack-project

$ cd webpack-project

# ソースや出力先のディレクトリも作成します
$ mkdir src dist

# ソースファイルを配置するディレクトリも作成しておきます
$ mkdir src/html src/js src/scss src/images

# ファイルを作成します
$ touch  src/html/index.html src/js/index.js src/scss/style.scss

$ yarn init
# 全て空欄のままエンターキーを押します
yarn init v1.22.19
question name (test):
question version (1.0.0):
question description:
question entry point (index.js):
question repository url:
question author:
question license (MIT):
question private:
success Saved package.json
Done in 3.23s.

$ cat package.json
{
  "name": "test",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}

# 適宜 VSCode 等のエディタで作業します
$ code .

webpack を使用するために必要なパッケージをインストールします。JavaScript をバンドルするだけならこれだけで OK です。

$ yarn add -D webpack webpack-cli

JavaScript をバンドルする

JavaScript のソースとなるsrc/js/index.jsにコンソールを出力する記述を追加します。

"src/js/index.js"
console.log('hello! webpack!');

次に JavaScript をバンドルするようにwebpack.config.jsを作成します。後で開発用と本番用の設定ファイルに分割します。

"./webpack.config.js"
// Node.jsで提供されているパスに関するモジュールを読み込む
const path = require('path');
// distディレクトリまでのフルパスを取得します
const dist = path.resolve(__dirname, 'dist');
// srcディレクトリまでのフルパスを取得します
const src = path.resolve(__dirname, 'src');

module.exports = {
  // 開発用モードで実行
  mode: 'development',
  // ソースマップの生成方法を制御する
  devtool: 'inline-source-map',
  // エントリーポイント. webpackが最初に読み込むファイル
  entry: './src/js/index.js',
  output: {
    // 出力ディレクトリを絶対パスで指定
    path: dist,
    // 出力するファイル名を設定
    filename: './js/bundle.mim.js',
  },
  resolve: {
    // エイリアスを指定
    alias: {
      '@js': `${src}/js`,
      '@images': `${src}/images`,
      '@scss': `${src}/scss`,
    },
    // importするときのファイル名から拡張子部分を省略できるようになる
    extensions: ['.js'],
  },
}

package.jsonscriptsを追加します。

"package.json"
{
  "name": "webpack-test",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "webpack": "^5.74.0",
    "webpack-cli": "^4.10.0"
  },
  "scripts": {
    "build": "webpack"
  }
}

scriptsフィールドにコマンドを追加することで、yarn run xxxxで実行することが出来ます。

yarn run buildを実行することでwebpackが呼び出されます。実行するとdist\js\bundle.mim.jsファイルが作成されます。バンドル後のファイルを確認すると以下のようになっています。

inline-source-map

webpack の設定ファイルで指定してるdevtool: 'inline-source-map',ですが、ソースマップはバンドル後のファイルとソースファイルを関連付けるためのファイルで、バンドル前のコードを確認できるでデバッグがしやすくなります。inline-source-mapは base64 エンコードすることで、生成ファイルにインライン化しています。

何も指定しないとデフォルトでevalが指定されます。何も指定しなかった場合のバンドルファイルは以下のように出力されます。

default-eval

devtool に関する設定の一覧は公式サイトに記載されています。どこまで詳しくデバッグするかでビルドの速度が変わってくるので、プロジェクトに適したものを選択するようにしましょう。

今度はソースマップが機能しているか確認するために、わざとエラーが出るようにindex.jsを書き換えます。

"src/js/index.js"
console.log(a);

変数aはどこにも存在していないのでエラーになります。HTML のテンプレートファイルと開発用サーバの設定が完了した後に確認します。

開発用サーバを利用する

Webpack で開発用サーバを立てて、ブラウザで動作確認を行っていきます。開発用サーバと html のテンプレートを読み込むプラグインを導入していきます。

$ yarn add -D webpack-dev-server html-webpack-plugin

設定ファイルを分割する

開発用サーバのプラグインは production のビルド時には使用しないため設定ファイルを分割していきます。development 用と production 用でファイルを分けて設定ファイルを見やすく管理しやすいようにします。

webpack.config.jswebpack.config.common.jsに名前を変更し、新しくwebpack.config.dev.jswebpack.config.prod.jsを作成します。

webpack.config.common.jsでは、開発モードの指定やソースマップの記述は削除します。

"./webpack.config.common.js"
// Node.jsで提供されているパスに関するモジュールを読み込む
const path = require('path');
// distディレクトリまでのフルパスを取得します
const dist = path.resolve(__dirname, 'dist');
// srcディレクトリまでのフルパスを取得します
const src = path.resolve(__dirname, 'src');

+ // HTMLテンプレートからバンドルファイルを読み込んだHTMLファイルを出力するプラグイン
+ const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
- // 開発用モードで実行
- mode: 'development',
- // ソースマップの生成方法を制御する
- devtool: 'inline-source-map',
  // エントリーポイント. webpackが最初に読み込むファイル
  entry: './src/js/index.js',
  output: {
    // 出力ディレクトリを絶対パスで指定
    path: dist,
    // 出力するファイル名を設定
    filename: './js/bundle.mim.js',
  },
+  // 各種プラグインを読み込む
+  plugins: [
+    // HTMLのテンプレートファイルからバンドルされたモジュールを読み込んだHTMLファイルを出力する
+    new HtmlWebpackPlugin({
+      template: './src/html/index.html',
+      filename: 'index.html',
+    }),
+  ],
  resolve: {
    // エイリアスを指定
    alias: {
      '@js': `${src}/js`,
      '@images': `${src}/images`,
      '@scss': `${src}/scss`,
    },
    // importするときのファイル名から拡張子部分を省略できるようになる
    extensions: ['.js'],
  },
}

development 実行時の設定です。

ポイントとしてwatchOptions.aggregateTimeoutwatchOptions.pollを指定することで、ファイルに変更があった場合に自動更新する用に指定します。

VirtualBox、WSL2、Docker など環境下でwatch機能が動作しない場合にしていします。

"./webpack.config.dev.js"
const { merge } = require('webpack-merge');
const path = require('path');
const dist = path.resolve(__dirname, 'dist');
const commonConfig = require('./webpack.config.common.js');

module.exports = merge(commonConfig, {
  mode: 'development',
  devtool: 'inline-source-map',
  watchOptions: {
    // ファイルが変更された後に再ビルドが開始されるまでの遅延を設定
    aggregateTimeout: 200,
    // ファイルシステムの変更をポーリングする頻度を設定
    poll: 1000
  },
  devServer: {
    open: true,
    port: 9000,
    static: {
      directory: dist,
    },
  },
});

production 実行時の設定です。

"./webpack.config.prod.js"
const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.config.common.js');

// JSのコメントをビルド時に削除する
const TerserPlugin = require('terser-webpack-plugin');

module.exports = merge(commonConfig, {
  mode: 'production',
  optimization: {
    minimizer: [
      new TerserPlugin({
        extractComments: false,
        terserOptions: {
          compress: {
            drop_console: true, // console.log を出力するかどうか
          },
        },
      }),
    ],
  },
});

terser-webpack-pluginを使用するため新しくプラグインをインストールします。

このプラグインは、production モードで実行時にconsole.log()のコードを削除してくれるプラグインです。

$ yarn add -D terser-webpack-plugin

packege.jsonに記述したscriptsを修正します。

{
  "name": "webpack-test-coding",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "html-webpack-plugin": "^5.5.0",
    "terser-webpack-plugin": "^5.3.6",
    "webpack": "^5.75.0",
    "webpack-cli": "^5.0.1",
    "webpack-dev-server": "^4.11.1"
  },
  "scripts": {
-   "build": "webpack"
+   "start": "webpack-dev-server --config webpack.config.dev.js",
+   "dev": "webpack --config webpack.config.dev.js",
+   "prod": "webpack --config webpack.config.prod.js",
  }
}

--configオプションを指定することで、実行する webpack の設定ファイルを指定することができます。webpack.config.dev.jsではwebpack-mergeを使用してwebpack.config.common.jsを読み込んでいるので共通の設定も読み込まれます。

コマンドを実行すると簡易サーバが起動しブラウザが起動します。

$ yarn run start

ソースマップを確認する

起動したブラウザでディベロッパーツールを開くと、コンソールでエラーが発生しています。

source-map01

index.js:1という部分をクリックするとバンドル前のソースコードでエラーが発生している箇所が見えます。

source-map02

一旦、開発用サーバを停止して、devtool: 'inline-source-map'の箇所をdevtool: falseに書き換えて開発用サーバを起動します。

先程index.js:1だったところがバンドル後のファイル名に変わっていることが分かります。

source-map03

エラー箇所の内容を見てみても、バンドル前のファイルのどの行のコードにエラーが出ているのか分かりません。

source-map04

モジュールバンドラーを使用するときは、開発時はソースマップを指定しておいた方がデバッグがし易いことが確認できました。

意図的に発生させておいたエラーコードを削除するかコメントアウトして下さい。開発用サーバで実行時にエラーが発生していた場合、ライブリロードが実行されないので注意して下さい。

HTML テンプレートには JavaScript などを読み込む記述は不要

ソースの HTML テンプレートは以下のような記述になっています。

"src/html/index.html"
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

</body>
</html>

バンドル後の HTML ファイルを見てみましょう。

"dist/index.html"
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
+ <script defer src="./js/bundle.mim.js"></script></head>
<body>

</body>
</html>

自動的に<script defer src="./js/bundle.mim.js"></script>が記述されていることが分かります。

defer属性は JavaScript のファイルの読み込みと実行のタイミングを調整するための属性です。

通常 JavaScript のファイルは</body>の直前で読み込ませますが、defer属性を使用することで<head>タグ内でも html ファイルの読み込みと解析が終わったあとに JavaScript の実行が行われます。

おわりに

今回は、モジュールバンドラーを使用するメリットやなぜ使うのか?などを整理してみました。

次回は CSS や画像の読み込み方法を書いていこうと思います。