【webpack】GROWIにおけるwebpackの設定について

はじめに

こんにちは、インターン生の手塚です。

今回はGROWIにおけるwebpackの設定について、調べてみたので記事にします。この記事はGROWIにおけるwebpackの設定に着目しているのでwebpackの基礎知識や、使い方の詳細は説明していません。webpackについてある程度の知識がある人に、プロジェクトへの活用例として参考にしてもらえればなと思っております。

webpackは設定が複雑で、そのため「webpack職人」と呼ばれる人たちが存在します。本当は、誰もが簡単に設定できるのが理想であり、Next.jsなどでは一部を自動的にやってくれたりもしています。ですが、まだwebpackがweb開発におけるモジュールバンドラーとして多く用いられているのは事実であり、webpackの知識は持っていて損はないでしょう。ということを先輩に言われたのでそういう思いで勉強しました。

そもそもwebpackとは

公式ドキュメント

https://webpack.js.org/

webpackはいわゆる「モジュールバンドラー」と呼ばれるもので、設定ファイルの指示に基づいて複数のJSファイルやCSSファイル、画像ファイルなどを一つにまとめる機能を持っています。ブラウザを例に出すと、モジュールバンドラーを活用することで読み込むファイルの数が少なくなったり、バンドル化される際に無駄な行が省かれたりして効率よくファイルを読み込むことができます。そして、そのモジュールバンドラーの筆頭が「webpack」というわけです。

GROWIにおけるwebpackの使い方の概要・イメージ

この記事はwebpack4系に関する情報になります。2022年6月時点での最新版は5.73.0なので、最新の情報は公式ドキュメントを確認してください
https://webpack.js.org/

GROWIでは開発用と製品用の2種類のwebpack設定があり、それぞれの共通設定をまとめたファイルもあります。開発用と製品用の設定ファイルでそれぞれ共通の処理を呼び出しているイメージです。なのでwebpack設定関連のファイルは

  • webpack.common.js
  • webpack.dev.js
  • webpack.prod.js

の3つになります。

そして、それぞれのファイルに従ってJSファイルやCSSファイル、画像ファイルをまとめた上で、そのまとめられたファイルをブラウザ上で読み込むことでアプリが動いています。ここからはGROWIの実際の設定ファイルの中でもメインとなる共通の設定ファイルをみて説明していきます。webpackの設定はたくさんありますがこの記事はあくまでGROWIの設定の紹介なので登場しない設定もあります、ご了承ください。

GROWIにおけるwebpackの詳細設定

これが共通のファイルです。

// webpack.common.js

const path = require('path');
const webpack = require('webpack');

/*
  * Webpack Plugins
  */
const WebpackAssetsManifest = require('webpack-assets-manifest');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');

/*
  * Webpack configuration
  *
  */
module.exports = (options) => {
  return {
    mode: options.mode,
    entry: Object.assign({
      'js/boot':                      './src/client/boot',
      // ~~省略~~
      'styles/style-hackmd':          './src/styles-hackmd/style.scss',
    }, options.entry || {}), // Merge with env dependent settings
    output: Object.assign({
      path: path.resolve(__dirname, '../public'),
      publicPath: '/',
      filename: '[name].bundle.js',
    }, options.output || {}), // Merge with env dependent settings
    externals: {
      jquery: 'jQuery',
      emojione: 'emojione',
      hljs: 'hljs',
      'dtrace-provider': 'dtrace-provider',
    },
    resolve: {
      extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
      plugins: [
        new TsconfigPathsPlugin({
          configFile: path.resolve(__dirname, '../tsconfig.build.client.json'),
          extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
        }),
      ],
    },
    node: {
      fs: 'empty',
    },
    module: {
      rules: options.module.rules.concat([
        // ~~省略~~
        {
          test: /\.(eot|woff2?|svg|ttf)([?]?.*)$/,
          use: 'null-loader',
        },
      ]),
    },
    plugins: options.plugins.concat([
      new WebpackAssetsManifest({ publicPath: true }),
      // ~~省略~~
    ]),

    devtool: options.devtool,
    target: 'web', // Make web variables accessible to webpack, e.g. window
    optimization: {
      namedModules: true,
      splitChunks: {
        cacheGroups: {
          style_commons: {
            test: /\.(sc|sa|c)ss$/,
            chunks: (chunk) => {
              // ignore patterns
              return chunk.name != null && !chunk.name.match(/style-|theme-|legacy-presentation/);
            },
            name: 'styles/style-commons',
            minSize: 1,
            priority: 30,
            enforce: true,
          },
          // ~~省略~~
        },
      },
      minimizer: options.optimization.minimizer || [],
    },
    performance: options.performance || {},
    stats: options.stats || {},
  };
};

