マークアップエンジニアが考える最低限のwebpack構成

2024年3月31日

🔴2023/04/26追記🔴

こちらでご紹介しているpackage構成は、2023年現在では一部脆弱性エラーが発生しております。

2023年に構築した環境を紹介する記事を投稿しましたので、よろしければこちらをご参考ください

マークアップエンジニアが考えたNode.jsのFrontend Starter環境




近年はgrunt/gulp < webpackの風潮が強く、例にもれず自分もしょっちゅうwebpackで環境構築をしております。

【build-toolの関心度】

https://2020.stateofjs.com/en-US/technologies/build-tools/

ただ、毎回package構成やらバージョンの相性やらを忘れて調べているので、自分用の最小構成メモろうっていうのが本記事の趣旨。


webpack(モジュールバンドラー)なんだから全部バンドルしてjsにしちゃえばもっと楽なんだろうけど、企業勤めだと大体backend絡める時点でそんな作りには出来ない状況なのが常。。。

デザイナー・マークアップエンジニア・プログラマがノンストレスな、スキルセットや制作環境があればみんなハッピーなのになぁ。。。

閑話休題。


今回の環境で使う想定機能としてはこちら

  • HTMLファイルを圧縮
  • SCSSをCSSとしてファイル生成&圧縮
  • JS(ES6可)を圧縮
  • 画像を圧縮
  • webpackのstatsはエラーと警告のみにして、watch中はchokidarを使って変更ファイルを監視

developmentとproductionで分けて非圧縮・圧縮分けても良いけど、今回の環境では割愛。

常時mode:productionとしてbuildされます。

/* folder */
root
├── .babelrc
├── .gitignore
├── html-minifier.js
├── package.json
├── src
│ ├── img
│ │ └── img.jpg
│ ├── index.html
│ ├── js
│ │ ├── component
│ │ │ └── _sample.js
│ │ └── script.js
│ └── scss
│ ├── _sample.scss
│ └── style.scss
└── webpack.config.js
/* package.json */
{
 "name": "webpack_sample",
 "version": "1.0.0",
 "description": "",
 "scripts": {
  "build": "webpack --config webpack.config.js",
  "w": "webpack --watch --info-verbosity none --config webpack.config.js"
 },
 "author": "",
 "license": "ISC",
 "devDependencies": {
  "@babel/cli": "^7.14.8",
  "@babel/core": "^7.15.0",
  "@babel/preset-env": "^7.15.0",
  "autoprefixer": "^9.8.6",
  "babel-loader": "^8.2.2",
  "chokidar": "^3.5.2",
  "copy-webpack-plugin": "^6.3.2",
  "css-loader": "^4.2.2",
  "html-minifier": "^4.0.0",
  "imagemin-gifsicle": "^7.0.0",
  "imagemin-mozjpeg": "^9.0.0",
  "imagemin-pngquant": "^9.0.2",
  "imagemin-svgo": "^9.0.0",
  "imagemin-webpack-plugin": "^2.4.2",
  "mini-css-extract-plugin": "^0.11.0",
  "npm-run-all": "^4.1.5",
  "postcss": "^8.3.6",
  "postcss-loader": "^3.0.0",
  "rimraf": "^3.0.2",
  "sass-loader": "^7.2.0",
  "webpack": "^4.44.1",
  "webpack-cli": "^3.3.12",
  "webpack-fix-style-only-entries": "^0.5.1"
 }
}
/* webpack.config.js */
const glob = require("glob");
const { execSync } = require('child_process');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const FixStyleOnlyEntriesPlugin = require("webpack-fix-style-only-entries");
const CopyPlugin = require("copy-webpack-plugin");
const ImageminPlugin = require('imagemin-webpack-plugin').default;
const ImageminMozjpeg = require('imagemin-mozjpeg');
const ImageminPngquant = require('imagemin-pngquant');
const ImageminGifsicle = require('imagemin-gifsicle');
const ImageminSvgo = require('imagemin-svgo');
const chokidar = require('chokidar');
const fs = require('fs');

// mode set
let mode = "build";
if(process.argv.includes("--watch")){
  mode = "watch";
}

// entry set
let entries = {};
glob.sync("./src/?(js|scss)/**/*.?(js|scss)").map(function(file){
  fileName = file.match(".+/(.+?)\.[a-z]+([\?#;].*)?$")[1];
  if(fileName.slice(0,1) !== "_"){
    filePath = file.substring(0, file.lastIndexOf("."));
    filePath = filePath.replace(/src/g,"assets");
    filePath = filePath.replace(/scss/g,"css");
    entries[filePath] = file;
  }
});

const watcher = chokidar.watch('src/');
const watch = () => {
  watcher.on('ready', () => {
    console.log("watch start");

    watcher.on('all', (event, path) => {
      if(event === "unlink"){
        fs.unlinkSync(path.replace(/src/,"assets"));
      }
      if(path.indexOf(".html") > 0){
        execSync("npx html-minifier --input-dir src/ --output-dir assets/ --file-ext html -c html-minifier.js");
      }
      console.log(event, path);
    });
  });
};

module.exports = {
  stats: 'errors-warnings',
  mode: "production", // or development
  entry: entries,
  // ファイルの出力設定
  output: {
    //  出力ファイルのディレクトリ名
    path: `${__dirname}`,
    // 出力ファイル名
    filename: '[name].js'
  },
  module: {
    rules: [
      {
        test: /\.scss/, // 対象となるファイルの拡張子
        use: [
          { // JSデータをCSSとして外部ファイル化
            loader: MiniCssExtractPlugin.loader
          },
          { // CSSをJSに変換
            loader: 'css-loader',
            options: {
              url: false,
              sourceMap: true,
            }
          },
          { // PostCSSのための設定
            loader: 'postcss-loader',
            options: {
              sourceMap: true,
              plugins: [
                require('autoprefixer')({
                  overrideBrowserslist: ['ie >= 11', '> 5%']
                })
              ]
            }
          },
          { // SCSSをCSSに変換
            loader: 'sass-loader'
          },
        ]
      },
      {
        test: /\.js/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [ '@babel/preset-env' ]
          }
        }
      }
    ]
  },
  plugins: [
    // 不要なJSファイルは削除
    new FixStyleOnlyEntriesPlugin({
      silent: true
    }),
    // cssの出力先を指定する
    new MiniCssExtractPlugin({
      filename: '[name].css'
    }),
    // 画像の圧縮
    new ImageminPlugin({
      test: /\.(jpe?g|png|gif|svg)$/i,
      // minFileSize: 250000, // bite // 250kb以下で設定中
      plugins: [
        ImageminMozjpeg({ quality: 80 }),
        ImageminPngquant({ quality: [0.7, 0.8] }),
        ImageminGifsicle(),
        ImageminSvgo()
      ]
    }),
    // 画像ファイルの移動
    new CopyPlugin({
      patterns: [
        { from: "./src/img/", to: "./assets/img/" }
      ],
    }),
    () => {
      // assetsフォルダの削除
      execSync("rimraf assets");
      // HTMLファイルの圧縮
      execSync("mkdir assets && npx html-minifier --input-dir src/ --output-dir assets/ --file-ext html -c html-minifier.js");

      if(mode == "watch"){
        watch();
      }
    },
  ],
};