webpackの苦手意識を無くす
はじめに
これまでRailsエンジニアとして、フロントエンドのbuildツールでwebpackerを使用し続け、良くも悪くも便利すぎるという恩恵を受けてきた。
がしかし、webpackerにはさまざまなデメリットが存在する。
良くも悪くもRailsに依存してしまっている部分が厄介になりやすい。
そこでフロントエンドのbuild周りを疎結合にするべく脱webpackerの第一歩としてwebpackを基礎の基礎から入門することにした。
本記事のハンズオン部分は、専用のリポジトリを作成してGitHubにあげています。
専用のリポジトリ
webpackについて
そもそもwebpackとは?
今や、JavaScriptはファイル(モジュール)分割することが当たり前になった。
そんな分割した多数のJavaScriptファイルをbundle(一つのファイルにまとめる)してくれるいわゆるbundler(バンドラ)と言われるもの。
webpackのようなモジュールバンドラにはwebpack以外にもesbuildやParcelなどがある。
webpackを実際に使ってみる
webpackを使う際の大まかな手順としては以下の通り。
- webpackとwebpackをcliから使うためのwebpack cliをインストールする。
npm install --save-dev webpack
npm install --save-dev webpack-cli
- 実際に使用するモジュールファイルとエントリーポイントファイルを用意する。
// モジュールファイルのサンプル
export class Hello {
name;
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, ${this.name}!!!`);
}
}
// エントリポイントファイルのサンプル
import { Hello } from './hello';
const taro = new Hello('taro');
taro.greet();
- バンドルを実行する
webpackはデフォルトで、src/index.js
(エントリポイントファイル)とそこから読み込まれているモジュールファイルをバンドルして、dist/main.js
というファイルを出力する。
※あくまでデフォルトの挙動なので、設定ファイルの記述次第で挙動は変わる。
webpackでバンドルは以下のどちらかのコマンドで実行可能。
# バンドルの実行
./node_modules/.bin/webpack
# または、以下のコマンド
npx webpack
以上のコマンドでバンドルを行うと、dist/main.js
というバンドルされたファイルが生成される。
バンドルファイルについて
上記のバンドル実行手順通りにバンドリングを行うと、dist/main.js
ファイルには以下のようなjavascriptコードが生成されているはず。
※改行を加えて見やすくしてあります。
(() => {
"use strict";
new (class {
name;
constructor(e) {
this.name = e;
}
greet() {
console.log(`Hello, ${this.name}!`);
}
})("Jiro").greet();
})();
現状、バンドル後のjavascriptファイルのバージョンなどを指定していないため、class
記法を使用したES2015以降の記法が採用されている。
ただし、このままだとES2015以降の記法が使えないブラウザでこのスクリプトが実行できない。
そこで必要になってくるのが、ポリフィル(機能の実装の提供)/トランスパイラ(古いバージョンのコードに自動で変換) によるES5記法への変換。
webpackにBabelを追加する
BabelはJavaScript用のトランスパイラ兼ポリフィル。
ES6(ES2015+) を ES5 へ変換するだけではなく、JSX などの非標準の JavaScript 構文を変換することも可能。
(Reactとか使う時に知らず知らずのうちにお世話になっているかも)
Babel導入手順
Babelを実際に導入する手順としては、以下の通り。
- 現在のプロジェクトにてBabel本体と、「ES6 → ES5」の変換に必要なプリセットをインストールする。
※babelで登場するcore-jsってのがポリフィルみたいな役割だった気がする(違ったらごめん)
# Babel本体
npm install --save-dev @babel/core
# ES6 -> ES5の変換に必要なプリセット
npm install --save-dev @babel/preset-env
- ローダーを導入
webpackでは、javascriptの形式を変換したり、他にもCSS(スタイルシート)やアセット画像などをJSへバンドルしたりするときにはローダーと呼ばれるプログラムを利用します。
ここでいう、「javascriptの形式の変換」というのは「ES6 → ES5」のようなバージョンの変換の他にも、例えば「jsx
->js
」や「ts
->js
」のような変換がある。
どういう変換を行うかだったり、バンドルしたいファイル形式に応じて適切なローダーをwebpackに利用させる必要がある。
※ローダーにはいろんな種類が存在するため。
今回は、ES6からES5に変換させるのでbabel-loader
を使用する。
# babel-loaderのインストール
npm install --save-dev babel-loader
- babelの設定ファイルを記述する
babelの設定はプロジェクトの直下(場合によっては、フロントエンドのルートディレクトリの直下)に.babelrc
ファイルを作成し、それに諸々の設定を記述する。
以下では、ES6からES5に変換する際のプリセットを@babel/preset-env
を使用するように設定。
{
"presets": ["@babel/preset-env"]
}
-
webpack.config.js
ファイルを作成して、webpackにbabel用の設定を追加する。
ここまではwebpackの設定をいじることなくデフォルトの挙動でwebpackを使っていたが、ここからはbabelを使うため、webpackでbabelを使うための設定を記述する必要がある。
※webpack の設定ファイルを JavaScript で記述する場合にはCommonJS形式を使う。
プロジェクトフォルダ直下へ webpack.confg.js を作成
webpack.config.js
ファイルの詳しい書き方についてはこちら
// webpack とbabelを一緒に使えるようにするための設定を記述している
module.exports = {
target: ['web', 'es5'],
mode: "development",
module: {
// 変換やバンドルのルールは module.rules 配列に指定する
// 多くの場合、test でファイル形式を指定し、loader(または use 配列)へローダーを指定することになる
rules: [
{
// 拡張子 js のファイル(正規表現)
test: /\.js$/,
// ローダーの指定
loader: "babel-loader",
},
],
},
};
変換やバンドルのルールは module.rules 配列に指定する。
多くの場合、test
でファイル形式を指定し、loader
(またはuse
配列)へローダーを指定する。
- バンドルを実行する
npx webpack
上記のコマンドでバンドルを行うと、dist/main.js
の記法ES5の記法になっている。(らしいです。自分は何も変わっていないけど、、、、)
こんな感じのclass記法がない形のコードに変換されているのが正しいっぽい。
(() => {
"use strict";
function e(e, n) {
for (var t = 0; t < n.length; t++) {
var r = n[t];
(r.enumerable = r.enumerable || !1),
(r.configurable = !0),
"value" in r && (r.writable = !0),
Object.defineProperty(e, r.key, r);
}
}
モードとソースマップについて
現状の設定だとバンドルを実行すると下記のような警告文が出ていた。
WARNING in configuration
Set 'mode' option to 'development' or 'production'
to enable defaults for each environment.
mode オプションを 'development'(開発時)または 'production'(デプロイ時)の環境に応じて設定する必要がある。
バンドルを実行するコマンドからこのモードを設定する場合は、以下のようにオプションを設定すればOK。
npx webpack --mode development
設定ファイルwebpack.config.js
へ指定することもできる。
(自分はなぜかできていない、、、)
module.exports = {
+ mode: "development",
module: {
rules: [
【番外編】筆者のwebpack.config.jsの設定が読み込まれていなかった問題について
上記のいくつかの箇所で、「なぜか自分はできていない」という文面があったと思うが、それらの問題はそもそも筆者のwebpack.config.js
ファイル自体が読み込まれておらず、設定が反映されていなかったから。
どう対応したか?
結論、バンドルを実行するコマンドを下記のようにconfigファイルをしているようにしたところ、webpack.config.js
ファイルに記述した設定が反映されるようになった。
# うまくいっていなかったときのコマンド
npx webpack
# うまくいったコマンド(configファイルをオプションで指定)
npx webpack --config=./webpack.config.js
参考になった記事:webpack 5入門
ソースマップ
Babelでは、ES6からES5への変換を行なっているため、変換前後のコード対応表のようなものがあればデバッグに非常に役立つ。
webpack にソースマップも出力させるには devtool エントリを指定します。
※ソースマップというのは、変換前と後のコードを比較できるように紐づけられるもののこと。
mode: "development",
// または 'inline-source-map' など
devtool: "source-map",
webpack-dev-server でホットリロードしよう
ここまではVSCodeの拡張機能LiveServerを利用してきたが、実はローカルサーバもwebpackから立ち上げることができる。
そのためにまず、webpack-dev-server
をインストールします。
# npm install --save-dev と同義
npm i -D webpack-dev-server
デフォルトではdevServerはホストのルートディレクトリ/
を起点として起動するため、サーブすべきディレクトリをwebpack.config.js
内で指定してあげる必要がある。
ここではバンドルした後のindex.html
を置いているプロジェクト直下の./dist
を指定する。
// webpack.config.jsファイル
module.exports = {
target: ['web', 'es5'],
mode: "development",
devtool: "source-map",
// webpack-dev-serverのサーブするディレクトリを指定する
//(デフォルトはプロジェクトのルートディレクトリ)
devServer: {
static: {
directory: "./dist"
},
},
module: {
// 変換やバンドルのルールは module.rules 配列に指定する
// 多くの場合、test でファイル形式を指定し、loader(または use 配列)へローダーを指定することになる
rules: [
{
// 拡張子 js のファイル(正規表現)
test: /\.js$/,
// ローダーの指定
loader: "babel-loader",
},
],
},
};
※devServer の起動には serve を付け加える。
--config ./webpack.confg.jsのオプションをつけてconfigファイルを指定する
npx webpack serve --config ./webpack.confg.js
localhost:8080
をブラウザで開くとバンドル結果が表示されているはず。
serve
コマンドを用いて起動した場合には --watch
オプションと同様に自動的にファイルへの変更が反映(=ホットリロード)が適応される。
JSX (React) もバンドルしてみる
1. (React用の)Babelプリセットを追加する
Babelを用いて「JSXなどの非標準のJavaScript構文を変換する」ことが可能であるが、そのためには「JSX
-> JS
」変換用のプリセットを用意する必要がある。
Babelのreact用のプリセットをインストールする。
npm i -D @babel/preset-react
さらにBabelの設定ファイルである.babelrc
ファイルにreact用のプリセットを使用することを追加で記述する。
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
2. Reactをインストールする
ReactとReact-domをインストールする。
# npm install --save と同義
npm i -S react react-dom
3. Reactアプリを準備
手順としては、まず/dist
フォルダ内のバンドルファイルを削除し、/src
フォルダ内のファイルをシンプルなReactアプリ (JSX) で置き換えます。
- 既存のバンドルファイルとjsのソースコードファイルを削除する。
rm dist/*.js* src/*.js
-
/src
ディレクトリのソースコードやエントリファイルのコードをjsxに書き換える
import React from "react";
export const App = () => {
return (
<div className="container">
<h1>Hello.</h1>
</div>
);
};
エントリファイル
import React from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
-
/dist
フォルダ内のindex.html
へReactアプリのマウントポイントとなるJSファイルを追加する。
<body>
<!-- Reactアプリがマウントするためのポイント --->
+ <div id="root"></div>
<script src="./main.js"></script>
</body>
4. エントリーファイルと依存関係の解決
最初の方で「デフォルトで/src/index.js
(=エントリーファイル)とそこから読み込まれているモジュールをバンドルして」と記述したが、このデフォルトの挙動は使えない。
すでにデフォルトのエントリポイントとなるsrc/index.js
は存在せず、他のモジュールをインポートするエントリーファイルはsrc/index.jsx
となったため。
従前のバンドルのためのコマンドを実行すると次のようなエラーとなる。
ERROR in main
Module not found: Error: Can't resolve './src' in '/Users/**/webpack/learn-webpack'
resolve './src' in '/Users/**/learn-webpack'
上記の「srcディレクトリ内でのモジュール解決(=resolve)が出来ません」というエラーメッセージの通り、モジュール間の依存関係の解決にどの種類のファイルを参照すべきなのかを明示的にwebpackへ伝える必要が発生した。
これにはwebpack.config.jsファイルないで、resolve
エントリーを用いて、jsxファイルがエントリファイルとして認識されるように設定する。
module.exports = {
// 依存関係解決に参照するファイルの拡張子を指定
resolve: {
extensions: [".js", ".json", ".jsx"],
},
// ...
また、新たなjsの形式変換のルールを追加するため、modules.rules
を更新しなければならない。
babelでトランスパイルする対象のファイルがjsxファイルになったことをwebpackに伝える。
module: {
rules: [
{
// 拡張子 js または jsx のファイル(正規表現)
+ test: /\.jsx?$/,
- test: /\.js$/,
loader: "babel-loader",
},
],
},
エントリーファイル名や出力ファイル名をカスタマイズする
エントリファイルをカスタマイズする
上記のwebpack.cofig.jsファイルのresolve
オプションの設定によってsrc/index.jsx
をエントリーファイルとすることが出来ましたが、index以外のファイル名(たとえば main.jsx など)を使いたい場合もあるでしょう。
そういう場合はwebpack.config.jsファイルのentry
オプションへ、直接そのファイル名を明示的に指定する必要がある。
module.exports = {
resolve: {
extensions: [".js", ".json", ".jsx"],
},
+ entry: "./src/main.jsx",
出力先のファイル名をカスタマイズする
加えて、エントリーファイル同様に出力されるバンドルファイルの名前や出力先フォルダもカスタマイズすることができる。これにはwebpack.cofig.jsファイルのoutput
オプションを使って設定する。
// Node.js の path モジュールをインポート
const path = require("node:path");
module.exports = {
entry: "./src/main.jsx",
output: {
// ファイル名
filename: "bundle.js",
// 出力するフォルダ
path: path.resolve(__dirname, "dist"),
},
ここでは node:path モジュールを使って出力フォルダの絶対パスを指定している。
※注意:webpackでのバンドル後の出力先のファイルをoutputオプションで更新したら、./dist/index.html
のマウントポイントのファイルも変更しないといけない。
チャンク名でエントリファイルと出力ファイルの名前を揃える
また、entry
オプションでエントリーファイルにチャンク名を付けた場合には出力ファイルにもその名前を適用することができる。
entry: {
// チャンク名 "app"
app: "./src/main.jsx",
},
output: {
// [name] -> "app"
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
},
これは、複数のエントリーファイルがある場合などに自動的に出力名を割り振ってくれるのでめちゃくちゃ便利。
プロダクションビルドする
本番環境にデプロイするためのビルドは、モード(mode)にproduction
を指定し、serve
コマンドをbuild
へ置き換えるだけです。
コマンド
npx webpack build --mode production --config ./webpack.confg.js
このコマンドを実行するだけで、本番環境用のビルドが実行される。
NPM スクリプトを登録する
毎回ターミナルへ長いコマンドを打ち込むのも疲れてきたので、package.json
のscripts
へ NPMスクリプトを登録します。
ちなみに、NPMスクリプトは npm run スクリプト名 で実行することができます。
"scripts": {
"dev": "webpack serve --mode development --config ./webpack.confg.js",
"build": "webpack build --mode production --config ./webpack.confg.js"
},
実際にコマンドを使う際は下記のようにする。
# 開発時
npm run dev
# プロダクションビルド
npm run build
NODE_ENV で処理を分岐する
ソースマップを作成するか否かなどの処理を環境変数NODE_ENV
の値によって分岐できるようにする。
上記で設定した開発環境用と本番環境用のコマンドにNODE_ENV
という定数に値を代入する箇所を追加する。
"scripts": {
// ...
"dev": "NODE_ENV=\"development\" webpack serve --config ./webpack.confg.js",
"build": "NODE_ENV=\"production\" webpack build --config ./webpack.confg.js",
// ..
},
さらに、webpck.config.jsファイルの中身もNODE_ENVの値によって分岐できるように修正する。
const isDevEnv = process.env.NODE_ENV === 'development'
// webpack とbabelを一緒に使えるようにするための設定を記述している
module.exports = {
target: ['web', 'es5'],
mode: isDevEnv ? "development" : "production",
// ソースマップは開発環境でのみ有効にする
devtool: isDevEnv ? "source-map" : undefined,
// ...
CSS(スタイルシート)もバンドルしてみる
webpackでは、CSSファイルもJSへバンドルしてしまうことが可能。
webpackでCSSをバンドルするには、style-loader
とcss-loader
の2つのローダーが必要です。
それぞれのローダーについて、
-
style-loader
: <link /> タグへCSSを展開します。 -
css-loader
: CSS をJSへバンドルします。
ローダーは css-loader -> style-loader の順で適用する必要があります。
(依存関係的な話か???)
1. 必要なローダーをインストールする
npm i -D style-loader css-loader
2. module.rulesを追加する
CSS用のバンドルルールをmodule.rules
に追加する。
複数のローダーを適用する場合にはloader
の代わりにuse
配列を用いる。
module: {
// 変換やバンドルのルールは module.rules 配列に指定する
// 多くの場合、test でファイル形式を指定し、loader(または use 配列)へローダーを指定することになる
rules: [
{
// 拡張子 jsxのファイル(正規表現)
test: /\.jsx?$/,
// ローダーの指定
loader: "babel-loader",
},
// css関連のローダーを用意する
{
test: /\.css$/,
// css-loader -> style-loaderの順で適応される。
use: ["style-loader", "css-loader"]
},
],
},
※use
配列のローダーは配列の最後尾から順に適用されます。
よってcss-loader
-> style-loader
の順で適応となります。
3. ソースファイルの用意
適当なCSSをstyles.css
へ記述して、それをApp.jsx
の中でインポートする。
body {
margin: 0;
}
.container {
height: 100vh;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
import React from "react";
+ import "./styles.css";
export const App = () => {
// ...
4. dev serverを起動する
npm run dev
5. CSSにもソースマップをつける
デバッグ時にはバンドル前のCSSファイルへのソースマップも必要になる。
CSSにソースマップをつけるには、options
エントリをmodule.rulesのcss-loader
の部分に追加することでソースマップのオプションを設定することができる。
{
test: /\.css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
// dev モードではソースマップを付ける
sourceMap: isDev,
},
},
],
},
これで、開発環境下では、devtoolからCSSのソースマップを確認することが可能になった。
Sassもバンドルする
Sassファイルをバンドルするにはsass
とsass-loader
の追加が必要。
それぞれのローダーについて、
-
sass
: sass(scss) -> cssの変換を行います。スタイルシート版のBabelのようなもの。 -
sass-loader
: webpackにSassを扱わせます。
必要なローダーをインストールする。
npm i -D sass sass-loader
ローダーの適用順はsass-loader
-> css-loader
-> style-loader
となる。
(これまた依存関係かな???)
// ...
module: {
// 変換やバンドルのルールは module.rules 配列に指定する
// 多くの場合、test でファイル形式を指定し、loader(または use 配列)へローダーを指定することになる
rules: [
{
// 拡張子 jsxのファイル(正規表現)
test: /\.jsx?$/,
// ローダーの指定
loader: "babel-loader",
},
// 以下のcssまたはscssのバンドルに関わるルールを更新
{
// 拡張子 scss または css のファイルが対象
test: /\.s?css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
// dev モードではソースマップを付ける
sourceMap: isDevEnv,
}
},
{
loader: "sass-loader",
options: {
sourceMap: isDevEnv,
},
},
]
},
],
},
// ...
さらにCSSで記述していた箇所をSCSSの記法に変更する。
// scssの記法に変更
body {
margin: 0;
.container {
height: 100vh;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
}
最後に、jsxのソースファイルでインポートしている部分をSCSSファイルをインポートするように修正する。
- import "./styles.css";
+ import "./styles.scss";
// ...
最後に、SCSSに変更してもスタイルが崩れていなければOK!
画像などのアセットファイルをバンドルする
webpack v5.x系では別途ローダーを必要とせずにJSから読み込まれる画像やフォントなどのアセットファイルをバンドルできるようになりました。
アセットファイルのバンドルにはmodules.rules
でtype
エントリーを用います。
module: {
rules: [
{
// 画像やフォントファイル
test: /\.(ico|png|svg|ttf|otf|eot|woff?2?)$/,
type: "asset",
},
// ...
type
エントリの値にはいくつかの選択肢がありますが、通常は以下の3つから選択することになります。
-
asset/inline
: アセットをJSファイルへバンドルする。 -
asset/resource
: アセットを別ファイルとして出力する。 -
asset
: アセットをバンドルするか、別のファイルとして出力するかを自動的に選択する。
アセットをJSファイルへ含めてしまうとバンドルサイズが大きくなりすぎる(=読み込みに時間がかかる)場合のために別ファイルとして出力するオプションが用意されています。
このオプションを選択するケースには、ブラウザのリクエスト数を増やしてでもバンドルファイルの読み込みを早めるほうがメリットが大きい場合などが該当する。
アセット類の出力先を設定する
type
エントリで、asset/resource
を選択した場合のアセット類の出力先もoutput
エントリでカスタマイズすることができます。
これにはassetModuleFilename
を利用する。
// ...
entry: {
// ここでチャンクを利用しているため[name] -> 'app'になる
app: "./src/entry.jsx"
},
output: {
// ファイル名 [name] -> 'app'
filename: "[name].js",
// 出力するフォルダ
path: path.resolve(__dirname, "dist"),
// "dist/asset/名前.拡張子" として出力される
// (※ここでの[name]はチャンクではなくアセットのファイル名っぽい)
assetModuleFilename: "asset/[name][ext]",
},
// ...
※上のfilenameエントリとは異なり、ドット.
は不要であることに注意が必要です。
自分で用意したアセットを使用する
以下の手順でアセット(今回は画像リソース)を使用できるようになる。
- アセットをimportする
- バンドル出力後のアセットへの参照をimgタグのsrc属性に渡す
assetをimportした状態であればbundleで出力した画像データを参照して表示することもできる。
※ただ、bundleした後のファイルがdistのディレクトリの配下に作成されない。(imageだけでなく、jsもcssも)
→ production buildするとbuild結果のファイルたちが出力される。
import React from "react";
import "./styles.scss";
import './favicon.ico';
export const App = () => {
return (
<div className="container">
<img src="asset/favicon.ico" alt="" />
<h1>Hello....</h1>
</div>
);
};
アセットのファイルサイズによってバンドルの可否を分ける
アセットファイルの容量によって自動的にasset/inline
とasset/resource
をアセットごとに使い分けることもできる。
これにはparser.dataUrlCondition.maxSize
オプションを使用する。
rules: [
{
test: /\.(ico|png|svg|ttf|otf|eot|woff?2?)$/,
// type は自動モード
type: "asset",
parser: {
dataUrlCondition: {
// 2kb以上なら`asset/resource`を使用する
// 2kb以下なら`asset/inline`を使用する
maxSize: 1024 * 2,
},
},
},
HTML も webpack から出力する
これまではdist
ディレクトリに置いたHTMLファイルをdevServerでサーブしていましたが、実はこれもwebpackから出力することができます。
html-webpack-pluginというライブラリをインストールして、使用する。
npm i -D html-webpack-plugin
html-webpack-pluginはデフォルトでsrc/index.ejs
をテンプレートとし、それにバンドル済みのJSを<script> ~ </script>
タグとして差し込んだHTMLファイルを出力する。
これ以外のファイル(たとえば*.html
ファイル)をテンプレートにしたい場合にはtemplateオプションへ明示的にそのファイルを指定する必要があります。
// プラグインの読み込み
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// "plugins" エントリーを追加
plugins: [
// プラグインのインスタンスを作成
new HtmlWebpackPlugin({
// テンプレート
template: "./src/index.html",
// <script> ~ </script> タグの挿入位置
inject: "body",
// スクリプト読み込みのタイプ
scriptLoading: "defer",
// ファビコンも <link rel="shortcut icon" ~ /> として挿入できる
favicon: "./src/favicon.ico",
}),
],
CSSも別ファイルとして出力する
CSP (Content-Security-Policy) の設定によってはインライン・スタイルの使用が禁じられているような場合があります。
CSS を JS へバンドルすることはインライン・スタイルに該当するため、これもアセット類と同様に別ファイルとして出力したいケースがあるでしょう。
これを実現するプラグインがmini-css-extract-plugin
です。
mini-css-extract-pluginのインストール。
npm i -D mini-css-extract-plugin
webpack.config.jsでmini-css-extract-plugin
を読み込み、style-loader
に代わってこのプラグインのローダーを使用します。
// プラグインの読み込み
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
{
test: /\.s?css$/,
use: [
- "style-loader",
+ MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
sourceMap: isDev,
},
},
上のhtml-webpack-plugin
を利用しない場合、HTMLファイルへ手動で<link rel="stylesheet" ~ />
としてインポートする必要があります。
こんな感じる↓↓↓
<head>
<meta charset="UTF-8" />
<title>Webpack</title>
+ <link rel="stylesheet" href="main.css" />
</head>
逆にhtml-webpack-plugin
を利用する場合には、自動的に<link rel="stylesheet" ~ />
タグが HTML へ挿入されます。
ここまで時点のwebpack.config.jsファイルの中身(コメント・メモ付き)
※buildごとにdistディレクトリに溜まっていく不要なファイルを自動で削除するプラグインの導入周りはオリジナルで追加。
参考サイト:【webpack5】clean-webpack-pluginを使ってコンパイル毎に出力先フォルダ内を削除する
またhtml-webpack-pluginのところで以下のサイトを参照。
参考サイト:htmlを自動生成するプラグインの使い方
const path = require("node:path");
const isDevEnv = process.env.NODE_ENV === 'development';
// webpackのhtml用のプラグインの読み込み
const HtmlWebpackPlugin = require("html-webpack-plugin");
// mini-css-extract-pluginの読み込み
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// buildするごとに溜まっていく./distディレクトリの不要なファイルを削除するプラグイン
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// webpack とbabelを一緒に使えるようにするための設定を記述している
module.exports = {
// "plugins"エントリーを追加
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin(),
// プラグインのインスタンスを作成
new HtmlWebpackPlugin({
// テンプレート(バンドル後の出力先を指定)
template: "./src/index.html",
// <script> ~ </script> タグの挿入位置
inject: "body",
// スクリプト読み込みのタイプ
scriptLoading: "defer",
// ファビコンも <link rel="shortcut icon" ~ /> として挿入できる
favicon: "./src/favicon.ico",
}),
],
target: ['web', 'es5'],
mode: isDevEnv ? "development" : "production",
// ソースマップは開発環境でのみ有効にする
devtool: isDevEnv ? "source-map" : undefined,
devServer: {
static: {
directory: "./dist"
},
},
// 依存関係解決に参照するファイルの拡張子を指定
resolve: {
extensions: [".js", ".json", ".jsx"],
},
entry: {
app: "./src/entry.jsx"
},
output: {
// ファイル名 [name] -> 'app'
filename: "[name].js",
// 出力するフォルダ
path: path.resolve(__dirname, "dist"),
// "dist/asset/名前.拡張子" として出力される(※ここでの[name]はチャンクではなくアセットのファイル名っぽい)
assetModuleFilename: "assets/[name][ext]",
},
module: {
// 変換やバンドルのルールは module.rules 配列に指定する
// 多くの場合、test でファイル形式を指定し、loader(または use 配列)へローダーを指定することになる
rules: [
{
// 画像やフォントファイル
test: /\.(ico|png|svg|ttf|otf|eot|woff?2?)$/,
type: "asset/resource",
},
{
// 拡張子 jsxのファイル(正規表現)
test: /\.jsx?$/,
// ローダーの指定
loader: "babel-loader",
},
{
// 拡張子 scss または css のファイルが対象
test: /\.s?css$/,
use: [
// "style-loader",
// mini-css-extract-pluginのローダーを使用する
//(アセット類と同様にCSSも別ファイルに出力したいため)
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
// dev モードではソースマップを付ける
sourceMap: isDevEnv,
}
},
{
loader: "sass-loader",
options: {
sourceMap: isDevEnv,
},
},
]
},
],
},
};
TypeScriptをバンドルする
1. 必要なパッケージをインストールする
# TypeScript本体のインストール
npm i -D typescript
# ローダーのインストール
npm i -D ts-loader
2. tsconfig.jsonの作成
TypeScriptの挙動を規定するtsconfig.json
を作成します。
また、Reactを利用する場合には"jsx": "react"
の追記も必要です。
tsconfig.jsonファイルを生成するためのコマンド
npx tsc --init
実際に生成されるtsconfig.jsonファイルは下記
※ファイル生成時に記述されている子メメントを削除して、Reactを利用する場合に必要な"jsx": "react"
を追記
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"jsx": "react"
}
}
3. webpack.config.jsのアップデート
TSのバンドルにはbabel-loader
の代わりに、ts-loader
が必要になる。
※古いブラウザへの対応などの理由でbabel-loader
とts-loader
を併用するケースもあります。
module.exports = {
entry: {
// "tsx" へ変更
main: "./src/index.tsx",
},
resolve: {
// TS ファイルを追加
extensions: [".js", ".jsx", ".ts", ".tsx"],
},
module: {
rules: [
{
// "tsx" へ変更
test: /\.tsx?$/,
// "babel-loader" -> "ts-loader"
loader: "ts-loader",
},
],
},
};
上記は、実際に変更箇所のみを抜粋している。
実際のwebpack.config.jsファイル全体としては下記のようになっている。
const path = require("node:path");
const isDevEnv = process.env.NODE_ENV === 'development';
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin(),
new HtmlWebpackPlugin({
template: "./src/index.html",
inject: "body",
scriptLoading: "defer",
favicon: "./src/favicon.ico",
}),
],
target: ['web', 'es5'],
mode: isDevEnv ? "development" : "production",
devtool: isDevEnv ? "source-map" : undefined,
devServer: {
static: {
directory: "./dist"
},
},
resolve: {
extensions: [".js", ".json", ".jsx", ".ts", ".tsx"],
},
entry: {
main: "./src/entry.tsx"
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
assetModuleFilename: "asset/[name][ext]",
},
module: {
rules: [
{
// test: /\.(ico|png|svg|ttf|otf|eot|woff?2?)$/,
test: /\.(ico|png|svg|jpg|jpeg|gif)$/i,
type: "asset/resource",
},
{
test: /\.tsx?$/,
// ローダーの指定
// "babel-loader" -> "ts-loader"
loader: "ts-loader",
},
{
// 拡張子 scss または css のファイルが対象
test: /\.s?css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
// dev モードではソースマップを付ける
sourceMap: isDevEnv,
}
},
{
loader: "sass-loader",
options: {
sourceMap: isDevEnv,
},
},
]
},
],
},
};
webpack.configをTypeScriptで記述する
ちょっと番外編っぽいけど一応まとめておく。
webpack.config.jsの拡張子を.ts
へ変更するには以下の2つのパッケージが必要。
-
ts-node
: TypeScriptのままNode.jsを実行できるようにするモジュール。 -
@types/node
: TypeScript用Node.jsの型定義ファイル。
上記の二つのパッケージをインストールする。
npm i -D ts-node @types/node
また、tsconfig.jsonにはts-node
セクションを追加する必要がある。
最終的にはtsconfig.jsoファイルは下記のようになった。
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"lib": ["DOM", "ES2020"],
"esModuleInterop": true,
"moduleResolution": "Node",
"strict": true,
"jsx": "react",
},
"ts-node": {
"compilerOptions": {
"module": "CommonJS"
}
},
"include": [
"./src"
],
}
また、webpack.config.tsファイルは下記のようになった。
import path from 'node:path';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
// 以下の二行は保管を効かせるためのインポート
import 'webpack-dev-server';
import { Configuration } from 'webpack';
const isDevEnv: boolean = process.env.NODE_ENV === 'development';
const config: Configuration = {
// target: ['web', 'es5'],
mode: isDevEnv ? "development" : "production",
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
},
entry: {
main: "./src/entry.tsx"
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
assetModuleFilename: "asset/[name][ext]",
},
module: {
rules: [
{
test: /\.(ico|png|svg|jpg|jpeg|gif)$/i,
type: "asset/resource",
},
{
test: /\.tsx?$/,
loader: "ts-loader",
},
{
test: /\.jsx?$/,
loader: "babel-loader",
},
{
test: /\.s?css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
sourceMap: isDevEnv,
}
},
{
loader: "sass-loader",
options: {
sourceMap: isDevEnv,
},
},
]
},
],
},
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin(),
new HtmlWebpackPlugin({
template: "./src/index.html",
inject: "body",
scriptLoading: "defer",
favicon: "./src/favicon.ico",
}),
],
devtool: isDevEnv ? "source-map" : undefined,
devServer: {
static: {
directory: "./dist"
},
},
}
// 上記の設定をデフォルトエクスポートする
export default config;
以上で、今回勉強した記事の学習まとめは終了。
以下のPRに今回の諸々の実装がまとまっている。
Discussion