webpackの個人メモ

フロントエンドを開発しているときによく出てくるwebpack。

npmの記事がとても良くて、同じ著者の方の以下の2つの記事を動作確認しつつ、メモしていきたいと思います。

zenn.dev

zenn.dev

webpackがわからない

  • 最近はViteが注目されている。
    • Vue.jsのEvanさんが開発した、ビルドツール。
  • webpackは現場では使われる
  • フロントエンドの開発環境は複雑
    • Frontend DepOpsという専門職ができている
      • まじか
  • webpackを知ることで、Viteのありがたみが理解できる
    • 何できるかわからんから、ありがたみもわからんよね

webpackとは

  • モジュールバンドラーのこと
  • モジュールバンドラーとは
    • 複数のモジュールの依存関係を解決して一つにまとめる「バンドリング」するもの
  • モジュールとは
    • 単体ではなくて組み合わせて使う個々のプログラムのこと

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

  • リクエストの回数をへらすため
    • リクエストが増えると画面の表示速度が落ちる
    • HTTP/1.1では一度に処理できるリクエストの数が限られている
      • HTTP/2ではそうでもなかったような
    • scriptタグで複数のモジュールを読むと
      • 読み込む順番に気をつける必要がある
      • リクエスト回数が増える
  • 一つのJSファイルにロジック書いたらよいのでは?
    • 行数が多くなる
      • 保守性が低くなる
      • 再利用もしづらいよね

なので

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

モジュールバンドラーは、これを解決してくれる。とのこと。

なるほど。そうですよね。

HTTP/2で通信のオーバーヘッドが小さくなったり、ブラウザ上でモジュールの依存関係を解消できるようになっても、依存関係解消のオーバーヘッドとかあるし、JSをまとめたものをフロントに返す、というのはなくならない気もするな。 (つまり、バンドラーは残り続ける、かもしれない)

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

Old JavaScript

  • ES5以前
  • モジュールを読み込むという概念ない
  • 変数宣言もvarのみなので、関数スコープ
    • 即時関数を多用

Current JavaScript

  • ES2015(ES6)移行
  • constletが使える。ブロックスコープになったのかな
    • 変数の巻き上げ、宣言の上書きができないように
  • モジュールの読み込みができるようになった(import)

npmとの違い

  • npmはパッケージを管理するツール
  • webpackはモジュールバンドラー

Gulpとの違い

  • Gulpはタスクランナー
    • タスクをいくつか定義していき、そのタスクにしたがって処理をする
    • webpackより記述が独特で複雑
  • webpackはバンドルに特化している
    • バンドルに関してはwebpackがシンプルに記述できる
    • 多機能なタスクランナーがなくても、大抵webpackでなんとかなる

色々やりたいならタスクランナーのGulpを使う。webpackで事足ればwebpackって感じなのかな。

Viteとの違い

  • Viteはノーバンドルツール
    • バンドルはする
    • 開発時には依存関係の解決と多少のバンドルだけやる
      • 多分、高速なのがメリット
    • バンドラーの一種ではある

準備

  • webpackのインストール
    • webpackとwebpack-cli
  • テストファイルの準備
    • src/module/momdule.js
    • src/index.js
      • エントリーポイントはとりあえずsrc/index.js
$ npm init -y
$ npm i -D webpack webpack-cli
# webpackとwebpack-cliって何が違うんだ

# src/module/module.js
# src/index.js
# を作成
$ node src/index.js 
(node:21806) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
# moduleとして認識されない
# package.jsonにtype=moduleを追加すると
$ node src/index.js 
hello
# 正常に出力 モジュールとして認識され依存関係が解決されて自動的に参照モジュールが読まれた
# 一旦、type=moduleを削除

ビルド

  • webpackコマンドでビルドできる
    • npx webpack
$ npx webpack
$ cat dist/main.js 
(()=>{"use strict";console.log("hello")})();
# バンドルされているだけ。インライン展開ってかんじだな。
# foo()を2回よぶとどうなるんだ
$ cat dist/main.js 
(()=>{"use strict";const o=()=>{console.log("hello")};o(),o()})();
# へー、デフォはminifyされたコードになる。

# -mode developmentを追加してwebpackを実行
$  npx webpack --mode development
# めちゃクソ複雑なソースが出力される

毎回npx webpack --mode developmentと打つのは面倒なので、scriptsに追加する。 当たり前かもだが、npxはつけなくてよいね、そういえば。

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

これでnpm run buildでdevelopmentモードでビルドできる。

ソースマップ

  • ビルド後のコードはビルド前のコードと違うので、元コードのどの部分かわかるようにビルド前後の対応関係を記したソースマップを出力できるようにする
  • 同じディレクトリにmain.js.mapが出力される
    • ビルド後のファイル名がmain.jsだからかな。末尾が.mapになる
    "build": "webpack --mode development --devtool=source-map",

