webpackの個人メモ
フロントエンドを開発しているときによく出てくるwebpack。
npmの記事がとても良くて、同じ著者の方の以下の2つの記事を動作確認しつつ、メモしていきたいと思います。
webpackがわからない
- 最近はViteが注目されている。
- Vue.jsのEvanさんが開発した、ビルドツール。
- webpackは現場では使われる
- フロントエンドの開発環境は複雑
- Frontend DepOpsという専門職ができている
- まじか
- Frontend DepOpsという専門職ができている
- webpackを知ることで、Viteのありがたみが理解できる
- 何できるかわからんから、ありがたみもわからんよね
webpackとは
- モジュールバンドラーのこと
- モジュールバンドラーとは
- 複数のモジュールの依存関係を解決して一つにまとめる「バンドリング」するもの
- モジュールとは
- 単体ではなくて組み合わせて使う個々のプログラムのこと
なぜモジュールをバンドリングするのか
- リクエストの回数をへらすため
- 一つのJSファイルにロジック書いたらよいのでは?
- 行数が多くなる
- 保守性が低くなる
- 再利用もしづらいよね
- 行数が多くなる
なので
- 開発するときは、なるべく機能をわけたい
- 実行するときは、なるべく機能をまとめたい
モジュールバンドラーは、これを解決してくれる。とのこと。
なるほど。そうですよね。
HTTP/2で通信のオーバーヘッドが小さくなったり、ブラウザ上でモジュールの依存関係を解消できるようになっても、依存関係解消のオーバーヘッドとかあるし、JSをまとめたものをフロントに返す、というのはなくならない気もするな。 (つまり、バンドラーは残り続ける、かもしれない)
なぜwebpackは複雑と言われるのか
Old JavaScript
- ES5以前
- モジュールを読み込むという概念ない
- 変数宣言も
var
のみなので、関数スコープ- 即時関数を多用
Current JavaScript
- ES2015(ES6)移行
const
とlet
が使える。ブロックスコープになったのかな- 変数の巻き上げ、宣言の上書きができないように
- モジュールの読み込みができるようになった(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してみたら、たしかに絶対パスが出力された
- webpack.config.jsに
- contextを指定したら、entryも指定しないとビルドエラーになるな
- entryのデフォルト値を読まれないっぽい
output
- 出力先ディレクトリの設定
- path : ビルドしたファイルの出力先、絶対パス
- __dirname + "/dist" とか
- filename:出力ファイル名、指定しないと
main.js
./assets/js/main.js
のようにパス階層を指定できる.
clean:true
source
- ソースマップの設定
- package.jsonでもできるが、webcpack.config.jsでもできる
- package.jsonの
--devtool=source-map
の指定のこと
- package.jsonの
- 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
からフォークした
github.com
- これは、プラグインじゃなくてコマンドなのね。
- npm install terser
でインストールできる
optimization: { minimizer: [ new TerserPlugin({ parallel: true, terserOptions: { //色々ある } }), ], }
上記設定して(terserOptionsは指定していない)、手元で動かしてみたが、使われているのかどうかがよくわからんかったな。
以下から、「エピソード2」へ。
ローダーとは
- webpackは基本的にはjavascriptのデータしか扱うことはできない
- その他のデータをJavaScriptのオブジェクトにして、webpackで扱えるようにするのがローダー
- リソースごとにローダーが存在する
JavaScriptのオブジェクトにする、というのがいまいちイメージわかないけど。具体的な実装を見るとピンとくるかも。
ローダーおよびバンドルの注意点
- なんでもかんでもバンドルするのは好ましくない
- バンドルは諸刃の剣、通信回数は減らせるかもだが、データ容量は増える
- CSSや画像をバンドルすると、元の容量より変換後の方が多くなりがち
- 小さい画像ならよいかも
- 見極めるのは技術者の腕の見せ所
CSS
css-loader
- cssをjavascriptにバンドルする
style-loader
$ 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()ってなんだっけ
例
url(https://example.com/images/myImg.jpg); url(data:image/png;base64,iRxVB0…); url(myFont.woff); url(#IDofSVGpath); url('../images/bullet.jpg')
Sass
- 現在スタイルシートのコーディングにはほとんどSassを利用しているらしい
sass-loader
: SassをCSSに変換するローダー- コンパイルには
sass
モジュールが必要 npm i -D sass-loader node-sass
- module.rules[0].useにsass-loaderを追加
{ 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ファイルはそこそこ大きそうだった。
バンドルする画像を分ける
- ローダーとバンドルの注意点
type
プロパティをassert/resource
にすると、画像は出力されるがバンドルはされない- どういうこと?
- 一意ぽいファイル名になって、cssのurlプロパティでもこのファイルを指定するようになっているぽい。なるほど
- どういうこと?
$ ls ./dist/ f1c205a6509fe15aa484.png index.html main-bundle.js main-bundle.js.map
- ファイルサイズに応じてバインドするか外だしにするかを切り替える
type
をasset
にする- 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ファイルをビルドしてくれる
- そのときに、javascriptとcssの読み込み設定もしてくれる
// 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とか使ってなかったらどうなるうだろう。
まとめ
長かったが、結構ためになったな。