はじめに こんにちは、インターン生の手塚です。 今回は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 で指定していますが、 string や string[] などでも指定できます。大規模な開発になると複数のファイルを読み込んで複数のバンドルを生成することもあるので、その場合はエントリーポイントにチャンク名をつけることで出力先でチャンク名を利用することができ、わかりやすくすることができます。 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.json の baseUrl , 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 を指定して、 exclude で node_modules 配下のファイルは除外することを設定しています。さらに、その中で、 exclude で /node_modules/codemirror/ をしているので node_modules 配下のうち codemirror のみを含めます。ここまでがruleのうち、条件です。そしてこれらの条件に合致するファイルを use で指定したloaderで処理します。今回は ts-loader で処理しており、 option で transpileOnly を true にすることで型のチェックや型定義ファイルの出力を省略することでコンパイルにかかる時間を少なくしています。ここで型のチェックを行わない代わりに、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 このプラグインではモジュールを import や require で読み込まなくて自動的に読み込む機能を提供します。 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 この項目では「複数のエントリーポイント間で利用している共通モジュールをバンドルしたファイル」を出力するための設定をします。 splitChunks に cacheGroups を設定することで共通化するバンドルファイルをグループ化することができます。 1つ目の設定を例にみると scss, sass, css のファイルが複数のモジュールから利用されている場合、チャンクの名前がnullでなく、名前に「style-, theme-, legacy-presentation」が含まれない場合、 styles-style-commons にバンドルファイルが生成されます。さらに minSize で 1 が設定されているので1byte未満のモジュールは複数のモジュールで利用されていても共通化はしないような設定になっています。また、 priority が設定されているのでチャンクが複数の cacheGroup にまたがっているときには priority が高いグループが優先されます。 minimizer この項目に値をいれることで実行時に利用するデフォルトのminimizerを上書きすることができます。 performance この項目ではwebpackの様々な挙動を設定します。GROWIでは開発時に hints を false にすることでwebpackが何か変化を検知しても警告や注意を出さない設定にしています。 まとめ 以上がGROWIにおけるwebpackの設定です。最初にも書いたように、理想はこんな複雑な設定を書かなくてもだれでもモジュールバンドラーの設定ができることでしょう。しかし、webpackはまだまだ使われているため、webpackの設定を知っていないと困る場面があるかもしれません。今回はそんなwebpackの設定について調べてみたという記事でした。