制作の方針
- webpack5を採用し汎用性の高い構成を採用しています。
- Pug、Stylusを採用することで既存の関数を複数のプロジェクトで使用し効率化を図ります。
- プロジェクトに参画しやすいようわかりやすい構成を意識しています。
ディレクトリ構成
今回以下の構成にします。
開発環境用はdistへ、本番環境用ファイルはprodへビルドを想定しています。
├─ src/
│ ├─ html/
│ │ └─ index.pug
│ ├─ static/
│ ├─ js/
│ │ └─ index.ts
│ ├─ stylus/
│ │ └─ index.styl
│ └─ images/
├─ dist/
├─ prod/
├─ package.json
├─ postcss.config.js
└─ tsconfig.json
事前準備
以下の設定ファイルを予め用意してnpm install
を行います。
- package.json
- postcss.config.js
- tsconfig.json
package.json
今回使用するライブラリをdevDependenciesに記載します。
開発用にはnpm run start
本番公開用にはnpm run build
を実行致します。
{
"name": "sample",
"version": "1.0.0",
"description": "",
"main": "index.js",
"config": {
"path": "/"
},
"scripts": {
"start": "webpack --watch --mode development",
"build": "webpack --mode production"
},
"author": "",
"license": "ISC",
"devDependencies": {
"autoprefixer": "^10.4.16",
"browser-sync-webpack-plugin": "^2.3.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1",
"css-minimizer-webpack-plugin": "^5.0.1",
"globule": "^1.3.4",
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.4",
"html-webpack-pug-plugin": "^4.0.0",
"image-webpack-loader": "^8.1.0",
"mini-css-extract-plugin": "^2.7.6",
"postcss-loader": "^7.3.3",
"pug-html-loader": "^1.1.5",
"stylus-loader": "^7.1.3",
"terser-webpack-plugin": "^5.3.9",
"ts-loader": "^9.5.1",
"typescript": "^5.3.3",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
}
}
postcss.config.js
module.exports = {
plugins: [
require("autoprefixer")({
cascade: false,
grid: "autoplace",
}),
],
};
tsconfig.json
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"strict": true,
"module": "es6",
"moduleResolution": "node",
"target": "es5",
"jsx": "react"
},
"include": ["src/**/*"],
"exclude": ["./dist/"]
}
こちらでnpm install
を行います。
TypeScriptをコンパイル
まずはエントリーファイルとなるTypeScriptのコンパイルを実装致します。
こちらを実行すると/assets/js/bundle.js
が出力されます。
※TerserPluginを使用して本番環境からconsole.logを削除しています。
※fullhashで公開時はファイル名にハッシュ値を付与します。
※開発環境のみsource-mapの出力を行います。
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
module.exports = (_, args) => {
const isDevelopment = args.mode === "development";
const outputPath = isDevelopment ? "dist" : "prod";
const jsName = isDevelopment ? "bundle.js" : "bundle.[fullhash].js";
const config = {
entry: path.resolve(__dirname, "src/js/index.ts"),
output: {
path: path.resolve(__dirname, outputPath),
publicPath: "/",
filename: `assets/js/${jsName}`,
},
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
plugins: [],
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: isDevelopment ? false : true,
},
},
}),
],
},
};
if (isDevelopment) {
config.devtool = "source-map";
} else {
config.devtool = false;
}
return config;
};
stylusをコンパイル
※MiniCssExtractPluginを使用してビルドされるJavaScriptファイルからCSSファイルとして抽出します。
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = (_, args) => {
const cssName = isDevelopment ? "style.css" : "style.[hash].css";
const config = {
module: {
rules: [
{
test: /\.styl$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
url: true,
import: true,
},
},
"postcss-loader",
"stylus-loader",
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: `assets/css/${cssName}`,
}),
],
optimization: {
minimizer: [
new CssMinimizerPlugin(),
],
},
};
};
index.tsでindex.stylを読み込む
import "../stylus/style.styl";
画像をコンパイル
デフォルトの設定ではビルド時にファイル名が変更されます。
そこでimage-webpack-loaderを使いディレクトリ構成とファイル名を変えずに最適化してコンパイルします。
module.exports = (_, args) => {
const config = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: "asset/resource",
generator: {
filename: (pathData) => {
const newPath = pathData.filename.replace("src/", "");
return `assets/${newPath}`;
},
},
use: [
{
loader: "image-webpack-loader",
options: {
mozjpeg: {
progressive: true,
quality: 75,
},
optipng: {
enabled: true,
optimizationLevel: 7,
},
pngquant: {
quality: [0.65, 0.75],
speed: 4,
},
gifsicle: {
interlaced: false,
optimizationLevel: 1,
},
webp: {
quality: 75,
},
},
},
],
},
],
},
};
};
pugをコンパイル
まずはsrc/html/
内のpugファイルをで先頭に_の無いファイルを除外して一覧で取得します。
※本番環境ではhtmlにpretty
オプションでminify化しています。
※data
オプションでpugファイルで使用する変数を渡しています。
今回はif(isDevelopment === false)
で開発環境の条件分岐を行うことができます。
const globule = require("globule");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const HtmlWebpackPugPlugin = require("html-webpack-pug-plugin");
module.exports = (_, args) => {
const config = {
module: {
rules: [
{
test: /\.pug$/,
use: [
"html-loader",
{
loader: "pug-html-loader",
options: {
pretty: isDevelopment ? true : false,
basepath: path.resolve(__dirname, outputPath),
data: {
isDevelopment,
},
},
},
],
},
],
},
plugins: [
new HtmlWebpackPugPlugin({
adjustIndent: true,
}),
],
optimization: {
...
},
};
const pugFiles = globule.find("./src/html/**/*.pug", {
ignore: ["./src/html/**/_*.pug"],
});
pugFiles.forEach((file) => {
const filename = file.replace("./src/html/", "").replace(".pug", ".html");
config.plugins.push(
new HtmlWebpackPlugin({
filename: filename,
template: file,
inject: "body",
minify: false,
})
);
});
return config;
};
ローカルサーバーの設定
browser-sync-webpack-pluginを使用してローカルサーバーを立ち上げます。
const BrowserSyncPlugin = require("browser-sync-webpack-plugin");
module.exports = (_, args) => {
const config = {
module: {
...
},
plugins: [
...
],
optimization: {
...
},
};
if (isDevelopment) {
config.plugins.push(
new BrowserSyncPlugin({
host: "localhost",
port: 3000,
server: { baseDir: [outputPath] },
startPath: "/",
files: ['./src/**/*.*'],
})
);
}
return config;
};
その他設定
その他ファイルのビルド
CopyFilePluginを使用してファビコンなどその他のファイルもビルドファイルに含めます。
今回src/static/
に入れたファイルがビルドファイルのルートディレクトリにコピーされます。
ビルド時に既存のファイルを削除
clean-webpack-pluginを使用して既存ファイルを削除致します。
ハッシュ値の異なるビルドファイルが残ってしまうのを防ぎます。
const CopyFilePlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = (_, args) => {
const config = {
module: {
rules: [
{
test: /\.pug$/,
use: [
"html-loader",
{
loader: "pug-html-loader",
options: {
pretty: isDevelopment ? true : false,
basepath: path.resolve(__dirname, outputPath),
data: {
isDevelopment,
},
},
},
],
},
],
},
plugins: [
new CleanWebpackPlugin(),
new CopyFilePlugin({
patterns: [
{
from: "./src/static",
to: path.resolve(__dirname, outputPath),
},
],
}),
],
optimization: {
...
},
};
};
ビルドファイルの構成
最終的にdist
、prod
それぞれこの様な構成でビルドされます。
├─ dist/
│ └─ index.html
│ └─ assets/
│ ├─ css/
│ │ └─ style.css
│ ├─ js/
│ │ └─ bundle.js
│ └─ images/
│ └─ hogehoge.png
└─ prod/
└─ index.html
└─ assets/
├─ css/
│ └─ style.[hash値].css
├─ js/
│ └─ bundle.[hash値].js
└─ images/
└─ hogehoge.png
webpack.config
最終的に完成したwebpack.config.jsです。
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const globule = require("globule");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const HtmlWebpackPugPlugin = require("html-webpack-pug-plugin");
const CopyFilePlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const BrowserSyncPlugin = require("browser-sync-webpack-plugin");
module.exports = (_, args) => {
const isDevelopment = args.mode === "development";
const outputPath = isDevelopment ? "dist" : "prod";
const jsName = isDevelopment ? "bundle.js" : "bundle.[fullhash].js";
const cssName = isDevelopment ? "style.css" : "style.[hash].css";
const config = {
entry: path.resolve(__dirname, "src/js/index.ts"),
output: {
path: path.resolve(__dirname, outputPath),
publicPath: "/",
filename: `assets/js/${jsName}`,
},
module: {
rules: [
{
test: /\.styl$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
url: true,
import: true,
},
},
"postcss-loader",
"stylus-loader",
],
},
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
{
test: /\.pug$/,
use: [
"html-loader",
{
loader: "pug-html-loader",
options: {
pretty: isDevelopment ? true : false,
basepath: path.resolve(__dirname, outputPath),
data: {
isDevelopment,
},
},
},
],
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: "asset/resource",
generator: {
filename: (pathData) => {
const newPath = pathData.filename.replace("src/", "");
return `assets/${newPath}`;
},
},
use: [
{
loader: "image-webpack-loader",
options: {
mozjpeg: {
progressive: true,
quality: 75,
},
optipng: {
enabled: true,
optimizationLevel: 7,
},
pngquant: {
quality: [0.65, 0.75],
speed: 4,
},
gifsicle: {
interlaced: false,
optimizationLevel: 1,
},
webp: {
quality: 75,
},
},
},
],
},
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
plugins: [
new MiniCssExtractPlugin({
filename: `assets/css/${cssName}`,
}),
new HtmlWebpackPugPlugin({
adjustIndent: true,
}),
new CleanWebpackPlugin(),
new CopyFilePlugin({
patterns: [
{
from: "./src/static",
to: path.resolve(__dirname, outputPath),
},
],
}),
],
optimization: {
minimize: true,
minimizer: [
new CssMinimizerPlugin(),
new TerserPlugin({
terserOptions: {
compress: {
drop_console: isDevelopment ? false : true,
},
},
}),
],
},
};
const pugFiles = globule.find("./src/html/**/*.pug", {
ignore: ["./src/html/**/_*.pug"],
});
pugFiles.forEach((file) => {
const filename = file.replace("./src/html/", "").replace(".pug", ".html");
config.plugins.push(
new HtmlWebpackPlugin({
filename: filename,
template: file,
inject: "body",
minify: false,
})
);
});
if (isDevelopment) {
config.plugins.push(
new BrowserSyncPlugin({
host: "localhost",
port: 3000,
server: { baseDir: [outputPath] },
startPath: "/",
files: ["./src/**/*.*"],
})
);
}
if (isDevelopment) {
config.devtool = "source-map";
} else {
config.devtool = false;
}
return config;
};