🤪

やっぱりwebpackがわからない(エピソード1)

2022/01/28に公開
7

やっぱりwebpackがわからない(エピソード2)そもそもnpmからわからないを公開しました。

webpackがわからない

最近はViteが注目されだして、実際にとても良いビルドツールです。Vue.jsのEvan Youさんが開発しただけのことはありますね。ネーミングもイカしてます。しかし、だからといって、では開発環境にViteを採用しようと簡単にはできないのが、業務の辛い所です。新しい技術を採用して、「わしが全責任を引き受けるぜよ」というThe 男気!な人はなかなかいません。

したがって、当分はwebpackを使い続けることになるのですが、これが未だによくわからないという人が意外と多いです。フロントエンドプログラミングの初心者に近い人などは、この段階でつまずくことにより、すっかり自信をなくしてしまうこともあります。

ですが一先ず安心してください。webpackを含むこれらフロントエンドの環境設定はそもそも複雑な物なのです。どのくらい複雑かと言いますと、Frontend DevOpsという専門職ができ、金銭が発生するくらい複雑です。ただし、そもそもwebpackがよくわからないというのは、フロントエンドに携わる者にとっては問題となります。基本的に何をどうしてるのかくらいは理解できていないといけません。

そこで今回は、Viteまたはノーバンドルツールに関して書きたい欲求を抑えて、ひとまずwebpackに関して改めて説明させてもらいます。既にありきたりな内容かもしれませんが、簡単な内容からじわじわと理解していけるように解説しますので、webpackがよくわからないという方にとっては、ちょっとした手助けになるのではと思います。また、webpackを知ることにより、Viteのありがたみが理解できるでしょう。

ではまず、webpackとは結局何なのかというとことから、説明したいと思います。これは、webpackのみならず、モジュールバンドラーを使用する人にとっては基本的なことになりますので、分からない方はぜひ読んでみてください。

webpackとは

webpack(ウェブパック) とはモジュールバンドラー(module bundler) です。モジュールバンドラーとは、複数のモジュールの依存関係を解決して1つにまとめる、いわゆるバンドリング(bundling) するものであり、モジュールとはそれ単体というより、組み合わせて使う個々のプログラムのことです。

なぜモジュールをバンドリングするのか

ウェブブラウザーとサーバ間では、HTTP/1.1という通信プロトコルで通信をしています(最近はHTTP/2で通信するサーバが増えています)。このHTTP/1.1では一度に処理できるリクエストの数が限られているので、リクエストが増えるとパフォーマンス(表示速度)が落ちます。そのためモジュールはできるだけ1つにまとめたほうが、パフォーマンスを落とさずにすみます。例えば、次のような読み込みはパフォーマンスに悪く、読み込む順番にも気をつけなければいけません。

<script src="a.js"></script>
<script src="b.js"></script>
<script src="c.js"></script>

それならば最初から一つにまとめて書けばいいのでは、と思うかもしれませんが、それだと非常に行数が多く、かつ保守性の低いソースコードになってしまいます。したがって、JavaScriptに限らず、システム開発において1つのファイルで完結するということはほぼありません。

開発するときには、なるべく機能を分けたい。
実行するときは、なるべく機能をまとめたい。

これを解決してくれるのが、モジュールバンドラーです。自分で作成したモジュールだけではなく、外部モジュール(npmなどでインストールできるパッケージなど)も利用できます。

なぜwebpackは複雑と言われるのか

モジュールをバンドリングするだけなのに、なぜwebpackの記述は複雑と言われるのでしょうか。

最近のフロントエンド開発では、 CSSのプリプロセッサーやTypeScriptのトランスパイルなどを駆使して開発します。これらはGulpやGruntなどのタスクランナーと呼ばれるツールで行っていましたが、それをwebpackでも行えるようになったりました。その結果、モジュールのバンドリング以外の役割が増え、その分、記述内容も増えたことがwebpackを複雑に思わせる要因となっています。

Old JavaScript