これは共通のファイルですが、開発用、製品用の設定もあります。
それは、上の共通ファイルにもあるように

options.module.rules.concat([

このようにして、共通のファイルと開発用、製品用の設定をそれぞれマージしています。

設定の詳細についてみていきましょう。

mode

mode: options.mode, // 開発用、製品用でそれぞれ'development', 'production'を設定

この項目に設定できるのは、production, development, noneの3つです。productionモードでは不要な行が削除されたり、ブラウザが読み込むのに最適化されているのでデバッグに向いていません。なのでGROWIではそれぞれ、開発用では development、本番用では productionを設定しています。

entry

entry: Object.assign({
      'js/boot':                      './src/client/boot',
      'js/app':                       './src/client/app',
      // ~~省略~~
      'styles/theme-blackboard':      './src/styles/theme/blackboard.scss',
      'styles/style-hackmd':          './src/styles-hackmd/style.scss',
    }, options.entry || {}), // Merge with env dependent settings

この項目では読み込みを開始するファイル(エントリーポイント)を指定します。現在はObjctで指定していますが、stringstring[]などでも指定できます。大規模な開発になると複数のファイルを読み込んで複数のバンドルを生成することもあるので、その場合はエントリーポイントにチャンク名をつけることで出力先でチャンク名を利用することができ、わかりやすくすることができます。

output

output: Object.assign({
      path: path.resolve(__dirname, '../public'),
      publicPath: '/',
      filename: '[name].bundle.js',
    }, options.output || {}), // Merge with env dependent settings

この項目ではバンドル化されたファイルの出力先を指定します。GROWIではpublicディレクトリ下に[name].bundle.jsというバンドルファイルが生成されます。エントリーポイント設定の一番上の例でみると、js/bootというチャンク名で./src/client/bootのファイルが指定されているので/js/boot.bundle.jsというファイルがpublicディレクトリ下に生成されます。publicPathの項目では、outputに出力されたファイルが参照される先を指定します。GROWIの設定の場合、出力されたファイルは/ルートディレクトリから参照されます。

external

externals: {
      jquery: 'jQuery',
      emojione: 'emojione',
      hljs: 'hljs',
      'dtrace-provider': 'dtrace-provider',
    },

この項目では外部依存のままにしたいため、バンドル対象から外すものを設定しています。GROWIではjQueryやemojioneなどをこの項目に設定して、外部依存にしています。ここに設定せずにscriptでCDN読み込みをしていて、importして使用しているとwebpackはモジュール解決できずにエラーが出てしまいます。

resolve

resolve: {
      extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
      plugins: [
        new TsconfigPathsPlugin({
          configFile: path.resolve(__dirname, '../tsconfig.build.client.json'),
          extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
        }),
      ],
    },

この項目ではモジュールがバンドル化される際の設定をすることができます。

  • extensions

ここではエントリーポイントのファイル拡張子を定義します。webpackはファイルを処理するとき、ここに定義された拡張子を配列の先頭から見ていき、当てはまったときに処理を開始します。

  • plugins

ここではモジュールをバンドル化する際に使用するプラグインを定義します。GROWIではTsconfigPathsPluginというプラグインを用いています。ts-loaderを用いてJSのトランスパイルをするとき、tsconfig.jsonbaseUrl, pathsを用いている場合、このプラグインを入れないとpathsのエイリアスをwebpackが利用できません。

node

node: {
      fs: 'empty',
    },

この項目ではnodeにおけるモジュールに対して、ポリフィルを行ったり、モックを入れたりすることを設定します。nodeのモジュールはブラウザからは利用できないため、適切に設定をしないまま実行すると「fsがないよ」と怒られてしまいます。なのでGROWIではemptyを設定することでfsに空のオブジェクトをいれ、エラーを回避しています。

https://stackoverflow.com/questions/39249237/node-cannot-find-module-fs-when-using-webpack

上のリンクでも議論されていますが、これは少し古めの解決策になっています。

module

module: {
      rules: options.module.rules.concat([
        {
          test: /.(jsx?|tsx?)$/,
          exclude: {
            test: /node_modules/,
            exclude: [ // include as a result
              /node_modules\/codemirror/,
            ],
          },
          use: [{
            loader: 'ts-loader',
            options: {
              transpileOnly: true,
              configFile: path.resolve(__dirname, '../tsconfig.build.client.json'),
            },
          }],
        },
        {
          test: /locales/,
          loader: '@alienfast/i18next-loader',
          options: {
            basenameAsNamespace: true,
          },
        },
        /*
          * File loader for supporting images, for example, in CSS files.
          */
        {
          test: /\.(jpg|png|gif)$/,
          use: 'file-loader',
        },
        /* File loader for supporting fonts, for example, in CSS files.
         */
        {
          test: /\.(eot|woff2?|svg|ttf)([?]?.*)$/,
          use: 'null-loader',
        },
      ]),
    },

この項目ではそれぞれのモジュールがどのようにバンドル化されるかの詳細を設定します。

  • rules

公式ドキュメントによると、ruleには3つのパーツがあり、Conditions, Results, nested Rulesとされています。それぞれを解釈するなら、条件処理ネストされた条件となるかなと思います。この項目では基本的に、こんな条件の時はこういった処理をする、という設定をしています。一番上の例を見ると、正規表現でjs, jsx, ts, tsxを指定して、excludenode_modules配下のファイルは除外することを設定しています。さらに、その中で、exclude/node_modules/codemirror/をしているのでnode_modules配下のうちcodemirrorのみを含めます。ここまでがruleのうち、条件です。そしてこれらの条件に合致するファイルをuseで指定したloaderで処理します。今回はts-loaderで処理しており、optiontranspileOnlytrueにすることで型のチェックや型定義ファイルの出力を省略することでコンパイルにかかる時間を少なくしています。ここで型のチェックを行わない代わりに、GROWIではCIでtscを実行し、チェックしています。

1番上の設定では上記のようなことを行っています。

plugins

plugins: options.plugins.concat([

      new WebpackAssetsManifest({ publicPath: true }),

      new webpack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      }),

      // ignore
      new webpack.IgnorePlugin(/^\.\/lib\/deflate\.js/, /markdown-it-plantuml/),
      new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),

      new LodashModuleReplacementPlugin({
        flattening: true,
      }),

      new webpack.ProvidePlugin({ // refs externals
        jQuery: 'jquery',
        $: 'jquery',
      }),

    ]),

