🌳

webpackとesbuild-loaderで爆速なVue開発環境を構築しよう

2021/06/11に公開6

はじめに

フロントエンドのライブラリ・フレームワークの移り変わりって激しいですよね。
webpack5 がリリースされてもう半年と少しくらい経ってしまいました。
最近では新たなフロントエンドのビルドツールに、esbuildvite などのビルドツールもますます盛り上がって来ています。

そういう中、webpack が今後どうなっていくのか少し不安というのがあります。今回は、esbuild から派生した、esbuild-loader を利用することで、webpack でも爆速な Vue のビルド環境が構築できるよということを書きますので、そんな不安を少しでも取り除ければいいなと思います。

また、今回は vite を利用しなくても、esbuild-loaderwebpack だけでも快適なビルド環境を構築できるということを主眼に置いています。

記事の対象

Node.js についてある程度理解している、npm などの環境構築が問題なくできる, webpack の環境構築やなにかしらフロントエンドの環境構築をしたことがある方を対象としています。

準備

以下、必要なモジュールなどをインストールしていきます。

npm i -D typescript ts-node @types/node webpack-cli webpack @types/webpack

これで webpack のコンフィグファイルが TypeScript で記述されてても起動できるようになりました。

以下で、必要な loader などをインストールしていきます。

npm i -D esbuild-loader fork-ts-checker-webpack-plugin html-webpack-plugin postcss-loader sass-loader css-loader thread-loader vue-loader vue-style-loader node-sass autoprefixer cssnano

今回は以下の package.jsondependenciesdevDependencies に記載されているnpmモジュールを利用します。(2021/06/11時点)

{
  "dependencies": {
    "vue": "^2.6.14"
  },
  "devDependencies": {
    "@types/webpack": "^5.28.0",
    "autoprefixer": "^10.2.6",
    "css-loader": "^5.2.6",
    "cssnano": "^3.10.0",
    "esbuild-loader": "^2.13.1",
    "fork-ts-checker-webpack-plugin": "^6.2.10",
    "html-webpack-plugin": "^5.3.1",
    "node-sass": "^6.0.0",
    "path": "^0.12.7",
    "postcss": "^8.3.2",
    "sass-loader": "^12.1.0",
    "thread-loader": "^3.0.4",
    "ts-node": "^10.0.0",
    "typescript": "^4.3.2",
    "vue-loader": "^15.9.7",
    "vue-style-loader": "^4.1.3",
    "vue-template-compiler": "^2.6.14",
    "webpack": "^5.38.1",
    "webpack-cli": "^4.7.2"
  }
}

今回メインとなっているnpmモジュールは、vue-loader, fork-ts-checker-webpack-plugin, esbuild-loader, thlead-loader なのでそれ以外はそこまで重要ではないです。

利用するnpmモジュールについて

esbuild-loader

先ほども説明しましたが、esbuild が提供しているAPIを利用して作成された、loader です。トランスフォーム処理をフックして、フックした際に取得したコードのトランスパイルに esbuild が利用されているので、よく TypeScript のトランスパイルに利用される ts-loader よりは確実に早いです。また、esbuild のビルド速度については esbuild のドキュメントに記載されています。

fork-ts-checker-webpack-plugin

webpack を利用する際に、TypeScript のトランスパイルによく利用されている、ts-loader の型チェックの部分のみを切り出したプラグインです。このプラグインを利用することで、
esbuild, esbuild-loader の弱点である、型チェックが行えないという問題を解決することができます。

thread-loader

基本的に、webpack を利用する際には、ファイルの拡張子ごとに loader を分けるなど、細かい設定をすることが多々あります。その際に、この thread-loader を使うことによって、各々の拡張子に対応する loader ごとにスレッドを分割し、マルチスレッドで処理を行うことできます。
thread-loader に関しては、webpack の公式ドキュメントでも紹介されています。
https://webpack.js.org/loaders/thread-loader

vue-loader

