3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

eject後のwebpack.config.jsを1から読み解いてみる①

Last updated at Posted at 2020-08-11

はじめに

最近ReactやGraphQL, TypeScriptの環境構築をやっているのですがWebpackがあまりに難しすぎて挫折しかけたのでWebpackに対する苦手意識をなくすためにもwebpack.configの内容を1から調べていこうと思います。僕と同じくWebpackの内容の多さに絶望した方の助けになれば幸いです。

動作環境

  • npm 6.14.5
  • node.js 14.3.0
  • create-react-app 3.4.1

Ln1-Ln51

use strict

strictモードの呼び出し

  • 一部の問題が起こりやすいコードをエラーとして処理する
  • javascriptの処理を高速化する

strictモードについて細かく書くとそれだけで1記事分になりそうなので細かい違いはこちらをご参照ください。

const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const resolve = require('resolve');
const PnpWebpackPlugin = require('pnp-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const safePostCssParser = require('postcss-safe-parser');
const ManifestPlugin = require('webpack-manifest-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const paths = require('./paths');
const modules = require('./modules');
const getClientEnvironment = require('./env');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');

const postcssNormalize = require('postcss-normalize');

const appPackageJson = require(paths.appPackageJson);

基本的にインポートなので省略。

// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';

SourceMapを使用するかどうかのフラグ。
使用することでWebpackによってコードがまとめられたときにもとのコードの情報が残るようになりデバッグがしやすくなる。

// Some apps do not need the benefits of saving a web request, so not inlining the chunk
// makes for a smoother build process.
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';

InlineChunkHtmlPluginを使用するかのフラグ。
使用することで共通の設定をChunkの中に埋め込み、http通信の数を減らすことができる。

const isExtendingEslintConfig = process.env.EXTEND_ESLINT === 'true';

Eslintrcを使用するかのフラグ。
ただし2020/6/7時点で機能してないと思われる。
(こちらに報告あり)

const imageInlineSizeLimit = parseInt(
  process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);

url-loader使用時に事前読み込みを行うファイルサイズの上限を設定する。
事前読み込みを行うことでhttp接続を減らすことができる。

// Check if TypeScript is setup
const useTypeScript = fs.existsSync(paths.appTsConfig);

TypeScriptを使用しているかをチェックしている。
paths.jsで設定されているpaths.appTsConfigが存在するかで判定している。(初期ではtsconfig.json)

// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;

styleファイルの正規表現をまとめたもの。

Ln53-Ln129

module.exports = function(webpackEnv) {
  const isEnvDevelopment = webpackEnv === 'development';
  const isEnvProduction = webpackEnv === 'production';

以降はmodule.exportsという関数の定義となる。webpackEnvは開発環境か本番環境かを分ける引数。

// Variable used for enabling profiling in Production
// passed into alias object. Uses a flag if passed into the build command
const isEnvProductionProfile =
  isEnvProduction && process.argv.includes('--profile');

コードの圧縮時にclassnameや関数名を保存するか、パフォーマンスの計測を可能にするかのフラグを設定する。

// We will provide `paths.publicUrlOrPath` to our app
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
// Get environment variables to inject into our app.
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));

環境変数の読み込み。process.envの内容がenvに読み込まれる。
なお、process.envの設定はdotenvにより行われているが、dotenvファイルのパスはpaths.dotenvやenv.jsで変更できる。

// common function to get style loaders
const getStyleLoaders = (cssOptions, preProcessor) => {
  const loaders = [
    isEnvDevelopment && require.resolve('style-loader'),
    isEnvProduction && {
      loader: MiniCssExtractPlugin.loader,
      // css is located in `static/css`, use '../../' to locate index.html folder
      // in production `paths.publicUrlOrPath` can be a relative path
      options: paths.publicUrlOrPath.startsWith('.')
        ? { publicPath: '../../' }
        : {},
    },
    {
      loader: require.resolve('css-loader'),
      options: cssOptions,
    },
    {
      // Options for PostCSS as we reference these options twice
      // Adds vendor prefixing based on your specified browser support in
      // package.json
      loader: require.resolve('postcss-loader'),
      options: {
        // Necessary for external CSS imports to work
        // https://github.com/facebook/create-react-app/issues/2677
        ident: 'postcss',
        plugins: () => [
          require('postcss-flexbugs-fixes'),
          require('postcss-preset-env')({
            autoprefixer: {
              flexbox: 'no-2009',
            },
            stage: 3,
          }),
          // Adds PostCSS Normalize as the reset css with default options,
          // so that it honors browserslist config in package.json
          // which in turn let's users customize the target behavior as per their needs.
          postcssNormalize(),
        ],
        sourceMap: isEnvProduction && shouldUseSourceMap,
      },
    },
  ].filter(Boolean);
  if (preProcessor) {
    loaders.push(
      {
        loader: require.resolve('resolve-url-loader'),
        options: {
          sourceMap: isEnvProduction && shouldUseSourceMap,
        },
      },
      {
        loader: require.resolve(preProcessor),
        options: {
          sourceMap: true,
        },
      }
    );
  }
  return loaders;
};

style-loaderの設定
・style-loader (develop環境のみ)
 CSSをhtmlに埋め込む。
・MiniCssExtractPlugin.loader (production環境のみ)
 CSSを別ファイルに分離してまとめる。
 →htmlに埋め込まないためstyle-loaderは不要になる。
・css-loader
 CSSのメソッドをjavascriptのメソッドに変換する。
・postcss-loader
 pcssファイルをcssファイルに展開する。
 postcss-flexbugs-fixes→frexboxの挙動のズレを吸収する。
 postcss-preset-env→postcss-nestingなどを利用する。
 postcssNormalize→ブラウザごとのズレを吸収する。
・resolve-url-loader
 外部からファイルを読み込むときのパスを通す。
 getStyleLoadersで追加のloaderが指定されているときのみ使う。
 追加のloaderではsourceMapをtrueにする必要がある。

Ln131-Ln195

return {

ここからLn55の関数の返り値となる。

mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',

本番環境か開発環境かを返す。

// Stop compilation early in production
bail: isEnvProduction,

エラーが発生したときにコンパイルを早期終了させる。
(具体例などがなかったのでちょっとイメージが掴みづらいです...)

devtool: isEnvProduction
  ? shouldUseSourceMap
    ? 'source-map'
    : false
  : isEnvDevelopment && 'cheap-module-source-map',

bundle後のファイルでエラーが発生したときにbundle前のファイルをどう参照するかの設定。
Ln33のshouldUseSourceMapもここで使われる。
本番環境だともとのコードのままSourceMapが作られますが、開発環境だと少し簡略化された形でSourceMapが作られるようです。
(1ファイルが1行で表された形でbundleファイルが作られる...?)

entry: [
  // Include an alternative client for WebpackDevServer. A client's job is to
  // connect to WebpackDevServer by a socket and get notified about changes.
  // When you save a file, the client will either apply hot updates (in case
  // of CSS changes), or refresh the page (in case of JS changes). When you
  // make a syntax error, this client will display a syntax error overlay.
  // Note: instead of the default WebpackDevServer client, we use a custom one
  // to bring better experience for Create React App users. You can replace
  // the line below with these two lines if you prefer the stock client:
  // require.resolve('webpack-dev-server/client') + '?/',
  // require.resolve('webpack/hot/dev-server'),
  isEnvDevelopment &&
    require.resolve('react-dev-utils/webpackHotDevClient'),
  // Finally, this is your app's code:
  paths.appIndexJs,
  // We include the app code last so that if there is a runtime error during
  // initialization, it doesn't blow up the WebpackDevServer client, and
  // changing JS code would still trigger a refresh.
].filter(Boolean),

読み取りの起点を設定する。
初期だとpaths.appIndexJs(src/indexが指定されている)と、
WebpackDevServer(ファイルセーブしたときに再読込する)に接続するためのclient
の2つ。

output: {
  // The build folder.
  path: isEnvProduction ? paths.appBuild : undefined,
  // Add /* filename */ comments to generated require()s in the output.
  pathinfo: isEnvDevelopment,
  // There will be one main bundle, and one file per asynchronous chunk.
  // In development, it does not produce real files.
  filename: isEnvProduction
    ? 'static/js/[name].[contenthash:8].js'
    : isEnvDevelopment && 'static/js/bundle.js',
  // TODO: remove this when upgrading to webpack 5
  futureEmitAssets: true,
  // There are also additional JS chunk files if you use code splitting.
  chunkFilename: isEnvProduction
    ? 'static/js/[name].[contenthash:8].chunk.js'
    : isEnvDevelopment && 'static/js/[name].chunk.js',
  // webpack uses `publicPath` to determine where the app is being served from.
  // It requires a trailing slash, or the file assets will get an incorrect path.
  // We inferred the "public path" (such as / or /my-project) from homepage.
  publicPath: paths.publicUrlOrPath,
  // Point sourcemap entries to original disk location (format as URL on Windows)
  devtoolModuleFilenameTemplate: isEnvProduction
    ? info =>
        path
          .relative(paths.appSrc, info.absoluteResourcePath)
          .replace(/\\/g, '/')
    : isEnvDevelopment &&
      (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
  // Prevents conflicts when multiple webpack runtimes (from different apps)
  // are used on the same page.
  jsonpFunction: `webpackJsonp${appPackageJson.name}`,
  // this defaults to 'window', but by setting it to 'this' then
  // module chunks which are built will work in web workers as well.
  globalObject: 'this',
},

出力内容の設定
・path: どこにファイルを作成するか
・pathinfo: 使用したファイルをコメントで表示するか
・filename: 生成するファイルの名前
・futureEmitAssets:

Tells webpack to use the future version of asset emitting logic, which allows freeing memory of assets after emitting. It could break plugins which assume that assets are still readable after they were emitted.

ドキュメントより抜粋。常に最新のものを取得することで内部にデータを持たないようにするって感じだと解釈。ただ何を指すのかピンと来ない。
※この機能はwebpack5.0以降ではデフォルトになるようです。

・chunkFileName: ファイルをchunkで区切ったときのchunkのファイル名
・publicPath: bundleファイルのアップロード先
・devtoolModuleFilenameTemplate: sourceMapの名前の付け方
・jsonpFunction: 外部のデータを取得するのに使われるjsonpの設定
・globalObject: bundle後のコードでの即時関数の第一引数。Node.jsを使う場合デフォルトだとエラーになるのでthisにしないといけない。

Ln196-Ln273

optimization: {

以降はバンドル後のコードの最適化についての設定を表している。

minimize: isEnvProduction,

コードの最小化を行うかを判断するフラグ。trueなら後述のminimizerによりコードの最小化が行われる。

minimizer: [

以降最小化の方法を指定するminimizerに関する設定となる。
デフォルトではTenserPlugin, OptimizeCSSAssetsPluginの2つが用意されている。

// This is only used in production mode
new TerserPlugin({
  terserOptions: {
    parse: {
      // We want terser to parse ecma 8 code. However, we don't want it
      // to apply any minification steps that turns valid ecma 5 code
      // into invalid ecma 5 code. This is why the 'compress' and 'output'
      // sections only apply transformations that are ecma 5 safe
      // https://github.com/facebook/create-react-app/pull/4234
      ecma: 8,
    },
    compress: {
      ecma: 5,
      warnings: false,
      // Disabled because of an issue with Uglify breaking seemingly valid code:
      // https://github.com/facebook/create-react-app/issues/2376
      // Pending further investigation:
      // https://github.com/mishoo/UglifyJS2/issues/2011
      comparisons: false,
      // Disabled because of an issue with Terser breaking valid code:
      // https://github.com/facebook/create-react-app/issues/5250
      // Pending further investigation:
      // https://github.com/terser-js/terser/issues/120
      inline: 2,
    },
    mangle: {
      safari10: true,
    },
    // Added for profiling in devtools
    keep_classnames: isEnvProductionProfile,
    keep_fnames: isEnvProductionProfile,
    output: {
      ecma: 5,
      comments: false,
      // Turned on because emoji and regex is not minified properly using default
      // https://github.com/facebook/create-react-app/issues/2488
      ascii_only: true,
    },
  },
  sourceMap: shouldUseSourceMap,
}),

minimizerの1つ、TerserPluginについての設定。

parse: コードをどの型に変換するか。ECMAScript8に変換するように設定されている。
※ここでは8だが、他の場所で5に上書き設定される。

compress: 圧縮方法についての設定

  • ecma: 変換後の型。ここではECMAScript5になっている
  • warnings: 記述なし...おそらくwarningを表示するかを表す
  • comparisons: 論理系の表現を簡略化する
  • inline: 関数を1行に圧縮するかの設定。初期では変数宣言のない関数まで圧縮

mangle: 変数名を短くするなど

  • safari10: safari10/11のバグに対応させるかのフラグ

keep_classnames, keep_fnames: classnameなどをそのままにするか

output: 出力内容の設定

  • ecma: 変換後の型。ここではECMAScript5になっている
  • comments: コメントアウトされた部分を残すか
  • ascii_only: 対応する文字の範囲の指定

sourceMap: 圧縮時にsourceMap対応するか(Ln33参照)

// This is only used in production mode
new OptimizeCSSAssetsPlugin({
  cssProcessorOptions: {
    parser: safePostCssParser,
    map: shouldUseSourceMap
      ? {
          // `inline: false` forces the sourcemap to be output into a
          // separate file
          inline: false,
          // `annotation: true` appends the sourceMappingURL to the end of
          // the css file, helping the browser find the sourcemap
          annotation: true,
        }
      : false,
  },
  cssProcessorPluginOptions: {
    preset: ['default', { minifyFontValues: { removeQuotes: false } }],
  },
}),

minimizerのもう一つ、OptimizeCSSAssetsPluginについての設定。
cssProcessorとして使われているcssnanoにオプションを引き渡している。

cssProcessorOptions

  • parser: safePostCssParserによってCSSが壊れていても読み込むことができる
  • map
    • inline: sourceMapを同じファイルに作成するか
    • annotation: sourceMapのURLをCSSに入れるか

cssProcessorPluginOptions

  • minifyFontValues: 文字をサイズの小さい形に変換する
    removeQuotesがfalseなのでクオートは省略されない
// Automatically split vendor and commons
// https://twitter.com/wSokra/status/969633336732905474
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
splitChunks: {
  chunks: 'all',
  name: false,
},

各ファイルの呼び出し回数をもとに各chunkを適切に分解、統合する。
chunks: どのタイプのchunkを最適化対象にするか
name: chunkのファイル名(falseの場合変更なし)

// Keep the runtime chunk separated to enable long term caching
// https://twitter.com/wSokra/status/969679223278505985
// https://github.com/facebook/create-react-app/issues/5358
runtimeChunk: {
  name: entrypoint => `runtime-${entrypoint.name}`,
},

各エントリーポイントで動いているファイルのみでchunkを新しく作成する。
name: 新しく作られるchunkのファイル名。

終わりに

今回はwebpack.configの前半部分の内容についてどんなことをやっているかの簡単な説明をさせていただきました。残りの部分は1記事にまとまるかわかりませんが今月(2020/08)中には出したいなと考えております。

また、調べてもいまいち内容のつかめなかった部分もあったので詳しい方は教えていただけるとありがたいです。

参考ページ

この記事を書くにあたっていろいろなサイトを参考にさせていただきました。
量の問題で書ききれなかった部分も多いのでもしわかりにくい点があれば以下のページを参考にしていただけると幸いです。

全体
https://webpack.js.org/
http://js.studio-kingdom.com/webpack/api/configuration
https://qiita.com/soarflat/items/28bf799f7e0335b68186

strictモード
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Strict_mode

sourceMap, devtool
https://chuckwebtips.hatenablog.com/entry/2016/03/02/000000
https://t-hiroyoshi.github.io/webpack-devtool/
https://webpack.js.org/configuration/devtool/

InlineChunkHtmlPlugin
https://www.npmjs.com/package/html-webpack-inline-chunk-plugin

Eslintrc
https://github.com/facebook/create-react-app/issues/9047

env
https://maku77.github.io/nodejs/env/dotenv.html

MiniCssExtractPlugin
https://reffect.co.jp/html/webpack-4-mini-css-extract-plugin

css-loader, style-loader
https://ics.media/entry/17376/

postcss
https://qiita.com/okumurakengo/items/a10f6fa4b77b5b088cb9
https://unformedbuilding.com/articles/php-based-css-preprocessor-pcss-and-css-crush/
https://qiita.com/naru0504/items/86bc7c6cab22a679553e
https://techacademy.jp/magazine/19732

resolve-url-loader
https://e-joint.jp/907/

entry(webpackHotDevClient)
https://www.slideshare.net/ssuserc9c8d8/reactscriptswebpack-130687608

output.publicPath
https://www.it-swarm.dev/ja/javascript/webpack%E3%81%AE-publicpath%E3%81%AF%E4%BD%95%E3%82%92%E3%81%99%E3%82%8B%E3%81%AE%E3%81%A7%E3%81%99%E3%81%8B%EF%BC%9F/1050990542/

output.globalObject
https://qiita.com/riversun/items/1da0c0668d0dccdc0460

optimization
https://webpack.js.org/configuration/optimization/

ECMAScript
https://ja.wikipedia.org/wiki/ECMAScript

terserOptions
https://github.com/terser/terser
https://gist.github.com/shqld/d101cae50dd83ab7d3487cdb10b80f4d

cssProcessor
https://github.com/NMFR/optimize-css-assets-webpack-plugin
https://site-builder.wiki/posts/9654
https://cssnano.co/optimisations/minifyfontvalues

splitChunks
https://blog.hiroppy.me/entry/mechanism-of-webpack#SplitChunksPlugin-v4

runtimeChunk
https://webpack.js.org/configuration/optimization/#optimizationruntimechunk

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?