そもそもJavaScriptにはモジュールを読み込むという概念がなかったので、 JavaScript自体で他のJSコードを読み込むことができませんでした。したがって、HTML側で複数のファイルに分けて読み込ませるのですが、これが変数の衝突などの問題を引き起こす原因となっていました。
※ ES6からは、コードをimportすることができます。

なお、ES5以前はconstletも存在せず、変数宣言にはvarしか使えなかったため、変数はいつでも上書き可能で、意図しない値が入ってしまう問題が発生します。これは、短いプログラムなら問題ないかもしれませんが、複雑なプログラム、複数人で管理するプログラムになると非常に深刻な問題となります。この変数の上書き問題は当初、IIFE(即時関数)を使うなどで解決をしてました。これは関数スコープといって、varで定義した変数は関数内部でのみ参照できるという性質を利用したものです。

Current JavaScript

ECMAScript 2015(ES6)からは、constlet命令が組み込またことにより変数にスコープができ、また変数の巻き上げ、宣言の上書きができないようになりました。そしてモジュール機能が組み込またことにより、JavaScript自体でモジュールの読み込みができるようになりました。

npm、Gulpとの違い

npmやGulp、これらとwebpackはどうちがうのか、これが、webpackをわかりにくくする要因の1つでもあります。

まず、npmはパッケージを管理するツールです。パッケージとはGulp、webpackなどのことをいい、これらパッケージは他のパッケージと複雑に絡み合っており、例えばAパッケージで利用しているZパッケージがBパッケージでも使用されていることなどがあります。npmはその依存関係をよしなに解決してくれます。つまり、npmでwebpackをインストールして管理するということです。

次にGulpですが、これとwebpackの違いが一番わかりにくいかもしれません。結論から言いますと、Gulpはタスクランナーであって、モジュールバンドラーのwebpackとは別物です。タスクランナーとはタスク(処理)をいくつか定義していき、そのタスクにしたがって処理をします。したがって、バンドルのタスクを定義すればバンドルの処理も行えます。一方、webpackとはバンドルに特化したものであって、タスクランナーのように様々な処理を行うものではありません。それではGulpで十分ではないかと思うかもしれませんが、以下の理由からwebpackが選ばれる場合があります。

  • タスクランナーは、webpackより記述が独特で複雑になり、同じタスクランナーで同じ処理を行う場合でも、それぞれ書き方が異なる。
  • バンドルに関しては、webpackは特化している分シンプルに記述でき、また処理も速い。
  • タスクランナーは多機能だが、求めるタスクは大抵webpackで事足りるので、シンプルなwebpackで十分である。

自分なりに改良して多機能に使いたいのならタスクランナーを選択するのもいいでしょう。また、バンドリングのみをwebpackでおこなったり、Gulpからwebpackを利用することも可能です。

ちなみに、「Gulpなんて古くてダサい。今はwebpackだ。そもそもガルプなどという釣りのワームみたいな名前の物など使いたくない。臭そう。」などと考えている人が結構いますが、そんなことはなく、タスクを管理するということでは、Gulpは現役で使用されています。

Viteとの違い

Viteに関しては別で書こうと思いますので、ここではwebpackとの簡単な違いだけを説明させてもらいます。

webpackはバンドルを行うバンドラーですが、Viteはノーバンドルツールと呼ばれています。ノーバンドルツールとは、そのネーミングからいかにもバンドルしなさそうな感じですが、しっかりとバンドルします。

webpackは、ビルド時に全ての依存関係を解消した後、バンドルを行います。そのため、アプリケーション起動前にアプリケーション全体を走査してバンドルする必要があり、これは規模が大きくなればなるほど結構な時間がかかります。

これに対してViteは、開発時には依存関係の解決と多少のバンドル(pre-bundle)だけを行います。全てをバンドルするのではなく、ESModulesのimportでソースコードを読み込むことにより、高速な開発サーバを実現します。ちなみに、依存関係にはGo製のesbuildを使用して事前にバンドルするのですが、これが恐ろしく早い(従来の何十倍)らしいです。

