webpackは以前からずっと使っていますが、業務で使用する機会が無く、個人プロジェクトで導入するのみという状況でした。
こうなるとwebpack.config.jsを触るのは自分だけなので、ネットから寄せ集めた設定を組み合わせて雰囲気で動かしていました。
このままでは他人から渡されたり自動生成されたwebpack.config.jsをメンテする際に苦労しそうなので、create-react-app
のwebpack.config.jsを読んで、いろいろメモを書いていこうと思います。
トランスコンパイラには、最近勉強しているTypeScriptを使っています。
create-react-app: v1.5.2
準備
- アプリケーション生成
$ create-react-app --scripts-version=react-scripts-ts sample-app
- eject
$ cd sample-app
$ yarn eject
config
配下に生成された以下のファイルが、webpack関連の設定ファイルです。
config
├── paths.js
├── webpack.config.dev.js
├── webpack.config.prod.js
└── webpackDevServer.config.js
ファイル構成
-
paths.js
- エントリポイントやバンドルファイル出力先フォルダなどのパスを一括管理しています。
-
webpack.config.dev.js
- 開発時に使用する設定
-
webpack.config.prod.js
- リリース用ビルド時に使用する設定
-
webpackDevServer.config.js
- webpack-dev-serverの設定
webpack.config.dev.js
今回は開発時の設定webpack.config.dev.js
を読んでいきます。
上部のrequire
部分は飛ばして、module.exportsの中身を見ていきます。
devtool
devtool: 'cheap-module-source-map',
設定値によって、ビルド/差分ビルドの速度と出力されるソースマップの質が変わります。
指定可能な設定は公式ドキュメントにあります。
cheap-module-source-map
の場合、ソースマップにはコンパイル前のオリジナルコードが表示されますが、ビルド速度は普通、差分ビルドは少し遅めのようです。
また、表のquality欄に(lines only)と記載されているものは、行単位のソースマップしか出力できず、周辺の変数の情報などを取得できません。
例として、cheap-module-source-map
とinline-source-map
を比べてみます。
ブレークポイントで実行を中断したとき、inline-source-map
ではimportしているlogo
の中身が見えますが……
cheap-module-source-map
では、not definedと表示されます。
create-react-app
のデフォルト値はcheap-module-source-map
ですが、デバッグするときはinline-source-map
を使ったほうが捗りそうです。
entry
entry: [
require.resolve('./polyfills'),
// require.resolve('webpack-dev-server/client') + '?/',
// require.resolve('webpack/hot/dev-server'),
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appIndexJs,
],
ビルドのエントリポイント。
中段でdevServer用のエントリポイントを指定していますが、デフォルトではwebpack-dev-server
の代替として、react-dev-utilsのwebpackHotDevClientを使っています。
これを使うと、ビルドエラーの内容をブラウザ上に表示してくれます。
この機能が不要な場合は、上2行のコメントを外せば通常のwebpack-dev-server
を使用できます。
entry: [
require.resolve('./polyfills'),
require.resolve('webpack-dev-server/client') + '?/',
require.resolve('webpack/hot/dev-server'),
// require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appIndexJs,
],
新しくエントリポイントを追加するときは、ファイルの絶対パスか、webpack.config.jsの場所から辿ったファイルの場所を相対パスで指定します。
output
バンドルしたファイルの出力ルール。
output: {
pathinfo: true,
filename: 'static/js/bundle.js',
chunkFilename: 'static/js/[name].chunk.js',
publicPath: publicPath,
devtoolModuleFilenameTemplate: info =>
path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
},
output.pathinfo
require文に、実際のファイルの場所をコメント(/* ~~~ */)で付加します。
output.pathinfo
require文に、実際のファイルの場所をコメントで付加します。
output.filename, output.chunkFilename
出力ファイル名。
エントリポイントを複数指定した場合、output.chunkFilename
の規則に従ってそれぞれのファイルが出力されます。
output.publicPath
出力ファイルを<script>
タグなどで読み込む際の、ベースURL。
webpack.config.dev.jsでは /
が指定されていて、出力ファイル名(output.filename)が static/js/bundle.js
となっているため、読み込むときはそれらを繋いだ
<script type="text/javascript" src="/static/js/bundle.js"></script>
となります。
output.devtoolModuleFilenameTemplate
ソースマップの出力先(ドメイン、パス等)。
webpackのデフォルトでは webpack://
ドメイン配下に出力されますが、 create-react-app
ではバンドルファイルの出力先と同じ場所に出力しています。
また、絶対パスで出力することで、開発しているプロジェクトフォルダと同一の構成になるため、わかりやすいです。
開発ディレクトリ構成
├── config
│ ├── env.js
│ ├── jest
│ ├── paths.js
│ ├── polyfills.js
│ ├── webpack.config.dev.js
│ ├── webpack.config.prod.js
│ └── webpackDevServer.config.js
├── node_modules
└── src
├── App.css
├── App.test.tsx
├── App.tsx
├── index.css
├── index.tsx
├── logo.svg
└── registerServiceWorker.ts
実際のディレクトリから、バンドルしたファイルがそのままの構成で localhost:3000
配下に出力されています。
resolve
バンドル時のモジュールの依存解決に関する設定。
resolve.modules
モジュールの探索場所。
resolve: {
modules: ['node_modules', paths.appNodeModules].concat(
process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
),
......
}
create-react-app
では、NODE_PATH
にディレクトリを指定して起動すると、そのディレクトリを探索場所に追加するよう設定されています。
例えば以下のようなフォルダ構成で App.js -> Sample.js をimportするとき、普通は相対パスを使って記述します。
src
├── App.js
└── components
└── Sample.js
import Sample from './components/Sample'
しかし、NODE_PATH
に基準となるディレクトリ(今回はsrc
)を指定すると、こう書けるようになります。
import Sample from 'components/Sample'
Sample.js
の場所が変わってもsrc
フォルダから見たパスを指定すればimportできるので、「2つ上のディレクトリだから '../../'
で……」等と考えなくてもよくなります。
注意
TypeScriptでは上記の方法は使えません。代わりにtsconfig.json
内でbaseUrl
プロパティを指定することで、同様のimportが可能になります。
{
"compilerOptions": {
"baseUrl": "src",
}
}
resolve.extensions
import文で拡張子を省略したときに、ここで指定した拡張子を補ってファイルを探索してくれます。
resolve: {
......
extensions: [
'.mjs',
'.web.ts',
'.ts',
'.web.tsx',
'.tsx',
'.web.js',
'.js',
'.json',
'.web.jsx',
'.jsx',
],
......
}
resolve.alias
import文で使えるエイリアスを設定します。
resolve: {
......
alias: {
'react-native': 'react-native-web',
},
......
}
create-react-app
では、React Native for Webサポートのために'react-native'
を'react-native-web'
に置き換えています。
これにより、React Native for Webのコンポーネントをimport ... from 'react-native'
で読み込めます。
resolve.plugins
プラグイン設定。
resolve: {
......
plugins: [
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
new TsconfigPathsPlugin({ configFile: paths.appTsConfig }),
],
},
ModuleScopePlugin
importするモジュールを配置する場所をsrc
フォルダ配下に限定しています。
試しにsrc
と同階層にsample
フォルダを作ってimportしてみると、ちゃんとエラーになりました。
TsconfigPathsPlugin
tsconfig.json
の場所を指定します。
module
バンドルのルール設定。
module: {
strictExportPresence: true,
rules: [
......
],
},
module.strictExportPresence
モジュールのexport忘れをエラー扱いにします。
false
にするとexport忘れがエラーにならないためビルド自体は通りますが、そのモジュールを他モジュールから読み込んだ時点でエラーが発生します。
true
にしておくと、exportしていないモジュールを発見した時点でビルドがコケるので、すぐに発見できます。
module.rules
バンドル対象ファイルと、対象ファイルに対して使用するLoaderの設定。
処理対象ファイルのルール(正規表現)と、適用するLoaderを指定します。
Loaderは配列で複数指定できます。その場合、指定した逆順にLoaderが適用されていくので注意が必要です。
参考: Loader Definitions
簡単に書くと、
rules: [
{ test: /\.js$/, use: 'a-loader' },
{ test: /\.js$/, use: 'b-loader' },
{ test: /\.js$/, use: 'c-loader' },
]
この場合、拡張子.js
のファイルが c-loader
> b-loader
> a-loader
の順に処理されていきます。
同じ拡張子を対象にする場合はまとめて書くこともでき、上の例をまとめると
rules: [
{ test: /\.js$/, use: ['a-loader', 'b-loader', 'c-loader'] },
]
こうなります。
また、oneOf
という特殊な記述方法もあります。
oneOf
内に記述されたルールは、処理するファイルがどれか1つのルールにマッチし処理された時点で、他のルールとの照合を中止します。
1つのファイルに対してoneOf
内のLoaderが複数適用されることはありません。
注意すべきなのは、oneOf
配列のルール照合は記述順に行われるということです。
ここでcreate-react-app
のwebpack.config.jsを見てみます。
rules
部分を、重要なオプション以外を無視して擬似コードにするとこうなります。
rules: [
{
enforce: 'pre',
test: JavaScript,
use: 'source-map-laoder'
},
oneOf: [
{
test: Image,
use: 'url-loader',
options: {
limit: 10000,
name: 'static/media/[name].[hash:8].[ext]',
},
},
{
test: JavaScript,
use: 'babel-loader'
},
{
test: TypeScript,
use: 'ts-loader'
},
{
test: Stylesheet,
use: [
'style-loader',
'css-loader',
{ 'postcss-loader', options: autoprefixer }
]
},
{
exclude: [JavaScript, HTML, JSON],
use: 'file-loader',
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
},
],
]
このルール群をあえて文章にすると、
- 全てのJavaScriptファイルに対して、処理前に
source-map-loader
でソースマップを生成する - 以降はファイルタイプによって処理を分ける(1ファイルに付きどれか1つのみ適用)
- 画像ファイルは、
url-loader
でdata URI schemaに変換する- 画像サイズが10000Byte以上なら、変換はせずに指定フォルダにコピーする
- JavaScriptファイルは、
babel-loader
でトランスコンパイルする - TypeScriptファイルは、
ts-loader
でトランスコンパイルする - CSSファイルは
postcss-loader
でベンダープレフィックスを付けて変換したあと、css-loader
でimportを解決し、style-loader
で<style>
タグに埋め込む - 上記で処理されなかったJavaScript, HTML, JSON以外のファイルは、そのまま指定フォルダにコピーする
- 画像ファイルは、
こんな感じでしょうか。
plugins
使用するプラグイン設定。
plugins: [
new InterpolateHtmlPlugin(env.raw),
new HtmlWebpackPlugin({
inject: true,
template: paths.appHtml,
}),
new webpack.NamedModulesPlugin(),
new webpack.DefinePlugin(env.stringified),
new webpack.HotModuleReplacementPlugin(),
new CaseSensitivePathsPlugin(),
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new ForkTsCheckerWebpackPlugin({
async: false,
watch: paths.appSrc,
tsconfig: paths.appTsConfig,
tslint: paths.appTsLint,
}),
],
InterpolateHtmlPlugin
後述のHtmlWebpackPlugin
と併用し、index.html
に値を埋め込むために使用。
public/index.html
を見てみると、ところどころに%PUBLIC_URL%
という文字列があります。
InterpolateHtmlPlugin
にKeyと対応するValueを渡すと、HtmlWebpackPlugin
でファイルを配信する際にValueを埋め込みます。
HtmlWebpackPlugin
テンプレートのHTMLに、webpackでバンドルした出力ファイルを読み込むための<script>
コードを埋め込みます。
いろいろなオプションが指定できますが、create-react-app
では埋め込み先のテンプレート(template: paths.appHtml
)、埋め込みを許可するかどうか(inject: true
)を指定しています。
ちなみにinject
オプションはデフォルトでtrue
なので、こちらは削除しても正常に動きます。
webpack.NamedModulesPlugin
公式に「HMR時にモジュールの相対パスを表示するためのプラグイン」とありますが、正直どこに表示されているのかよく分からなかったです。
webpack.DefinePlugin
指定した環境変数を、フロントでも使用可能にする。
src/registerServiceWorker.js
でNODE_ENV
を使うために導入されているようです。
webpack.HotModuleReplacementPlugin
MHRを有効化します。
現時点ではCSSの変更にのみ対応しているようで、コンポーネント全体のHMRを有効化するには以下のような対応が必要です。
create-react-appでHMR
※ 追記
TypeScriptで利用する場合は@types/webpack-envパッケージが必要です。
$ yarn add -D @types/webpack-env
CaseSensitivePathsPlugin
Windows環境では、コード内でファイルパスを記述するときに大文字小文字を意識する必要がありません。
例えば以下のファイルをimportするとき、
src/Components/Sample.js
このようにimportしても正常に動作します。
import Sample from './components/sample.js'
しかしLinuxやmacOS環境では、上記のimport文はエラーとなります。
CaseSensitivePathsPlugin
を有効化すると、Windows環境でもこれがエラー扱いとなり、正確なパス記述を矯正することができます。
WatchMissingNodeModulesPlugin
足りてないパッケージをrequireなりimportなりしたときに、webpackは当然エラーを吐きます。
その後npm install
しても、通常はdevServerを一旦killして再起動しなければパッケージを認識してくれません。
WatchMissingNodeModulesPlugin
を導入すると、パッケージを新しくインストールしたとき、devServerの再起動をしなくても自動で認識してくれます。
webpack.IgnorePlugin
Moment.jsを使う場合に有用なプラグイン。
Moment.jsをそのままバンドルすると、膨大な量のロケールファイルが含まれ、ファイルサイズがとても大きくなってしまいます。
IgnorePlugin
の設定で、使わないロケールファイルを無視して、必要なものだけバンドルすることができます。
参考: webpack で moment.js の無駄なロケールファイルを除去する
ForkTsCheckerWebpackPlugin
TypeScriptの型チェック用プラグイン。
型チェック自体はts-loader
のオプションでも指定できますが、トランスパイル時に型チェックも同時に行うと、ビルドに時間がかかってしまいます。
ForkTsCheckerWebpackPlugin
を導入すると、ts-loader
ではトランスパイルのみ行い、型チェックは別プロセスで実行することができ、ビルド速度が改善します。
node
node: {
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
},
Node.js標準モジュールのpolyfillやmockを差し込む設定。
Node.js環境用に書かれたプログラムをブラウザなど別環境で動かしたいときに使えます。
performance
performance: {
hints: false,
},
実行時のパフォーマンスを上げるためのアドバイスを出してくれる機能ですが、create-react-app
はそのあたりの調整に関心を持っていないので、機能をオフにしているようです。
hints
プロパティを'warning'
か'error'
にすると機能が有効になりますが、バンドルファイルを250kB以下におさめる等、なかなか無茶なアドバイスが表示されてうっとおしいので、本当に必要な場合だけ有効化すればよいと思います。
Compiled with warnings.
asset size limit: The following asset(s) exceed the recommended size limit (250 kB).
This can impact web performance.
Assets:
static/js/bundle.js (1.66 MB)
entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (250 kB). This can impact web performance.
Entrypoints:
main (1.66 MB)
static/js/bundle.js
webpack performance recommendations:
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
For more info visit https://webpack.js.org/guides/code-splitting/
Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.
まとめ
いい勉強になったとはいえ、まだ他にも様々な機能があるので、全てを覚えておくのはかなり難しそうです。
公式をある程度眺めて、機能の概要だけでも頭に入れておくと良さそうです。
また、create-react-app
のwebpackはv3.x.xですが、本家のwebpackでは最近v4がリリースされました。
v4系に未対応のプラグインもあるようなので、アップデートは慎重に行ったほうがよいですね。