webpackでバインドするだけなら、これだけで完了。

まとめ

  • js側は普通にimportでモジュールを呼び出す
  • webpackコマンドを実行する
    • 開発時は--mode development --devtool=soiurce-map
    • プロダクションは--mode production
  • 毎回オプションをつけるのが面倒ならscriptsに定義
    • npm run build などで呼べるようにする

以下をやろうとおもったら追加設定が必要

  • 出力先を変えたい
  • cssや画像を扱いたい
    • sassを使いたい
  • TypeScriptをトランスパイルしたい
  • ES5のソースでトランスパイルしたい
  • Reactを使いたい

なるほどね。素のwebpackはバンドラーなので、依存関係を解決し1つのJSファイルにまとめるだけか。

webpack.config.js

  • webpackでデフォルトでsrc/index.jsをエントリーポイントとする
  • webpackの設定を行うのがwepack.config.js
    • カレントディレクトリのファイルを読む
    • オプションで指定もできるwebpack --config ファイル名
module.exports = {}

entry

  • webpackがビルドを始める開始点となるエントリーポイント
  • デフォルトは./src/index.js

context

  • エントリポイントとローダーのベースとなるディレクト
  • 絶対パス
  • デフォルトはカレントディレクト
    • コマンド実行時のディレクトリということかな、なんかそんなこともなさそう。パッケージ直下ぽい
  • __dirname はnode.jsが提供するグローバル変数絶対パスディレクトリ名まで取得可能
    • webpack.config.jsにconsole.log(__dirname)書いてbuildしてみたら、たしかに絶対パスが出力された
  • contextを指定したら、entryも指定しないとビルドエラーになるな
    • entryのデフォルト値を読まれないっぽい

output

  • 出力先ディレクトリの設定
    • path : ビルドしたファイルの出力先、絶対パス
    • __dirname + "/dist" とか
    • filename:出力ファイル名、指定しないとmain.js
      • ./assets/js/main.jsのようにパス階層を指定できる.
  • clean:true
    • 出力フォルダ内のファイルを全て削除してから出力
    • keepプロパティ
      • 削除したくないディレクトリ・ファイルを指定
      • cleanプロパティをbooleanじゃなくてオブジェクトにしてkeepを渡す

source

  • ソースマップの設定
  • package.jsonでもできるが、webcpack.config.jsでもできる
    • package.json--devtool=source-mapの指定のこと
  • webpack.config.jsにdevtool: "hidden-source-map"と記載する
    • devtool: trueを指定するとエラーになる
      • devtool:falseと指定してもエラーにはならないがソースマップは出力されない
  • devtoolには色々細かい設定があるらしい。一旦無視

mode

  • webpackコマンドのmodeオプションでしていしてたもの
  • mode: "development"とすると、webpackコマンドオプション無しでもdevelopmentモードになる

なるほど。てか、ソースマップを出力する=developmentモードってわけじゃないのか。prodocutionモードでも、ソースマップを出力する設定してたら、出力されるね。

developmentモードで出力されるごちゃごちゃしたソースって何のためのロジックなんやろうか。

パスの書き方

  • 環境によってパスの区切り文字が違う
    • path.joinなどで連結する
    • webpack.config.jsもnodejsのコードなのでrequireして使える -const path = require('path')
  • path.resolve絶対パスを返せる

複数のファイル

  • 複数のjsにバンドリングする
    • エントリーポイントを複数していできる
entry : {
   main: "./indes.js",
   sub1: "./sub1.js"
},
output : {
  filename: "[name].bundle.js"
}

複数のページをまとめてバンドリングする

  • これは、複数のエントリーポイントを1つのファイルにバンドリングする方法かな
    • 試して見たところ、左のjsから順に連結されて、即時実行の無名関数でラップされる
    • それぞれのエントリーポイントのJSは独立してないみたい。
      • "./index.js"が実行されて"./sub1.js"が実行されるイメージ。一つの即時関数内。
entry : ["./index.js", "./sub1.js"]

エントリーポイントごとに1つのファイルにしたり、全部をまとめて1つにしたりできたんだね。