ただし、Viteが多少のバンドルしかしないというのは開発環境でのことであり、プロダクションとしてビルドする際は、今のところ通常通りにバンドルする必要があります。

つまり、Viteもバンドルを行うのでwebpackと同じくバンドラーの一種であり、その開発過程が違うという事です。

準備

では、webpackを使用する準備をして行きましょう。開発環境にnode.js、npmがインストールされていることを前提とし説明を進めます。

インストール

webpack(本体)をインストールします。またwebpack4からwebpack-cli(CLI: Command Line Interface) が本体とは別になったので、それもインストールします。

$ npm init -y
$ npm i -D webpack webpack-cli

テストファイルの準備

例としてsrc/modules/module.jsというモジュールを作成し、src/index.jsmodule.jsを読み込む処理を用意します。なお、index.jsのようにメインとなる処理を行うJSファイルをエントリーポイント(Entry Point) と呼びます。

src/modules/module.js
export default () => {
  console.log("Hello!")
}
src/index.js
import foo from './modules/module.js';

foo();

なお、このコード自体はモジュールに対応していないウェブブラウザー、例えばIE11などでは使用できません。したがって、非対応のウェブブラウザーでも対応できるように変換する必要があるのですが、webpackはそのような処理も行ってくれます。

では、webpackを使用する前に、このコード自体が正常に動作するのかを、Node.jsで実行してみましょう。

$ node src/index.js
(node:7588) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.

エラーとなり、package.jsonに"type": "module"を設定するか、拡張子を.mjsにしなさいと警告されます。試しにpackage.jsonに"type": "module"を設定して実行します。

$ node src/index.js
Hello!

実行できました。確認が終われば、package.jsonは元に戻しておいてください。

ビルド

ビルド(bild) とは、コンパイル(機械語に変換)されたソースコード群を実行できるファイルにすることです。ここではJavaScriptのコードを非対応のウェブブラウザーでも対応できるように変換し、かつバインドすることです。

webpackのビルドはwebpackコマンドで実行できます。なお、今回はwebpackをローカルインストールしていますので、npxを付けて実行します。

$ npx webpack

distディレクトリが自動で作成され、その中にmain.jsとしてバインドされます。

また、以下の警告が出る場合があります。

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value.
Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

訳 : -mode オプションを付けてdevelopmentを設定しないとproductionでフォールバックする。
※フォールバックとは、止めることはしないで機能や性能を制限して動かすこと。

つまり、 developmentモードを指定しないと、productionモードでビルドを行うという意味です。 production(本番)は Minify(圧縮・軽量化)でビルドするのに対し、development(開発)はそのままビルドします。 developmentはproductionに比べビルドの時間が短いので、開発時はdevelopmentを使用した方がいいです。

とりあえずは、作成されたmain.jsを見ると、Minifyされたソースコードになっています。

dist/main.js
(()=>{"use strict";console.log("Hello!")})();

では、警告されていましたので、コマンドに-modeオプションを付けて、developmentを指定しビルドをしてみます。

$ npx webpack --mode development

Minifyされていない開発用のmain.jsが作成されます。確認すると、何やらおどろおどろしいソースコードとなっていますが、殆どが開発のためのコードですので、今のところは気にしなくても大丈夫です。

また、毎回オプションを付けて実行するのは面倒なので、package.jsonのscriptsを設定しておきましょう。ちなみに、npmのscriptsとはコマンドののエイリアス(ショートカット)を設定する機能です。

package.json
"scripts": {
  "build": "webpack --mode development",
  "prod": "webpack --mode production"
}

これでnpm run buildとコマンドラインで実行することで、内部的にwebpack --mode developmentが呼び出されます。上記の場合、 productionモードとしてprodも設定しています。