ここではloaderでは提供できない追加機能を提供するプラグインを設定します。

  • WebpackAssetsManifest

このプラグインでは元のファイル名とハッシュ化されたファイル名を対応させるためのJSONファイルを生成します。

  • webpack.DefinePlugin

このプラグインではコンパイル時に設定されるグローバルな定数を設定することができます。

  • webpack.IgnorePlugin

このプラグインではバンドル化される際に無視するものを定義します。例えば、localeファイルなどはここで設定することで使用しない大部分のファイルをバンドル時に無視することができます。

  • LodashModuleReplacementPlugin

このプラグインではloadshの中で使われているもののみをバンドル化してバンドルファイルが肥大化するのを防いでいます。

  • webpack.ProvidePlugin

このプラグインではモジュールをimportrequireで読み込まなくて自動的に読み込む機能を提供します。

devtool

devtool: options.devtool, // 開発時のみ'cheap-module-eval-source-map'を指定

この項目では、ソースマップを生成するかまたどのように生成するかを設定しています。ソースマップによってバンドル化されたファイルと元のファイルの関連がわかるのでデバッグがしやすくなるため、開発時にはとても便利です。このオプションにはたくさんの種類があるのですが、GROWIではts-loaderでサポートされているcheap-module-eval-source-mapを使用しています。製品版の設定ではこの項目は上書きされています。

