🐡

[学習メモ]初心から動かして学ぶwebpack5

2021/08/25に公開

はじめに

vueやreactを使っていると必ずといっていいほど使われるwebpack。
幸いなことにwebpackを意識しなくても開発はできてしまう。
しかし、webpackを知らなくても本当にいいのか?
曖昧なままにしていてはいけない、webpackを操ってこそ真のフロントエンドなのでは?
逃げずにちゃんと勉強しようと思って、webpackに入門してみる。

※翻訳には一部deepl(https://www.deepl.com/ja/translator)を利用しています。

webpackとモジュールバンドラー

まずは公式ドキュメントを読めるだけ読み進める。

最初に冒頭( https://webpack.js.org/concepts/ )の文から。

At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.

訳すと、
「webpackの中心的な役割は、モダンなJavaScriptのための静的なモジュールバンドラーである。webpackがアプリケーション内で動くと、プロジェクトが必要とするすべてのモジュールをマッピングする依存関係グラフを内部的に構築し、1つまたは複数のバンドルを生成します。」

ここで、まず「モジュールバンドラー」をもう少し深ぼっていく。

モジュールバンドラーとは?

端的に言えば依存関係を解消して複数のモジュールを一つのファイルにまとめてくれるもの。

イメージとしては以下のような形。


引用元: https://blog.mach3.jp/2016/10/01/module-bundler.html

ちなみにwebpack以外にもbrowserifyというのがある。
なお、gulpやgruntはタスクランナーというものに当たる。(ここら辺は今回は触れない)

なぜモジュールバンドラー?

細かくは色々な理由があるが、一番大きいのは 「実行時にはできる限り単一のファイルで読み込ませたい」 というもの。
やはりコードを書く上では、モジュールを分割してコードを書きたい。
より保守性の高いコードを書く上では、処理をカプセル化して、できる限りfatなファイルや処理を作りたくない。

しかし、そうしてしまうと実行側(ブラウザやサーバー)からは都合が悪い。
複数のファイルやモジュールに跨った処理を都度辿りながら実行すると遅くなってしまう。

そこでモジュールバンドラーにその責務を任せ、実行側はできる限りシンプルなファイルを読み込んで実行するようにしようというのが、モジュールバンドラーを使う大きなメリットだ。

webpackの特徴

1. js以外のhtmlやcss、assetのバンドルもしてくれる

https://webpack.js.org のトップがとてもわかりやすい。
今のフロントエンド開発では、jsに限らずhtml/cssも高度なものを扱う。
pugを使ったり、scssやsass、stylusを利用したり。
それらもまとめてバンドルしてくれるのが大きな特徴である。

2. ES Modulesを利用できる

複数のJSファイルを1つにまとめるだけなら他のツールでもできますが、webpackの場合は標準仕様のES Modulesが使えたり、node_modulesのモジュールを結合できるといったメリットがあります
引用元: https://ics.media/entry/12140/

3. とにかく高機能!ゆえにブラックボックス

以下参考
https://www.slideshare.net/ssuser46977e/webpack-why-cant-you-understand-the-webpack

実際に動かしてみるwebpack

実際に動かしてみて中身を掴んでいきたい。
公式ドキュメントのgetting startedの通りに進めていく。
https://webpack.js.org/guides/getting-started/

0.実行環境用意

色々めんどくさいので簡単なnode環境をdockerで用意した。
ローカル環境がある人は別にそちらでも問題ない。

docker-compose.yml
version: "3"

services:
  app:
    image: node:14.17.3
    tty: true
    volumes:
      - .:/webpack_tutorial
    working_dir: "/webpack_tutorial"

以下の作業はコンテナの中に入って行う。

% docker-composer run --rm app bash

1. npm環境のinit

以下を実行する。

% npm init -y

そうすると、package.jsonが生成される。
フロントエンドをいじる人にはお馴染みpackage.jsonだが、これはなにをしているのだろうか?
実際に中身を見ながら、少し紐解いていく。

package.json
{
  "name": "webpack_tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  }
  //リポジトリに関する情報等省略
}

ここで重要なのは、mainscriptsだろう。
main は外部から読み込む際のエントリーポイントとなるファイルである。
なのでこの場合は、モジュールとして外部にエクスポートするわけではないので、これ自体はそこまで重要ではない。
scripts はコマンドのエイリアスを設定できる。ビルドコマンドやlinterやテストのコマンドなどをここに設定しておくと、プロジェクト全体で利用できて便利。

なおこの段階ではまだ記述されていないが、一番重要なのは、dependenciesdevDependenciesである。これらは、プロジェクト内で利用するパッケージを記述してくれている。
devがついている方は開発やテストのみで使うものである。(eslintとか)
npm installでパッケージを追加すると、ここにパッケージ名とバージョンが追加される。

自分はrails出身なので、 Gemfileをイメージするとわかりやすい。(というか同じような役割)

npmとpackage.jsonに関しては以下の記事がとても参考になる。
https://qiita.com/righteous/items/e5448cb2e7e11ab7d477

2. webpackのインストール

webpackとwebpack-cliをインストール

% npm install webpack webpack-cli --save-dev

すると、上述した箇所にパッケージの情報が追加される。

package.json
  "devDependencies": {
    "webpack": "^5.51.1",
    "webpack-cli": "^4.8.0"
  }

3. シンプルなjsファイルをコンパイルしてみる

srcディレクトリを作成し、その中にindex.jsを作成する。
そして中に以下のようなとても簡単な関数を定義し、実行するコードを書く。

src/index.js
function helloWorld() {
  console.log("Hello World");
}

helloWorld();

ここで以下のコマンドを実行する。

% npx webpack

すると、dist/main.jsが生成される。その中身は以下の通り。

dist/main.js
console.log("Hello World");

src/index.jsをエントリーポイントとした実行結果が出力される。

なお、上述のindex.jsを以下のように関数定義だけにしてwebpackを実行すると、main.jsで空で出力される。

src/index.js
function helloWorld() {
  console.log("Hello World");
}

4. ES modulesを利用してみる

単一ファイルだとなんとなくわかりづらいので、moduleを実際にimportして使ってみる。
まずはlodashをinstall
※ちなみにlodashはなんか色々便利な関数を提供してくれるライブラリ、らしい。( https://lodash.com/

% npm install lodash

そして以下のようなlodashを使った関数を定義して、webpackを実行してみる。

src/index.js
import _ from "lodash";

function useLodash() {
  const doc = _.join(["Hello", "World"], ","); // "Hello, World"
  console.log(doc);
}

useLodash();

すると、大変なことになっているmain.jsが出現する。(真相は自分の目で確かめて…)
ただ、これだとなんとなくうまくバンドルしてくれている気がするが、実際のところがわからないので、htmlで出力させつつwebpackの動作を確かめてみる。

5. htmlへ出力させてみる

せっかくなのでブラウザへ出力させつつ、webpackの実行結果を見てみる。

まず以下のような、dist/index.htmlを作成する。
一旦、src/index.jsを読み込んでみる。

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>demo</title>
  </head>
  <body>
    <script src="src/index.js"></script>
  </body>
</html>

また、(公式ドキュメントのチュートリアルの内容だが)、 読み込む src/index.jsは以下のようにする。
「Hello, world」という文字列が入ったdivをdocument.bodyに追加するjsである。

index.js
import _ from "lodash";

function component() {
  const element = document.createElement("div");
  element.innerHTML = _.join(["Hello", "world"], ", ");
  return element;
}

document.body.appendChild(component());

この状態で、webpackを実行して、ブラウザでindex.htmlをみてみる。
すると、何も表示されない。
検証ツールを見てもjs側の1行目(lodashのimport)でエラーが発生していることがわかる。

では、index.htmlで読み込むjsを、バンドルされたmain.jsにしてみる。
すると、ちゃんと表示される!

このようにwebpackがlodashもふくめて、ブラウザが実行可能な形にバンドルしてくれていることがわかる。

6. 複数ファイルを使ってみる

モジュールバンドラーなので、1ファイルではなく、複数ファイルを用いてみたい。

src/sub.jsを以下のように作成する。
中身としては、「Hello, world」の文字列を生成するメソッドをexportしているだけ。

src/sub.js
import _ from "lodash";

export function generateHelloWorld() {
  return _.join(["Hello", "world"], ", ");
}

そしてそれを index.js で読み込む。

src/index.js
import { generateHelloWorld } from "./sub";

function component() {
  const element = document.createElement("div");
  element.innerHTML = generateHelloWorld();
  return element;
}

document.body.appendChild(component());

そしてwebpackを実行すると、前項と同じような結果が得られる。
このように、複数ファイルのバンドルが行われていることがわかる。

7. 設定ファイルを使ってみる

いままでは一切設定ファイルを書かず、デフォルトで動かしてきたが、ここで設定ファイルをいじってみる。
ルートにwebpack.config.jsを作成する。

webpack.config.js
const path = require("path");

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
  },
};

見ての通り、entryとoutputが指定されている。
こので書かれているファイルパスは今まで使ってきた、index.jsとmain.jsでこれがデフォルトの設定だ。
ここで以下のように、inputのファイルパスを変更してみる。

webpack.config.js
const path = require("path");

module.exports = {
  entry: "./src/index1.js",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
  },
};

これでwebpackを実行する。すると「inputファイルがみつからない」というエラーがでる。
また、outputの箇所を変更すると、今までとは違う場所にバンドルされたoutputファイルが出現する。
このようにして、webpackの設定をカスタムしていくことができる。

参考

https://webpack.js.org/
https://ics.media/entry/12140/
https://blog.mach3.jp/2016/10/01/module-bundler.html
https://www.slideshare.net/ssuser46977e/webpack-why-cant-you-understand-the-webpack
https://qiita.com/righteous/items/e5448cb2e7e11ab7d477

Discussion