なお、ビルド後のコードでエラーの場所を示されても、それが元のコードのどの部分なのかが分からなければデバッグがしにくくなります。そこで、特に開発時にはビルド前とビルド後の対応関係を記したソースマップというファイルが必要になります。ソースマップは先ほど設定したエイリアスにdevtoolオプションを設定することで、出力可能となります。

package.json
"scripts": {
  "build": "webpack --mode development --devtool=source-map",
  "prod": "webpack --mode production"
}

これで実行すると、同じディレクトリ内にmain.js.mapが出力されます。

はい。これでwebpackでのバインドは完了です。つまり、webpackで単にバインドをするだけなら、特別な設定などする必要はありません。やりたいことがバインドだけなら、これだけでOKです。ここから出力先を変えたい、CSSや画像を扱いたいとなると、設定が必要となってきます。ただしそれも、あたりまえですがやりたいことの設定を1つ1つしていけばいいだけです。いきなりReactやらTypeScriptやらの設定をばばーんと見せられるのでうんざりするだけで、1つ1つは本当に大したことありません。では、次からはその設定を1つ1つ行っていきましょう。

webpack.config.js

さて、無事ビルドすることができ、結果バインドもできたのですが、なぜwebpackと実行しただけでsrc/index.jsがエントリーポイントとなり実行されたのでしょうか。これは、webpackは特に設定しないとデフォルトでsrc/index.jsをエントリーポイントとするからです。そして、このようにwebpackに関する様々な設定を行うファイルが、webpack.config.jsとなります。

カレントディレクトリにwebpack.config.jsを用意することで、webpackの設定ができます。これまでのように、webpackは何も設定せずともJavaScriptをバンドリングをすることができるのですが、それ以外のことをするとなるとこのwebpack.config.jsを設定しなければいけません。そして、ここからが本番で、webpackがわからない人はこの設定をわからないと言っています。

なお、カレントディレクトリにwebpack.config.jsがあれば、webpackコマンドを実行した際にそれが自動で読み込まれるのですが、webpack --config ファイル名のように--configコマンドでファイルを指定すると、その指定したファイルを読み込みます。

それでは、例として基本的なwebpack.config.jsを作成してみます。これは、単にこのファイルをカレントディレクトリに作成すればいいだけですので、以下のようにコマンドで作成しても、右クリック > 新しいファイルなどで作成しても問題ありません。

$ touch webpack.config.js

module.exports

最も簡単なwebpack.config.jsの記述は以下となります。

module.exports = {}

JavaScriptのオブジェクトそのものですね。つまり、moduleオブジェクトのexportsプロパティにオブジェクトを設定して、その中にプロパティを設定していくわけです。webpackの設定は、どこまでいってもプロパティに値を設定しているだけとなります。

では、このmodule.exportsにプロパティを設定して行きましょう。

entry

entryプロパティは、webpackがビルドを始める際の開始点となるエントリーポイントの設定となります。指定がなければデフォルトでエントリーポイントが./src/index.jsとなります。

module.exports = {
  entry: `./src/index.js`,
}

context

contextプロパティで、エントリポイントと今後設定するローダーのベースとなるディレクトリを設定できます。設定は絶対パスで行い、デフォルト値はカレントディレクトリとなっています。つまり、contextプロパティを設定したら、entryプロパティはこのディレクトリからのパスで指定します。