target

target: 'web',

この項目では、生成したバンドルファイルのターゲットを設定します。例えばNode.jsの環境でrequireを使ってバンドルファイルを読み込む場合はここでnodeを設定したりするようですが、今回の目的はブラウザでHTMLから読み込むことなのでwebを指定します。

optimization

optimization: {
      namedModules: true,
      splitChunks: {
        cacheGroups: {
          style_commons: {
            test: /\.(sc|sa|c)ss$/,
            chunks: (chunk) => {
              // ignore patterns
              return chunk.name != null && !chunk.name.match(/style-|theme-|legacy-presentation/);
            },
            name: 'styles/style-commons',
            minSize: 1,
            priority: 30,
            enforce: true,
          },
          commons: {
            test: /(src|resource)[\\/].*\.(js|jsx|json)$/,
            chunks: (chunk) => {
              // ignore patterns
              return chunk.name != null && !chunk.name.match(/boot/);
            },
            name: 'js/commons',
            minChunks: 2,
            minSize: 1,
            priority: 20,
          },
          vendors: {
            test: /node_modules[\\/].*\.(js|jsx|json)$/,
            chunks: (chunk) => {
              // ignore patterns
              return chunk.name != null && !chunk.name.match(/boot|legacy-presentation|hackmd-/);
            },
            name: 'js/vendors',
            minSize: 1,
            priority: 10,
            enforce: true,
          },
        },
      },
      minimizer: options.optimization.minimizer || [],
    },

この項目ではwebpackを実行するときの様々な最適化についての設定をしています。

  • namedModule

この項目にtrueを設定することでモジュールに名前が設定され、デバッグがしやすくなります。

  • splitChunks

この項目では「複数のエントリーポイント間で利用している共通モジュールをバンドルしたファイル」を出力するための設定をします。splitChunkscacheGroupsを設定することで共通化するバンドルファイルをグループ化することができます。

1つ目の設定を例にみるとscss, sass, cssのファイルが複数のモジュールから利用されている場合、チャンクの名前がnullでなく、名前に「style-, theme-, legacy-presentation」が含まれない場合、styles-style-commonsにバンドルファイルが生成されます。さらにminSizeが設定されているので1byte未満のモジュールは複数のモジュールで利用されていても共通化はしないような設定になっています。また、priorityが設定されているのでチャンクが複数のcacheGroupにまたがっているときにはpriorityが高いグループが優先されます。

  • minimizer

この項目に値をいれることで実行時に利用するデフォルトのminimizerを上書きすることができます。

performance

この項目ではwebpackの様々な挙動を設定します。GROWIでは開発時にhintsfalseにすることでwebpackが何か変化を検知しても警告や注意を出さない設定にしています。

まとめ

以上がGROWIにおけるwebpackの設定です。最初にも書いたように、理想はこんな複雑な設定を書かなくてもだれでもモジュールバンドラーの設定ができることでしょう。しかし、webpackはまだまだ使われているため、webpackの設定を知っていないと困る場面があるかもしれません。今回はそんなwebpackの設定について調べてみたという記事でした。