watchモード

  • ファイルが変更されるのを監視して、自動でリビルドする
  • --watchオプションをつける
    • npx webpack --watch
  • ファイルサイズが大きくなってもwatchモードではキャッシュが有向になって、差分ビルドされる
  • `watchOptionsプロパティのignoredプロパティで監視対象から外すこともできる

optimization

  • 最適化のデフォルト設定を上書きする
  • 最初のうちは知らなくて良い
    • じゃあいいか

minimize

  • 圧縮を行うか
  • truenにするとモードに関わらずminifyする。
  • productionモードではデフォルトでtrueになっている

minimizer

  • デフォルトの圧縮方法を設定する
  • プラグインをインストールする必要がある
    • TerserPluginとか
  • どうやってインストールするんだ
    • npm install terser-webpack-plugin --save-dev

TerserPluginとは

  • ES6+に対応したJSの圧縮ツール(mangler/compressorとあるがmanglerってなんだ)
  • uglify-esという圧縮ツールがあるが、メンテされてないのとES6+に対応してない
  • uglify-esからフォークした

wp-works.com

github.com - これは、プラグインじゃなくてコマンドなのね。 - npm install terserでインストールできる

optimization: {
  minimizer: [
    new TerserPlugin({
      parallel: true,
      terserOptions: {
          //色々ある
      }
    }),
  ],
}

上記設定して(terserOptionsは指定していない)、手元で動かしてみたが、使われているのかどうかがよくわからんかったな。

以下から、「エピソード2」へ。

ローダーとは

  • webpackは基本的にはjavascriptのデータしか扱うことはできない
  • その他のデータをJavaScriptのオブジェクトにして、webpackで扱えるようにするのがローダー
  • リソースごとにローダーが存在する

JavaScriptのオブジェクトにする、というのがいまいちイメージわかないけど。具体的な実装を見るとピンとくるかも。

ローダーおよびバンドルの注意点

  • なんでもかんでもバンドルするのは好ましくない
  • バンドルは諸刃の剣、通信回数は減らせるかもだが、データ容量は増える
    • CSSや画像をバンドルすると、元の容量より変換後の方が多くなりがち
    • 小さい画像ならよいかも
  • 見極めるのは技術者の腕の見せ所

CSS

$ npm i -D style-loader css-loader
  • moduleプロパティにルールを設定する
    • module
      • ローダーなどのモジュールを設定する
    • module.rules
      • 各ローダーを設定するプロパティ
    • rules.test
      • 正規表現などで該当するファイルを指定する
    • rules.use
      • 使用するローダーを指定する
module: {
   rules: [
      test: /\.css$/,
      use:['style-loader', 'css-lolader']
   ]
}

※¥と\を間違ったな。\が正しい。

  • css-loaderでJS内にCSSの中身が文字列として保持される
  • style-loaderでその文字列のCSSをHTMLにappendする。
    • main-bundle.jsのscriptタグの下にsytleタグが動的に追加されてた

なるほど。cssをHTMLタグに記載しなくていいのか。これは知らなかった。

options

  • 各ローダーのソースマップを含めるにはsourceMapプロパティを設定する必要がある
    • trueで出力
    • falseまたは設定なし、で出力しない
  • ローダーごとにoptionsプロパティをもたせる
use: [
  "style-loader",
  {
    loader: "css-loader",
    options: {
       sourceMap: true
    }
  }
]
  • css-loaderでよく使うオプションとして、urlオプションがある
  • css内のurl()の有効無効を設定する
  • デフォルトはtrue

よく使うって、ことはurlをfalseにすることが多いってことかな。

options : {
  url: false
}

url()ってなんだっけ

  • CSS関数、ファイルを含めるために使用する
  • 絶対URL, 相対URL, データURIのいずれかを指定可能
  • 相対URLの場合はCSSファイルからの相対パス

url(https://example.com/images/myImg.jpg);
url(data:image/png;base64,iRxVB0…);
url(myFont.woff);
url(#IDofSVGpath);
url('../images/bullet.jpg')

developer.mozilla.org

Sass

  • 現在スタイルシートのコーディングにはほとんどSassを利用しているらしい
  • sass-loader: SassをCSSに変換するローダー
  • コンパイルにはsassモジュールが必要
  • npm i -D sass-loader node-sass
  • module.rules[0].useにsass-loaderを追加
    • 後ろから順に実行されるので後ろに追加
    • sass→css変換、cssをJS変換、CSSをHTMLにappendする処理をJSに追加
           {
               test: /\.(sass|scss|css)$/,
               use: ['style-loader', 'css-loader', 'sass-loader']
           } 

CSS内の画像をバンドル

  • url()プロパティをtrueにするだけでは画像をバンドルできない
  • base64形式に変換する必要がある
    • webpack4ではurl-loaderが必要だった
    • webpack5からはtypeプロパティにasset/inlineの指定でよくなった
  • webpack.config.jsに画像を処理する設定を入れる
            {
                test: /\.(gif|png|jpg|svg)$/,
                type: "asset/inline"
            }

ローカル環境で動作確認できた。バンドルされたJSファイルはそこそこ大きそうだった。

バンドルする画像を分ける

  • ローダーとバンドルの注意点
    • なんでもかんでもバンドルするのは好ましくない
    • base64エンコードした場合、容量は約1.33倍になる
  • typeプロパティをassert/resourceにすると、画像は出力されるがバンドルはされない
    • どういうこと?
      • 一意ぽいファイル名になって、cssのurlプロパティでもこのファイルを指定するようになっているぽい。なるほど
$ ls ./dist/
f1c205a6509fe15aa484.png  index.html  main-bundle.js  main-bundle.js.map
  • ファイルサイズに応じてバインドするか外だしにするかを切り替える
    • typeassetにする
    • module.rules[1].parser.dataUrlCondition.maxSizeを最大ファイル値にする。(100*1024=100kb)

プラグイン(Plugins)

  • ローダーは、リソースをwebpackで扱えるようにしたもの。
  • プラグインはローダーでは実現できない機能を提供するもの

mini-css-extract

  • バンドルしたJSからスタイルシートの箇所をcssファイルとして出力する
  • linkタグでcssファイルを読み込める
  • npm i -D mini-css-extract-plugin
  • webpack.config.jsの先頭でMiniCssExtractPluginをrequireする
  • loaderにMiniCssExtractPlugin.loaderを指定する
  • plugins[0]にMiniCssExtractPluginインスタンスを渡す
    • オプションに{ filename : './css/[name].css'}を指定する
  • JSから呼ばれているCSSを外だしできる
    • index.htmlにはlinkタグでcssを指定しないといけない

これ、どっちが主流なんだろうか。cssファイルは分けた方が良い気もするが。

html-webpack-plugin

  • dist以下にhtmlファイルを置くのではなくて、htmlファイルもビルドするプラグイン
  • 対象とするhtmlファイルをビルドしてくれる
    • そのときに、javascriptcssの読み込み設定もしてくれる
    // html-webpack-pluginの設定
    new HtmlWebpackPlugin({
      // 対象のテンプレートを設定
      template: `${__dirname}/src/index.html`,
      // 書き出し先
      filename: `${__dirname}/dist/index.html`,
      // ビルドしたjsファイルを読み込む場所。デフォルトはhead
      inject: 'body'
    }),
  • templateを指定しなかったら、自動でファイルが作成されるらしい、どんなファイル?
    • bodyが空のファイルだ。(bodyにjsの読み込み指定してれば、jsのscriptタグのみ)

copy-webpack-plugin

  • 指定したファイルをそのままコピーして出力
    • src -> distに出力するのに役立つ
  new CopyWebpackPlugin({
    patterns: [
      {
        from: `${__dirname}/src/img/`,
        to: `${__dirname}/dist/img/`,
      }
    ]
  }),

module.rules[n].typeがasset/resourceのときは、cssから参照される画像はdist直下にコピーされてたが、どういう使い分けなんじゃろか。 htmlのimgタグにsrc属性で画像ファイルのパスを記載していても、dist以下にコピーされない気もするが。

試してみたが、やっぱりされないか。オプションがあるのかな。

imagemin-webpack-plugin

  • ファイルを圧縮する
  • npm i -D imagemin-webpack-plugin
    • 各ファイル形式に対応したパッケージもインストールする
    • npm i -D imagemin-pngquant まずはpngだけためす
const ImageminWebpackPlugin = require('imagemin-webpack-plugin').default
// defaultでimportするのが最初わからずエラーになった

        new ImageminWebpackPlugin({
            test: /\.(png)$/i,
            pngquant: {
                quality: '10-20'
            }
        }),

おー、たしかに、dist以下の画像が圧縮されてた。不思議。CopyWebpackPluginとか使ってなかったらどうなるうだろう。

まとめ

長かったが、結構ためになったな。

  • webpackは基本的にはバンドルするだけ
    • 複数のjsファイルを一つにまとめる
  • jsやcssを最適化(minifyとか)するにはminimizerを指定する
    • optimization.minimizer[]に指定する
      • TerserPlugin() // jsのminify
  • JS以外を扱う場合はローダーを設定する
    • jsにcssを埋め込む(css-loader)
    • jsに埋め込まれたcssを動的にhtmlから参照できるようにする(style-loader)
    • sassをcssに変換する(sass-loader)
    • css内の画像をバンドルする(url-loaderやtype: asset)
  • ローダーでできない変換処理はプラグインを使う
    • cssを別ファイルに出力する(mini-css-extract)
    • htmlをsrcからdistに出力する(html-webpack-plugin)
    • srcのファイルをdistにコピーする(copy-webpack-plugin)
    • 画像を圧縮する(imagemin-webpack-plugin)