module.exports = {
  context: __dirname + "/src", 
  entry: `./index.js`,

__dirnameとありますが、これはNode.js標準で用意されているグローバル変数となり、絶対パスでディレクトリ名までを取得できます。

また、気を付けたいのが、contextプロパティを設定しentryプロパティを設定する場合、./を忘れずに付けてください。

output

outputプロパティは、ビルドしたファイルをどこにどのような名前で出力すればいいのかを指定します。オブジェクトを設定し、その中のpathプロパティとfilenameプロパティで設定します。

pathプロパティはビルドしたファイルの出力先を絶対パスで指定し、指定しなければ自動的にdist/が出力先になります。

filenameプロパティは出力ファイル名を設定し、指定しなければ自動的にmain.jsとなります。

module.exports = {
  context: __dirname + "/src", 
  entry: `./index.js`,

  output: {
    //  出力ファイルのディレクト名
    path: `${__dirname}/dist`,
    // 出力ファイル名
  // 出力ディレクト内のこの設定場所に書き出される。
    filename: "./assets/js/main.js"
  }
};

出力先に前に出力した物が残った状態で出力すると、いらないファイルが残ってしまうことがよくあります。webpackのversion5.20.0以上では、cleanプロパティを設定することにより、出力ファルダ内のファイルを全て削除してから、出力します。

output: {
  clean: true,
}

ただし、手動で設置したファイルなども全て削除されます。そこで、keepプロパティで除外するファイルやディレクトリを正規表現で指定することが可能です。

output: {
  clean: {
    keep: /index.html/, // index.html をキープ(削除しない)
  },
}

source

先ほどpackage.jsonでソースマップの設定をしましたが、これはwebpack.config.jsでも設定できます。

では、まずは先ほどのpackage.jsonの設定を解除しましょう。

"scripts": {
"build": "webpack --mode development",
"prod": "webpack --mode production"
},

ソースマップを出力するしないは、devtoolプロパティで設定します。細かな設定はありますが、とりあえずは値がfalseまたは記述がなければ出力されず、値を'hidden-source-map'で設定すると出力されると覚えておいて下さい。
Devtool

module.exports = {
  devtool: "hidden-source-map",

mode

webpackコマンドのmodeオプションでdevelopment、productionの設定をしましたが、これをmodeプロパティで設定できます。

module.exports = {
  mode: "development",

これで、webpackコマンドをオプションなしで実行しても、developmentモードとなります。

パスの書き方

パスは環境によって区切りが/でないこともあり、プログラムのエラーにつながる恐れがあります。したがって、適切に連結してくれるpath.joinなどで連結することを推奨します。path.joinは指定されたすべてのセグメントを結合します。

path.join('/x', '/y', '/z'); // /x/y/z
path.join('x', 'y', 'z');    // /x/y/z

次のように使用します。

// node.jsの標準モジュールpathの読み込み
const path  = require('path');

module.exports = {
  // __dirnameは絶対パスでディレクトリ名までを取得
  context: path.join(__dirname, "src"),
  // entryはこの場合でも./が必要
  entry: `./index.js`,

この他に、連結して絶対パスを作成するpath.resolveもあります。path.resolveは引数の右から順に処理され、/が出現するまで連結を繰り返します。つまり、引数の先頭に/があれば、そこで連結が終了します。最後まで絶対パスができなければ、実行時のカレントパスを付けて返します。

path.resolve('/x', 'y', 'z');   // /x/y/z
path.resolve('/x', '/y', 'z');  // /y/z
path.resolve('/x', '/y', '/z'); // /z
path.resolve('/x/y', './z');    // /x/y/z
path.resolve('/w/x', '/y/z/');  // /y/z

複数のファイル

複数のページをそれぞれバンドリングするには、entryプロパティとoutputプロパティを次のように記述します。なお、outputプロパティにある[name]はエントリーポイント名となります。

const path  = require('path');

module.exports = {
  mode: 'development',
  context: path.join(__dirname, "src"),
  entry: {
    main: "./index.js",
    sub1: "./sub1.js",
    sub2: "./sub2.js",
  },
  output: {
    path: path.join(__dirname, "dist"),
    filename: "[name].bundle.js"
  }
};

複数のページをまとめてバンドリングするには、次のようにします。

module.exports = {
  entry: [ "./index.js", "./sub1.js", "./sub2.js" ],

  output: {
    path: `${__dirname}/dist`,
    filename: "bundle.js"
  }
};

watchモード

watchモードは、ファイルが変更されるのを監視して、自動でリビルド(rebuild: 再構築)します。watchオプションを付けてビルドすることで、watchモードとなります。

$ npx webpack --watch

もしくは、webpack.config.jsでwatchプロパティをtrueにすることでも、watchモードにすることが可能です。

module.exports = {
  watch: true,  //watch オプションを有効にする
};

control + cで、watchモードを終了できます。

なお、ファイルを更新する度にビルドされると、ファイルサイズが大きくなると時間がかかってしまうのではと心配になりますが、watchモードではキャッシュが有効になり、差分がビルドされますので、ビルドにかかる時間は短くなります。ただし、ファイルを多く監視していると、どうしてもCPUやメモリの使用量が大きくなる恐れがあります。その場合、watchOptionsプロパティignoredプロパティを設定することで、指定するディレクトリやファイルを監視対象から外すことができます。ignoredプロパティは、文字列または正規表現で指定できます。

module.exports = {
  watchOptions: {
    ignored: /node_modules/
  }
};

配列で複数選択でき、ワイルドカードも使用できます。

ignored: ['foo/**/*.js', 'node_modules/**'],

optimization

optimizationプロパティは、最適化のデフォルト設定を上書きします。つまり、設定しないとデフォルトのままです。最初の内は特に知らなくてもいいのですが、とりあえずは主な2つを紹介しておきます。詳しくはOptimizationをご確認ください。

minimize

minimizeプロパティは、圧縮をおこなうかどうかの設定です。trueにするとモードに関わらずminify(圧縮)を行い、falseにすると行いません。なお、productionモードでは、デフォルトで有効trueとなっています。

module.exports = {
  optimization: {
    minimize: false,
  }
};

minimizer

minimizeプロパティは、デフォルトの圧縮方法を設定します。圧縮にはプラグインを使用しますので、指定のプラグインをインストールする必要があります。

以下は、付属しているTerserPluginを設定する例です。
optimization.minimizer

optimization: {
  minimizer: [
    new TerserPlugin({
      parallel: true,
      terserOptions: {
        // オプションは以下のgithubを参照してください
        //https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
      },
    }),
  ],
},

エピソード2へ

長くなりましたので、続きはやっぱりwebpackがわからない(エピソード2)で公開しています。また、そもそもnpmからわからないも公開しています。

なお、ここまでは基本的なバインドの設定なので、とくに複雑な事はなかったと思います。これが、CSSや画像などのローダーやプラグインなどの設定になると、急にややこしくなってきます。その辺りをエピソード2でじわじわと説明しようと思っています。

ありきたりな内容だったかもしれませんが、お読みいただきありがとうございました。

最後の最後に、よければ業務ができる中級者になるためのJavaScript入門(文法編)DOM編を公開しておりますので、無料公開ページだけでも読んでやってください。

Discussion

torish14torish14

こんにちは!
第二段落の「これが未だによくわからないとい人が意外と多いです。」という文章がタイポしていますのでご報告させていただきます。

ANTEZANTEZ

torish14さん。ありがとうございます。修正しました。

PiyopiyoPiyopiyo

はじめまして
Old JavaScriptの第二段落に

ES5以前はconstletも存在せず、変数宣言にはverしか使えなかったため

とありますが、varの打ち間違いでしょうか。
念の為ご報告させていただきます。

ANTEZANTEZ

Piyopiyoさん。ありがとうございます。varです。修正しました。

takataka

とてもわかりやすい記事ありがとうございます。
この記事のおかげで理解が深まりました。

僭越ながら
https://zenn.dev/antez/articles/58307946cf4f3e#mode

https://zenn.dev/antez/articles/58307946cf4f3e#モード
は内容が重複しておりますでしょうか。

私の認識違いでしたらすみません。

ANTEZANTEZ

isosaさん、ご閲覧いただきありがとうございます。
また、ご指摘の通り、内容が重複しておりましたので、修正しました。
ありがとうございました!

takataka

早々のご確認ありがとうございました。
パート2も読ませていただいておりますが
非常にわかりやすくパート3も個人的にはぜひ読んでみたいです!
有益な記事ありがとうございました!