【更新日 : 】
webpackでEJS・Sass・JSのコーディング環境を作る
- Category:
- 開発環境
モダン開発ではないWebサイトコーディング用のwebpack環境を作った備忘録です。
先に構築内容をまとめて記載しています。
後半は内容の補足などです。
この記事のwebpackでできること
- 複数のJSファイルを生成
- EJSで複数のHTMLファイルを生成
- ビルド後のHTMLファイルを整形(任意追加)
- Sassをビルドして複数のCSSファイルを生成
- CSSファイルで読み込んでいる画像のうち、指定サイズ以下のものはbase64に変換して埋め込む
- PostCSSによるオプション設定
- autoprefixer:ベンダープレフィックスの追加
- postcss-sort-media-queries:メディアクエリをソートして1つにまとめる
- css-declaration-sorter:プロパティ順のソート(smacss)
- postcss-purgecss:CSSファイルから未使用のスタイルを削除する
- postcss-normalize-charset:先頭にcharset追加
初期設定(ファイルのインストール)
構築したいディレクトリへ移動
作業を始める前に設定したいプロジェクトのあるディレクトリまで移動してください。
Windowsでの移動
cd C:¥Users¥.....¥webpackを構築したいプロジェクトディレクトリ
Macでの移動
cd /Users/....../webpackを構築したいプロジェクトディレクトリ
macなら該当フォルダを右クリック→「フォルダに新規ターミナルタブ」を選択してターミナルを立ち上げるとスムーズです。
package.jsonを作成
以下のコマンドを実行してpackage.jsonを作成します。
npm init -y
webpackのベースファイル一式をインストール
npm install -D webpack webpack-cli webpack-dev-server
babelプラグイン一式をインストール
npm install -D @babel/core @babel/preset-env babel-loader
ファイル分割プラグインをインストール
npm install -D webpack-watched-glob-entries-plugin
ファイルコピープラグインをインストール
npm install -D copy-webpack-plugin
EJSプラグイン一式をインストール
npm install -D html-webpack-plugin html-loader template-ejs-loader
Sassプラグイン一式をインストール
npm install -D css-loader sass-loader sass mini-css-extract-plugin
PostCSSプラグインをインストール
npm install -D postcss postcss-loader autoprefixer postcss-sort-media-queries css-declaration-sorter @fullhuman/postcss-purgecss postcss-normalize-charset
環境に合わせて不要なものは適宜省いてください。
プラグインの詳細は後述するPostCssのプラグインについてをご参照ください。
browserslistの設定(autoprefixerの設定)
autoprefixerを追加した場合は、
package.jsonに対象となるブラウザ範囲"browserslist"
を追記してください。
package.json
{
〜省略〜
"devDependencies": {
〜省略〜
},
"browserslist": [
"last 3 versions",
"> 5%",
"Firefox ESR",
"not dead"
]
}
browserslistは環境に合わせて適宜変更してください。
設定の詳細は以下をご参照ください。
Sassファイルの読み込み
src/js/ に内包するメインのjsファイル(任意の名称)にSassファイルをimportする記述を追加します。
src/js/[任意の名称].js
import "../sass/style.scss";
webpackの設定用JSファイルを作成
開発ディレクトリに3つのJSファイル(webpack.common.js、webpack.dev.js、webpack.prod.js)を作成します。
webpack.common.js (ベース設定ファイル)
webpack.common.js
/*
webpack.dev.js、webpack.prod.jsへビルド設定を渡す
引数[MODE]は webpack の出力オプション('production' か 'development')を指定します
*/
const webpackConfig = (MODE) => {
//purgecssの除外設定
const cssSafeList = ['hoge'];
//ベースファイルのパス設定
const filePath = {
js: './src/js/',
ejs: './src/ejs/',
sass: './src/sass/',
};
// ソースマップの利用有無判別(productionのときはソースマップを利用しない)
const enabledSourceMap = MODE === "development";
//ベース設定読み込み
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const WebpackWatchedGlobEntries = require("webpack-watched-glob-entries-plugin");
//sassファイル分割
const entriesScss = WebpackWatchedGlobEntries.getEntries([path.resolve(__dirname, `${filePath.sass}**/**.scss`)], {
ignore: path.resolve(__dirname, `${filePath.sass}**/_*.scss`),
})();
const cssGlobPlugins = (entriesScss) => {
return Object.keys(entriesScss).map(key => new MiniCssExtractPlugin({
//出力ファイル名
filename: `assets/css/${key}.css`
})
);
};
//jsファイル分割
const entriesJS = WebpackWatchedGlobEntries.getEntries([path.resolve(__dirname, `${filePath.js}*.js`)])();
//ejsビルド
const entries = WebpackWatchedGlobEntries.getEntries([path.resolve(__dirname, `${filePath.ejs}**/*.ejs`)], {
ignore: path.resolve(__dirname, `${filePath.ejs}**/_*.ejs`),
})();
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { htmlWebpackPluginTemplateCustomizer } = require("template-ejs-loader");
const htmlGlobPlugins = (entries) => {
return Object.keys(entries).map(key => new HtmlWebpackPlugin({
//出力ファイル名
filename: `${key}.html`,
//ejsファイルの読み込み
template: htmlWebpackPluginTemplateCustomizer({
htmlLoaderOption: {
//ファイル自動読み込みと圧縮を無効化
sources: false,
minimize: false
},
templatePath: `${filePath.ejs}${key}.ejs`,
}),
//JS・CSS自動出力と圧縮を無効化
inject: false,
minify: false,
})
);
};
//ビルド設定を各jsファイルへ渡す
return {
// モード値を production に設定すると最適化された状態で、
// development に設定するとソースマップ有効でJSファイルが出力される
mode: MODE,
// ローカル開発用環境を立ち上げる
// 実行時にブラウザが自動的に localhost を開く
devServer: {
hot: true, //ホットリロードを有効化(変更された部分のみを更新)
static: path.resolve(__dirname, 'src'),
open: true
},
target: 'web', //ローカルサーバのリロードを有効化する
//JS書き出しファイル読み込み
entry: entriesJS,
//JS書き出し設定
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'assets/js/[name].js',
clean: true, //ビルド時にdistフォルダをクリーンアップする
},
module: {
rules: [
//ejs
{
test: /\.ejs$/i,
use: ['html-loader','template-ejs-loader']
},
//babel-loader
{
// 拡張子 .js の場合
test: /\.js$/,
exclude: /node_modules/,
use: [
{
// Babel を利用する
loader: "babel-loader",
// Babel のオプションを指定する
options: {
presets: [
['@babel/preset-env', { targets: "defaults" }]
],
},
},
],
},
//Sass
{
test: /\.scss/, // 対象となるファイルの拡張子
use: [
// CSSファイルを書き出すオプションを有効にする
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: "css-loader",
options: {
// オプションでCSS内のurl()メソッドの取り込みを禁止する
url: true,
// ソースマップの利用有無
sourceMap: enabledSourceMap,
// Sass+PostCSSの場合は2を指定
// 0 => no loaders (default);
// 1 => postcss-loader;
// 2 => postcss-loader, sass-loader
importLoaders: 2
}
},
// PostCSSのための設定
{
loader: "postcss-loader",
options: {
// PostCSS側でもソースマップを有効にする
// sourceMap: true,
postcssOptions: {
plugins: [
['postcss-normalize-charset', {},],
['autoprefixer', {}],
['postcss-sort-media-queries', {}],
['css-declaration-sorter', {order:'smacss'}],
[
'@fullhuman/postcss-purgecss',
{
//purgecssオプション:ejsファイルとjsファイルを監視対象にする
content: [`${filePath.ejs}**/*.ejs`,`${filePath.js}**/*.js`],
//purgecssオプション:除外設定 https://purgecss.com/safelisting.html
safelist: cssSafeList //除外要素は8行目で設定
}
]
],
},
},
},
{
loader: "sass-loader",
options: {
// ソースマップの利用有無
sourceMap: enabledSourceMap
},
},
],
},
//CSS内の画像読み込み設定
{
test: /\.(gif|png|jpg|svg)$/,
// 閾値以上だったら埋め込まずファイルとして分離する
type: "asset",
parser: {
dataUrlCondition: {
// 4KB以上だったら埋め込まずファイルとして分離する
maxSize: 4 * 1024,
},
},
//書き出し設定
generator: {
filename: 'assets/images/[name][ext]'
}
},
//CSS内のWebfont読み込み設定
{
test: /\.(ttf|otf|eot|woff|woff2)$/,
// 閾値以上だったら埋め込まずファイルとして分離する
type: "asset",
parser: {
dataUrlCondition: {
// 4KB以上だったら埋め込まずファイルとして分離する
maxSize: 4 * 1024,
},
},
//書き出し設定
generator: {
filename: 'assets/fonts/[name][ext]'
}
},
],
},
plugins: [
// CSS出力
...cssGlobPlugins(entriesScss),
// ejs出力
...htmlGlobPlugins(entries),
//ディレクトリコピー
new CopyPlugin({
patterns: [
//画像コピー
{
from: path.resolve(__dirname, 'src/images/'),
to: path.resolve(__dirname, 'dist/assets/images'),
},
],
}),
],
};
}
module.exports = { webpackConfig };
webpack.dev.js (開発用設定ファイル)
webpack.dev.js
//webpack.common.jsを読み込み'development'設定で出力
const { webpackConfig } = require('./webpack.common');
module.exports = webpackConfig('development');
webpack.prod.js (ビルド用設定ファイル)
webpack.prod.js
//webpack.common.jsを読み込み'production'設定で出力
const { webpackConfig } = require('./webpack.common');
module.exports = webpackConfig('production');
package.jsonへコマンドを追加
package.jsonへ各コマンドを追加します。
package.json
{
〜省略〜
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack serve --config webpack.dev.js",
"watch": "webpack --watch --config webpack.dev.js"
},
〜省略〜
}
ビルドコマンド
npm run build
でファイル一式が書き出されます。
npm run build
開発コマンド
npm run dev
でローカルサーバーが立ち上がり開発を開始できます。
npm run dev
差分ビルドコマンド
npm run watch
でファイルの差分ビルドを開始できます。
npm run watch
package.json
“devDependencies”の各バージョンは記事執筆時のものですので、package.jsonをコピペで利用する場合は更新がないか確認をしてください。
{
"name": "プロジェクト名",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack serve --config webpack.dev.js",
"watch": "webpack --watch --config webpack.dev.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"@fullhuman/postcss-purgecss": "^5.0.0",
"autoprefixer": "^10.4.13",
"babel-loader": "^9.1.2",
"copy-webpack-plugin": "^11.0.0",
"css-declaration-sorter": "^6.3.1",
"css-loader": "^6.7.3",
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.7.2",
"postcss": "^8.4.21",
"postcss-loader": "^7.0.2",
"postcss-normalize-charset": "^5.1.0",
"postcss-sort-media-queries": "^4.3.0",
"sass": "^1.57.1",
"sass-loader": "^13.2.0",
"template-ejs-loader": "^0.9.4",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1",
"webpack-watched-glob-entries-plugin": "^2.2.6"
},
"browserslist": [
"last 3 versions",
"> 5%",
"Firefox ESR",
"not dead"
]
}
ディレクトリ構成
開発ファイルを src/で管理する構成です。
■プロジェクトディレクトリ
┣ dist (ビルドしたファイルが出力される場所)
┣ package.json (プロジェクトのjsonファイル)
┣ webpack.common.js (webpackの設定ファイル(ベース))
┣ webpack.dev.js (webpackの設定ファイル(開発用))
┣ webpack.prod.js (webpackの設定ファイル(ビルド用))
┣ .jsbeautifyrc (任意:HTML整形プラグインjs-beautifyの設定ファイル)
┃
┣ node_modules (編集不要:自動生成されるコアファイル格納場所)
┣ package-lock.json (編集不要:インストールしたパッケージ情報などが記載されている)
┃
┗ src
┣ ejs
┣ json (ejsで利用するjsonファイルを格納)
┣ js
┣ images
┗ scss
.jsbeautifyrc は任意追加です。
設定の詳細は後述する js-beautifyでビルドしたHTMLを整形する(任意追加) をご参照ください。
EJS内でのファイル参照について
EJSファイル内のJSやCSSを自動挿入したり、自動的に画像などのファイルを参照する設定は無効化していますので、普通のコーディングと同じように手動で各ファイルを参照してください。
index.ejs
<!DOCTYPE html>
<html lang="ja">
<head>
<link rel="stylesheet" href="./assets/css/style.css">
</head>
<body>
<img src="./assets/images/hoge.jpg" alt="">
<script src="./assets/js/fuga.js"></script>
</body>
</html>
EJSでJSONファイルを参照する
require()
を使うとJSONファイルからデータを引用することができます。
EJSファイルがある場所から相対パスで参照します。
JSファイルに更新パラメータを追加した例
src/json/data.json
{
"param": "2022071601"
}
src/ejs/index.ejs
<body>
<% const jsonData = require('../json/data.json') %>
<script src="./assets/js/common.js?<%= jsonData.param %>"></script>
</body>
</html>
JSON以外にもJSファイルで作成したオブジェクトを参照することも可能です。
詳細は以下をご参照ください。
js-beautifyでビルドしたHTMLを整形する(任意追加)
npm run build
でビルドしたHTMLをnpm scriptsを用いて整形する設定です。
環境に合わせて適宜導入を検討してください。
この項はwebpackの機能とは独立しており導入必須ではありません。
有名な整形ツールとしてprettierがありますが、本記事ではjs-beautifyを用いてHTMLを整形します。
js-beautifyはprettierと違って、整形しないタグを設定(除外設定)できたり、不用意な改行が入るといったことがないため、個人的にHTML整形においてはprettierより使いやすと思っています。
HTML整形プラグインjs-beautifyをインストール
以下のコマンドを実行してHTML整形プラグインをインストールします。
npm install -D js-beautify
.jsbeautifyrcのファイルを作成(js-beautifyの設定)
開発ディレクトリに.jsbeautifyrcを作成し、JSON形式でHTML整形のオプション設定を追加します。
.jsbeautifyrc
{
"html": {
"indent_size": 2,
"unformatted": ["svg", "pre"]
}
}
オプション設定は環境に合わせて適宜変更してください。
詳細は以下をご参照ください。
.jsbeautifyrcに追記する書き方は以下が参考になります。
package.jsonへコマンドを追加
package.jsonの "build"
コマンドにjs-beautifyのコマンドを追記します。
{
〜省略〜
"scripts": {
"build": "webpack --config webpack.prod.js && html-beautify dist/**/*.html",
〜省略〜
},
〜省略〜
}
実行方法
npm run build
を実行すると自動的に dist/ にあるHTMLファイルが整形されるようになります。
PostCssのオプション設定について
webpack.common.jsにオプション設定を記載しています。
各プラグインはpostcssOptions.plugins
で設定したものが呼び出されますので、不要なものは適宜削除してください。
webpack.common.js
const webpackConfig = (MODE) => {
//purgecssの除外設定
const cssSafeList = ['hoge'];
〜省略〜
//ビルド設定を各jsファイルへ渡す
return {
〜省略〜
module: {
rules: [
〜省略〜
//Sass
{
test: /\.scss/, // 対象となるファイルの拡張子
use: [
〜省略〜
// PostCSSのための設定
{
loader: "postcss-loader",
options: {
// PostCSS側でもソースマップを有効にする
// sourceMap: true,
postcssOptions: {
plugins: [
['postcss-normalize-charset', {},],
['autoprefixer', {}],
['postcss-sort-media-queries', {}],
['css-declaration-sorter', {order:'smacss'}],
[
'@fullhuman/postcss-purgecss',
{
//purgecssオプション:ejsファイルとjsファイルを監視対象にする
content: [`${filePath.ejs}**/*.ejs`,`${filePath.js}**/*.js`],
//purgecssオプション:除外設定 https://purgecss.com/safelisting.html
safelist: cssSafeList //除外要素は8行目で設定
}
]
],
},
},
},
{
loader: "sass-loader",
options: {
// ソースマップの利用有無
sourceMap: enabledSourceMap
},
},
],
},
〜省略〜
],
},
plugins: [
〜省略〜
],
};
}
module.exports = { webpackConfig };
PostCssのプラグインについて
ベンダープレフィックスを追加するautoprefixer
メディアクエリをソートして1つにまとめる postcss-sort-media-queries
CSSプロパティの順番をソートする css-declaration-sorter
CSSファイルから未使用のスタイルを削除する postcss-purgecss
案件によっては利用しない方が良いこともありますので、本当に必要かどうか必ず検討してください。
CSSファイルの先頭にcharset追加する postcss-normalize-charset
昔文字化けした経験があって明記しておきたい派なので個人的に導入していますが、最近はブラウザがよしなに判定してくれるようなので多くの場合は不要だと思います。
CSSファイル内の画像読み込みについて(base64変換)
Sassファイル内で画像を読み込んだ時、4KB以下の画像だった場合はビルド時にbase64へ自動変換され画像が文字情報として埋め込まれます。
- 4KB以下の画像:ビルド時にbase64へ自動変換し文字情報として埋め込まれる
- 4KBを超える画像:imagesへ格納した画像をそのまま参照する
ビルド前に4KB以下の画像を参照している例
src/style.scss
.hoge {
background-image: url(../images/piyo.svg);
}
base64に変換された例
dist/assets/css/style.css
.hoge {
/* 4KB以下はbase64へ変換される */
background-image: url(data:image/svg+xml;base64,xxx...);
}
容量の基準値を変更する
容量の基準値はwebpack.common.jsのparser.dataUrlCondition
で変更可能です。
webpack.common.js
const webpackConfig = (MODE) => {
〜省略〜
//ビルド設定を各jsファイルへ渡す
return {
〜省略〜
module: {
rules: [
〜省略〜
//CSS内の画像読み込み設定
{
test: /\.(gif|png|jpg|svg)$/,
// 閾値以上だったら埋め込まずファイルとして分離する
type: "asset",
parser: {
dataUrlCondition: {
// 4KB以上だったら埋め込まずファイルとして分離する
maxSize: 4 * 1024,
},
},
//書き出し設定
generator: {
filename: 'assets/images/[name][ext]'
}
},
],
},
plugins: [
〜省略〜
],
};
}
module.exports = { webpackConfig };
@babel/preset-envのtargets設定について
IE11対応が不要になった昨今ES5への対応は不要と考え、@babel/preset-envの設定はひとまず defaults
にしています。
targetsはautoprefixerと同じbrowserslistに順ずるようで、defaults
は > 0.5%, last 2 versions, Firefox ESR, not dead
とのことですので、モダン環境は問題ないように思いますが、記事執筆時点では未検証ですのでもし不具合など出るようなら各自の環境に合わせて対応範囲を変更してください。
webpack.common.js
const webpackConfig = (MODE) => {
〜省略〜
//ビルド設定を各jsファイルへ渡す
return {
〜省略〜
module: {
rules: [
〜省略〜
//babel-loader
{
// 拡張子 .js の場合
test: /\.js$/,
exclude: /node_modules/,
use: [
{
// Babel を利用する
loader: "babel-loader",
// Babel のオプションを指定する
options: {
presets: [
['@babel/preset-env', { targets: "defaults" }]
],
},
},
],
},
〜省略〜
],
},
plugins: [
〜省略〜
],
};
}
module.exports = { webpackConfig };
browserslistなど各内容の詳細は以下をご参照ください。
src/images/の移動について
ファイルコピープラグインcopy-webpack-pluginでnpm run build
時に複製する仕様です。
他にもそのまま複製したいディレクトリがある場合は patterns
オプションに任意の値を設定したオブジェクトを追加していってください。
コピープラグインはコピー元(今回はsrc/images/)のファイルが0件だとエラーになるため、コピーするファイルが存在しない場合はコードをコメントアウトしてください。
webpack.common.js
/*
webpack.dev.js、webpack.prod.jsへビルド設定を渡す
引数[MODE]は webpack の出力オプション('production' か 'development')を指定します
*/
const webpackConfig = (MODE) => {
〜省略〜
const CopyPlugin = require("copy-webpack-plugin");
〜省略〜
//ビルド設定を各jsファイルへ渡す
return {
〜省略〜
module: {
〜省略〜
},
plugins: [
〜省略〜
//ディレクトリコピー
new CopyPlugin({
patterns: [
//画像コピー
{
from: path.resolve(__dirname, 'src/images/'),
to: path.resolve(__dirname, 'dist/assets/images'),
},
],
}),
],
};
}
module.exports = { webpackConfig };
ビルド時にdistフォルダをクリーンアップする・しない
output.clean
設定を true
にすると npm run build
した時にdistフォルダをクリーンアップしてからファイルをビルドします。
クリーンアップしたくない場合は false
にするか、項目自体を削除してください。
webpack.common.js
const webpackConfig = (MODE) => {
〜省略〜
//ビルド設定を各jsファイルへ渡す
return {
〜省略〜
//JS書き出し設定
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'assets/js/[name].js',
clean: true, //ビルド時にdistフォルダをクリーンアップする
},
module: {
〜省略〜
},
plugins: [
〜省略〜
],
};
}
module.exports = { webpackConfig };
ファイル分割の仕様について
ビルド時と開発時で出力オプション(production
か development
)を変更する時、毎回オプションを手動で書き換えるのは手間だったので調べてみると、ビルドと開発時でファイルを分割管理する方法がありました。
一般的にはwebpack-mergeプラグインを導入して対応しているようなのですが、これを設定するとModule not foundのエラーが出てしまいました。
おそらくエラーはresolveオプションなどで上手く解決するのだと思うのですが自力で解決できなかったため導入を見送りました。
結局出力オプションの分岐は自作関数で対処したので、メジャーな記事とはベース構造が若干異なっていると思います。
ローカル開発環境(localhost)のホットリロードオプションについて
ホットリロードは、ページ全体をリロードせずファイルの変更部分だけ更新する方法です。devServer.hot
オプションを有効化するとホットリロードされるようになります。
webpack.common.js
const webpackConfig = (MODE) => {
〜省略〜
//ビルド設定を各jsファイルへ渡す
return {
〜省略〜
// ローカル開発用環境を立ち上げる
// 実行時にブラウザが自動的に localhost を開く
devServer: {
hot: true, //ホットリロードを有効化(変更された部分のみを更新)
static: path.resolve(__dirname, 'src'),
open: true
},
target: 'web', //ローカルサーバのリロードを有効化する
〜省略〜
module: {
〜省略〜
},
plugins: [
〜省略〜
],
};
}
module.exports = { webpackConfig };
詳細は以下をご参照ください。
ローカルサーバー起動時に特定のディレクトリを開く
devServer.opne
に開きたいディレクトリを指定すると、ローカルサーバーが起動した時に自動的にそのディレクトリが開くようになります。
devServer.openPageで設定すると言及している記事もありますが、
記事執筆時の最新環境では動作しませんでした。
localhost/my-page/ を開く設定例
module.exports = {
//...
devServer: {
open: ['/my-page/'],
},
};
詳細は以下をご参照ください。