今回は、vue-loader の threadMode機能 と loaders機能を利用して、快適なビルド環境を実現します。その他の設定も利用しますが、大筋ではないので、無視していただいても構いません。
https://vue-loader-v14.vuejs.org/ja/options.html

webpackのバンドル設定

今回は、範囲を限定して、vue2 で、js もしくは、ts を利用する場合のバンドル設定を記載します。おそらく見立てでは、vue-nextにも応用可能なので、ぜひお試しください。

webpack.config.tsの内容を以下に記載します。
わかりづらい箇所には適宜コメントをしています。

const webpack = require('webpack');
const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
const cssNanoPlugin = require('cssnano');
const autoPrefixer = require('autoprefixer');
const forkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const threadLoader = require('thread-loader');
const vueLoaderPlugin = require('vue-loader/lib/plugin')

// thread-loader用の設定オプション
const vueWorkerPoolOptions = {
  workers: 1,
  workerParallelJobs: 50,
  workerNodeArgs: ['--max-old-space-size=1024'],
  poolTimeout: 1000,
  name: 'vue-loader-pool'
}
const cssWorkerPoolOptions = {
  workers: 1,
  workerParallelJobs: 50,
  workerNodeArgs: ['--max-old-space-size=1024'],
  poolTimeout: 1000,
  name: 'css-loader-pool'
}

// thread-loaderのpre-warmupを行うことで実行時の遅延を防ぐ
threadLoader.warmup(vueWorkerPoolOptions, [
  'vue-loader',
  'vue-style-loader'
]);
threadLoader.warmup(cssWorkerPoolOptions, [
  'css-loader',
  'sass-loader'
]);

const IS_DEV = process.env.NODE_ENV === 'development';
const postCssPlugins = !IS_DEV
  ? [
    cssNanoPlugin({ preset: 'default' }),
    autoPrefixer({ grid: true })
  ]
  : [autoPrefixer({ grid: true })]

module.exports = () => {
  const configs = {
    // ForkTsCheckerWebpackPluginが自動的にtsconfig.jsonを見つけるために必要
    context: __dirname,
    mode: process.env.NODE_ENV,
    devtool: '',
    entry: path.resolve(__dirname, 'src/index.ts'),
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'bundle.js'
    },
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src'),
        vue: 'vue/dist/vue.js',
      },
      extensions: ['.vue', '.js', '.ts', '.tsx'],
    },
    module: {
      rules: [
        {
          test: /\.vue$/,
          use: [
            {
              loader: 'thread-loader',
              options: vueWorkerPoolOptions,
            },
            // vue-loaderによってvueファイルに定義されているjs, ts, sassなどが解釈される。
            // その後、それぞれのトランスフォーム処理が各ファイルによって定義したloaderに渡されます。
	    // 事前にvue用の定義を各ファイルに設定することも可能です。
            {
              loader: 'vue-loader',
              options: {
                postcss: { plugins: postCssPlugins },
                loaders: {
                  sass: [
                    { loader: 'vue-style-loader' },
                    { loader: 'css-loader' },
                    {
                      loader: 'sass-loader',
                      options: {
                        sassOptions: { indentedSyntax: true }
                      },
                    },
                  ],
                },
		// vue-loader上の各loaderの処理にもthread-loaderを適用するための設定。
                threadMode: true,
              },
            },
          ],
        },
        {
          test: /\.(css|scss|sass)$/,
          use: [
            {
              loader: 'thread-loader',
              options: cssWorkerPoolOptions,
            },
            { loader: 'css-loader' },
            {
              loader: 'sass-loader',
              options: {
                sassOptions: { indentedSyntax: true }
              },
            },
          ],
        },
        {
          test: /\.js$/,
          // ここでvue関連のファイルをexcludeしないとエラーになります。
          // ts-loaderのappendToSuffixオプションとほぼ同じ理由です。
          exclude: /node_modules|\vue\/dist|\vue-loader/,
          loader: 'esbuild-loader',
          options: {
            loader: 'js',
            target: 'esnext',
          },
        },
        {
          test: /\.ts$/,
          // ここでvue関連のファイルをexcludeしないとエラーになります。
          // ts-loaderのappendToSuffixオプションとほぼ同じ理由です。
          exclude: /node_modules|\vue\/dist|\vue-loader/,
          loader: 'esbuild-loader',
          options: {
            loader: 'ts',
            target: 'esnext',
          },
        },
        {
          test: /\.tsx$/,
          // ここでvue関連をexcludeしないとエラーになる。
          // ts-loaderのappendToSuffixオプションとほぼ同じ理由。
          exclude: /node_modules|\vue\/dist|\vue-loader/,
          loader: 'esbuild-loader',
          options: {
            // loaderはstring型なのでtsとtsxファイルの設定を別々に定義する必要があります。
            loader: 'tsx',
            target: 'esnext',
          },
        },
      ],
    },
    plugins: [
      new vueLoaderPlugin(),
      new forkTsCheckerWebpackPlugin(),
      new htmlWebpackPlugin({
        filename: 'index.html',
        template: 'src/html/index.html',
      }),
    ],
    // 圧縮設定
    optimization: {
      minimize: !IS_DEV
    },
  }

  // webpack5以降からはdevtoolsを個別に後から設定する必要があります。
  if (IS_DEV) {
    configs.devtool = 'eval-source-map';
  }

  return configs;
}

npmスクリプトも以下のように設定することによって、便利になりそうですね。

{
  "scripts": {
    "watch": "NODE_ENV=\"development\" webpack --progress --mode development --watch --hot",
    "dev": "NODE_ENV=\"development\" && webpack --progress --mode development",
    "prod": "NODE_ENV=\"production\" && webpack --progress --mode production"
  },
}

注意点

  • esbuild-loaderes2015 以降のトランスパイルにしか対応していません。
  • IE11 などの対応は、 esbuild-loader では行えないので、babel-loader を別途かませる必要があります。

おわりに

なにか間違いや、質問などありましたらコメントにお願いします。

Discussion

shingo.sasakishingo.sasaki

esbuildwebpack の組み合わせの知見ありがとうございます!大変参考になりました!

本筋とはやや異なる質問になりますが、気になったのでコメント致します。

記事の環境では vue-loader^15.9.7 を使用しているようですが、15系は14系までとは異なり、SFC内で検出された <style lang="scss"> などのブロックを、scss ファイルを import しているかのように振る舞うよう、仕様が変わった認識です。 (参考)

そのため、15系の vue-loaderoptions には loaders は存在せず、そちらに対して thread-loader を適用する必要はないと思うんですが如何でしょうか?

(vue-loader 周りの私の認識もかなりあやふやなので、間違いがあればご指摘いただけると助かります)

からころ / karacoroからころ / karacoro

ご質問に対する、曖昧な回答は持ち合わせているのですが、回答の内容がvue-loaderの詳細な挙動を確認しないとなんとも言えないところですので、少々返信お待ちください。

からころ / karacoroからころ / karacoro

すぐに回答できそうでした。
いただいた、参考資料に廃止はされていないけれど、今後廃止になるかもねと書かれていますね。

v15 also allows using non-serializable options for loaders, which was not possible in previous versions.

わたしの意図としては、vueに対しては、vue-style-loaderが必要なこともあって 、loaderの設定を純粋に外部からimportするcss/sass/scssと、vueファイル内部のcss/sass/scssと分けたくて、このような定義にしていました。

また、vue-loader14/15のどちらでもこの定義方法であれば、そのまま利用できるかなと思いますので、こちらで大丈夫かと思います!

shingo.sasakishingo.sasaki

丁寧なご回答ありがとうございます!

わたしの意図としては、vueに対しては、vue-style-loaderが必要なこともあって 、loaderの設定を純粋に外部からimportするcss/sass/scssと、vueファイル内部のcss/sass/scssと分けたくて、このような定義にしていました。

なるほど。 vue-loader 15系の仕様としては、 loaders で指定されている場合は内部、されてない場合は外部の依存と扱われ、SFC内の依存とそれ以外でビルド戦略を分けるときはあえてこうしたりもするんですね。参考になりました!