create-react-app 实现(下)- cra 中的 webpack 配置详解

发布于 2023-08-29 02:14:32 字数 20222 浏览 37 评论 0

上两篇中介绍了 create-react-app 中关于 create-react-app 包和 react-scripts 包中几个核心命令实现。最后还剩下一块重要内容在本篇中介绍 —— create-react-app 中的 webpack 配置。

说明:为了方便阅读,大部分介绍在 预览 部分对源码做了批注说明,如说明的内容过多则添加有 详见 标识,可在 详解 部分查看更详细的内容。

预览

// webpackEnv: 值为 'production'、'development'。此函数返回执行 `react-scripts start/build` 所需的 webpack 配置项。
module.exports = function (webpackEnv) {
  return {
    // 模式,不同模式下启用一系列不同的默认优化配置项。详见
    mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
    // 是否发现错误就立即抛出并退出编译,通常在 production 模式下开启。开发环境中使用 `HMR` 将在终端和浏览器控制台抛出错误
    bail: isEnvProduction, 
    // 选择 sourceMap 配置,详见
    devtool: isEnvProduction
      ? shouldUseSourceMap
        ? 'source-map' // sourceMap 信息最完整也是打包最慢的。
        : false
      : isEnvDevelopment && 'cheap-module-source-map', // 
    // 入口配置,详见
    entry:
    // 输出配置。webpack 如何输出结果的相关选项。
    output: {
      // 所有输出文件的目标路径,必须绝对路径(使用 Node.js 的 path 模块),paths.appBuild 指向 `build` 目录,webpack 默认是 'dist'
      path: isEnvProduction ? paths.appBuild : undefined,
      // 输出的 require() 中添加/* filename */ 注释,例如__webpack_require__(/*! ./a.js */ "./src/a.js")
      pathinfo: isEnvDevelopment,
      // 设置每个输出 bundle 的名称,开发环境下不会生成真正的文件。此处也不影响按需加载 chunk 和 loader 产生的输出文件。
      filename: isEnvProduction
        ? 'static/js/[name].[contenthash:8].js'
        : isEnvDevelopment && 'static/js/bundle.js',
      futureEmitAssets: true, // webpack 5 默认支持此特性,并移除此选项
      // 设置非入口(non-entry) chunk 文件的名称(按需加载 chunk 的输出文件)
      chunkFilename: isEnvProduction
        ? 'static/js/[name].[contenthash:8].chunk.js'
        : isEnvDevelopment && 'static/js/[name].chunk.js',
      // 按需加载资源或外部资源(如图片、文件等)的公开 url,尾部斜杠不能少。
      // publicUrlOrPath 则是按 process.env.PUBLIC_URL、'package.json#homepage' 、'/' 的顺序来推断。
      publicPath: paths.publicUrlOrPath,
      // 浏览器开发者工具 source map 文件名模板,这里将其指向原始磁盘位置(在 Windows 上格式为 URL)
      devtoolModuleFilenameTemplate: isEnvProduction
        ? info =>
            path
              .relative(paths.appSrc, info.absoluteResourcePath)
              .replace(/\\/g, '/')
        : isEnvDevelopment &&
          (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
      // jsonp 函数用于异步加载 chunk,在同一页面上使用多个 webpack runtime 时会造成 jsonp 命名冲突,因为默认都是 'webpackJsonp' 字符。这里拼接了 'package.json#name' 字段防止冲突。
      jsonpFunction: `webpackJsonp${appPackageJson.name}`,
      // 默认为 `window`,设置为 `this` 后让打包后的模块也可以在 web workers 中使用。
      globalObject: 'this',
    },
    // 优化
    optimization: {
      // 是否压缩 bundle,production 模式下默认为 true
      minimize: isEnvProduction,
      // 提供一个或多个自定义的 TerserPlugin 实例覆盖默认压缩工具
      minimizer: [
        // 仅用于 'production' 模式
        // terser-webpack-plugin 
        new TerserPlugin({
          //   https://github.com/terser/terser#minify-options  
          terserOptions: {
            // 解析 ecma8 代码
            parse: {
              // 我们希望 terser 解析 ecma 8 代码。
              // 但是不希望它应用任何将有效的 ecma 5 代码转换为无效的 ecma 5 代码的压缩步骤。
              // 所以 `compress` 和 `output` 中只应用 ecma5
              //   https://github.com/facebook/create-react-app/pull/4234  
              ecma: 8,
            },
            // 压缩
            compress: {
              // ecma 设置为 5 不会将 ES6+ 代码转换为 ES5。只是禁用 es6+ 优化
              ecma: 5,
              warnings: false,
              // 对二进制节点应用某些优化,比如 !(a <= b) → a > b。
              // Uglify 破坏了有效代码,故被禁用。  https://github.com/facebook/create-react-app/issues/2376  
              // 有待进一步调查:  https://github.com/mishoo/UglifyJS2/issues/2011  
              comparisons: false,
              // Terser 破坏了有效代码,故被禁用:
              //   https://github.com/facebook/create-react-app/issues/5250  
              // 有待进一步调查:   https://github.com/terser-js/terser/issues/120  
              inline: 2, // 内联调用函数,3:带有参数和变量的内联函数
            },
            // 混淆
            mangle: {
              // safari10 bug 修复,  https://bugs.webkit.org/show_bug.cgi?id=171041  
              safari10: true, 
            },
            // Added for profiling in devtools
            // isEnvProductionProfile = isEnvProduction && process.argv.includes('--profile')
            keep_classnames: isEnvProductionProfile, // 不破坏 class 名称
            keep_fnames: isEnvProductionProfile, // 不破坏函数名
            output: {
              // `ecma` 设置为 5 不会将 ES6+ 转换为 ES5,只是在美化器的控制下优化输出
              ecma: 5,
              // 是否保留注释,不保留
              comments: false,
              // 不启用的话,emoji and regex 无法正常压缩
              //   https://github.com/facebook/create-react-app/issues/2488  
              ascii_only: true,
            },
          },
          sourceMap: shouldUseSourceMap, // 开启 sourceMap
        }),
        // 仅用于 'production' 模式
        // 优化、压缩 css 插件,optimize-css-assets-webpack-plugin
        new OptimizeCSSAssetsPlugin({
          // cssProcessor: require('cssnano'), // css 压缩器,默认 cssnano
          // 传给 cssProcessor 的选项,这里即 cssnano
          cssProcessorOptions: {
            // 解析器
            // safePostCssParser:查找并修复 CSS 语法错误
            parser: safePostCssParser, // require('postcss-safe-parser');
            map: shouldUseSourceMap
              ? {
                  // 强制生成单独的 SourceMap 文件,不内联
                  inline: false,
                  // `annotation: true` 将 sourceMappingURL 附加到 css 文件 末尾,帮助浏览器找到 sourcemap
                  annotation: true,
                }
              : false,
          },
          // 传给 cssProcessor 插件的选项
          cssProcessorPluginOptions: {
            // 预设
            preset: ['default', { minifyFontValues: { removeQuotes: false } }], // 移除双引号
          },
        }),
      ],
      // 代码分割。默认会分割出一个 vendors(第三方) 和 commons (多入口共享的公共模块)
      // splitChunks 有一系列的默认配置,此处覆盖了两个默认配置,其他默认配置详见
      splitChunks: {
        // 选择哪些 chunk 进行优化(公共模块拆分)
        chunks: 'all', // = async(异步) + initial(同步),表示可以在异步和非异步 chunk 之间共享
        // 拆分 chunk 的名称,false 表示不修改名称
        name: isEnvDevelopment,
      },
      // 将 webpack runtime 代码分割出去,详见
      // webpack runtime 代码(编译后代码运行需要的代码)每次构建都会发生改变,默认注入到业务代码中导致文件名改变,导致浏览器缓存失效。
      runtimeChunk: {
        // 自定义 runtimeChunk 名称
        name: entrypoint => `runtime-${entrypoint.name}`,
      },
    },
    // 配置模块如何解析
    resolve: {
      // 告诉 webpack 解析模块时应该搜索的目录,优先匹配 'node_modules'
      //   https://github.com/facebook/create-react-app/issues/253  
      modules: ['node_modules', paths.appNodeModules].concat(
        modules.additionalModulePaths || []
      ),
      // 文件扩展名。引入模块时省略扩展名时(如 import File from '../path/to/file') 帮助 webpack 解析模块
      // paths.moduleFileExtensions 包含'web.mjs', 'mjs', 'web.js', 'js', 'web.ts', 'ts', 'web.tsx', 'tsx', 'json', 'web.jsx', 'jsx',
      extensions: paths.moduleFileExtensions
        .map(ext => `.${ext}`)
        // 未使用 TypeScript 时,过滤掉 ts 相关扩展名
        .filter(ext => useTypeScript || !ext.includes('ts')),
      // 创建 import 或 require 的别名,简化模块引入
      alias: {
        // 支持 React Native Web
        'react-native': 'react-native-web',
        // Allows for better profiling with ReactDevTools
        ...(isEnvProductionProfile && {
          'react-dom$': 'react-dom/profiling',
          'scheduler/tracing': 'scheduler/tracing-profiling',
        }),
        // 添加 `src: paths.appSrc`,
        ...(modules.webpackAliases || {}),
      },
      // 额外的解析插件列表
      plugins: [
        // 增加对 yarn pnp(Plug'n'Play)的支持。pnp 不再需要将依赖从缓存拷贝至 node_modules,而直接使用全局 module 缓存,减少大量文件 I/O,从而加快了模块安装速度。使用 `yarn --pnp` 开启 yarn pnp 功能。
        // `create-react-app` 中创建项目时增加 `--use-pnp` 选项开启 pnp,即 `create-react-app myapp --use-pnp`
        PnpWebpackPlugin, // require('pnp-webpack-plugin')
        // 阻止用户从 src/ 和 node_modules/ 之外的目录导入文件。因为之外的文件不被 babel 处理(见 babel-loader include 配置),除非你能保证引入的文件都是编译过的才可移除此插件。
        new ModuleScopePlugin(paths.appSrc, [
          paths.appPackageJson, // package.json 所在路径
          reactRefreshOverlayEntry, // require.resolve('react-dev-utils/refreshOverlayInterop');
        ]),
      ],
    },
    // 与 `resolve` 类似,但仅用于解析 webpack 的 loader 包(找到加载器在磁盘上的位置)
    resolveLoader: {
      plugins: [
        // 告诉 webpack 从当前包(PnpWebpackPlugin)加载它的加载器
        PnpWebpackPlugin.moduleLoader(module),
      ],
    },
    // 配置如何处理项目中的不同类型的模块
    module: {
      // 不存在的导入将报错而不是警告。如模块 animal 中没有 flower,但 `var { flower } = require('animal')`
      strictExportPresence: true,
      rules: [
        // 禁用 require.ensure,不是标准的语言特性,使用 import()
        { parser: { requireEnsure: false } },
        // 处理包含 source maps 的 node_modules 包。从源文件中提取 source maps(从 sourceMappingURL 中提取)
        // 如果 source maps 数据没有正确提取和处理,注入 bundle 后浏览器有可能无法正确解析这些数据。source-map-loader 允许 webpack 跨库处理 source map 数据,因而更易于调试。
        shouldUseSourceMap && {
          // loader 类型,详见 Rules.enforce
          enforce: 'pre', // 前置 loader
          // 排除所有符合条件的模块
          exclude: /@babel(?:\/|\\{1,2})runtime/,
          test: /\.(js|mjs|jsx|ts|tsx|css)$/,
          use: 'source-map-loader',
        },
        {
          // oneOf 将遍历所有后续加载器,直到满足要求为止。当没有加载器匹配时,它将回退到加载器列表末尾的 file-loader。
          oneOf: [
            // TODO: Merge this config once `image/avif` is in the mime-db.   https://github.com/jshttp/mime-db  
            {
              test: [/\.avif$/], // 一种图片格式
              loader: require.resolve('url-loader'),
              options: {
                // 字节大小限制(默认无限制,单位 b)。小于限制则转化为 Data URLs 内联在 javaScript 中以避免网络请求,否则单独生成文件。
                limit: imageInlineSizeLimit, // imageInlineSizeLimit = process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
                //  MIME 类型
                mimetype: 'image/avif',
                // chunk 文件名称
                name: 'static/media/[name].[hash:8].[ext]',
              },
            },
            // url-loader 与 file-loader 工作方式类似,不同之处在于它将小于指定字节限制的资源文件作为数据 URL 内联在 javaScript 中以避免网络请求。
            {
              test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], // 处理图片格式
              loader: require.resolve('url-loader'),
              options: {
                limit: imageInlineSizeLimit,
                name: 'static/media/[name].[hash:8].[ext]',
              },
            },
            // 使用 Babel 处理 src 目录 JS。
            // 预设包括 JSX、Flow、TypeScript 和一些 ESnext 特性。
            {
              test: /\.(js|mjs|jsx|ts|tsx)$/,
              include: paths.appSrc, // 仅处理 src 目录
              loader: require.resolve('babel-loader'),
              options: {
                // 自定义包装 babel-loader。
                // 在 `const myCustomLoader = require("babel-loader").custom(callback)` 中,`custom` 方法接受一个函数回调,允许用户为其处理的每个文件添加 Babel 配置的自定义处理。
                // 如不想创建新文件去调用 `custom` 方法,则可配置 `customize` 选项,值为导出 custom 回调的模块路径。
                customize: require.resolve(
                  // 详见 babel-preset-react-app
                  'babel-preset-react-app/webpack-overrides'
                ),
                // 一组预设。预设是一系列插件的组合
                presets: [
                  [
                    require.resolve('babel-preset-react-app'), // 详见 babel-preset-react-app 
                    // 指定 'babel-preset-react-app' 预设的参数,方式:["presetA", options]
                    {
                      // runtime:预设参数,表示 jsx 转换方式。详见 babel-preset-react-app 
                      // 'classic' 是旧的方式,会把 JSX 转换为 `React.createElement(...)` 调用,但存在一些缺陷。
                      // 'automatic' (react17)是一种新的转化方式,可解决 'classic' 方式的缺陷,使用 'react/jsx-runtime' 包进行转化。
                      runtime: hasJsxRuntime ? 'automatic' : 'classic', // hasJsxRuntime:检查 `react/jsx-runtime` 模块是否存在
                    },
                  ],
                ],
                // @remove-on-eject-begin ~ @remove-on-eject-end、@remove-file-on-eject 与 `react-scripts eject` 命令弹出配置项有关,这里不做详细解释
                // @remove-on-eject-begin
                babelrc: false, // 禁用 .babelrc
                configFile: false, // 禁用 configFile 文件
                // 缓存唯一标识符,标识符更改时强制缓存失效
                // 默认由 @babel/core 版本号 + babel-loader 版本号 + .babelrc 文件内容 + 环境变量(BABEL_ENV || NODE_ENV)组成,这里使用 react-scripts 和 babel-preset-react-app 版本号替代,`react-scripts eject` 会删除这部分内容。
                cacheIdentifier: getCacheIdentifier(
                  // getCacheIdentifier:require('react-dev-utils/getCacheIdentifier')
                  isEnvProduction
                    ? 'production'
                    : isEnvDevelopment && 'development',
                  [
                    'babel-plugin-named-asset-import',
                    'babel-preset-react-app',
                    'react-dev-utils',
                    'react-scripts',
                  ]
                ),
                // @remove-on-eject-end
                // 一组插件。插件分为语法插件(语法解析)和转换插件(代码转化)。
                plugins: [
                  [
                    // 非 js/css 模块可显式命名导入。目前 create-react-app 中仅适用于 svg 资源
                    // 具体可查看   https://github.com/facebook/create-react-app/issues/3722  
                    require.resolve('babel-plugin-named-asset-import'),
                    {
                      loaderMap: {
                        // svg 模块作为 React 组件导入:
                        // import { ReactComponent as Logo } from './logo.svg';
                        // const Header = () =>  <div><Logo /><div>;
                        svg: {
                          ReactComponent:
                            '@svgr/webpack?-svgo,+titleProp,+ref![path]',
                        },
                      },
                    },
                  ],
                  // 支持 react-refresh。一种模块热替换(HMR)方案,用于替代 react-hot-loader
                  isEnvDevelopment &&
                    shouldUseReactRefresh &&
                    require.resolve('react-refresh/babel'),
                ].filter(Boolean),
                // 这是 webpack 的 `babel-loader` 的一个特性(不是 Babel)。
                // 它在 ./node_modules/.cache/babel-loader/ 目录缓存结果,加快重新编译过程。
                cacheDirectory: true,
                // 使用 Gzip 进行缓存压缩。禁用原因   https://github.com/facebook/create-react-app/issues/6846  ,简言之对大多数项目并不会产生明显收益,除非需要转换上千个文件。
                cacheCompression: false, // 禁用
                // 紧凑模式,省略所有不必要的换行符和空格
                compact: isEnvProduction,
              },
            },
            // 使用 Babel 处理应用程序(src)之外的任何 JS。与应用 JS 不同,这里只编译标准的 ES 特性。
            {
              test: /\.(js|mjs)$/,
              exclude: /@babel(?:\/|\\{1,2})runtime/,
              loader: require.resolve('babel-loader'),
              options: {
                babelrc: false,
                configFile: false,
                compact: false,
                presets: [
                  [
                    //   https://github.com/facebook/create-react-app/blob/main/packages/babel-preset-react-app/dependencies.js  
                    require.resolve('babel-preset-react-app/dependencies'),
                    { helpers: true },
                  ],
                ],
                cacheDirectory: true,
                // 使用 Gzip 进行缓存压缩。禁用原因   https://github.com/facebook/create-react-app/issues/6846  
                cacheCompression: false,
                // @remove-on-eject-begin
                cacheIdentifier: getCacheIdentifier(
                  isEnvProduction
                    ? 'production'
                    : isEnvDevelopment && 'development',
                  [
                    'babel-plugin-named-asset-import',
                    'babel-preset-react-app',
                    'react-dev-utils',
                    'react-scripts',
                  ]
                ),
                // @remove-on-eject-end

                // 调试 node_modules 代码需要 Babel sourcemaps。如果没有下面的选项,像 VSCode 这样的调试器会显示不正确的代码并在错误的行上设置断点。
                // sourceMaps 为 true 表示为代码生成 sourceMaps。
                sourceMaps: shouldUseSourceMap,
                // inputSourceMap 为 true 表示如果文件包含 `//# sourceMappingURL=...` 注释,将尝试加载 source map,加载或解析失败将丢弃。
                inputSourceMap: shouldUseSourceMap,
              },
            },
            // postcss-loader 将 autoprefixer(添加浏览器兼容前缀) 应用到 css。
            // css-loader 解析 css 中的路径(@import、url())并将资产添加为依赖项。
            // style-loader 将 css 转换为注入 <style> 标签的 js 模块。
            // 生产环境使用 MiniCSSExtractPlugin 来提取 css 为独立的样式文件,但在开发环境下 css-loader 可以对 css 进行 HMR。
            // 默认情况下,我们支持扩展名为 .module.css 的 CSS Modules
            {
              test: cssRegex,
              // 排除扩展名为 .module.css 的 CSS Modules,交由处理
              exclude: cssModuleRegex,
              use: getStyleLoaders({
                // 表示配置在 `css-loader` 之前的 loader,有几个可以去处理 `@import` 资源(如 `@import 'a.css';`)。此配置中 1 表示 `@import` 进来的资源可以经过 `postcss-loader`
                importLoaders: 1, // 1 => postcss-loader
                // 生成 SourceMap
                sourceMap: isEnvProduction
                  ? shouldUseSourceMap
                  : isEnvDevelopment,
                // 启用 CSS Modules
                modules: {
                  // compileType:输入样式的编译级别。可选值 `module | icss`。
                  // - 'module': 表示解析所有 CSS Modules 特性。
                  // - 'icss': 表示只会编译低级别的可交互的 css 特性,即 ICSS,相比标准的 CSS 规范仅额外新增了两个伪类( :import 和 :export)用于变量的导入和导出。
                  // webpack v4 之前,css-loader 默认将 ICSS 特性应用于所有文件。
                  compileType: 'icss',
                },
              }),
              // Don't consider CSS imports dead code even if the
              // containing package claims to have no side effects.
              // Remove this when webpack adds a warning or an error for this.
              // See   https://github.com/webpack/webpack/issues/6571  
              // 声明模块具有副作用,避免 css 被 webpack tree shaking 去除而没进入生产包。
              sideEffects: true, // 业务代码在 loader 处声明
            },
            // 支持 CSS Modules (  https://github.com/css-modules/css-modules)  
            // 支持 .module.css 扩展名
            {
              test: cssModuleRegex,
              use: getStyleLoaders({
                importLoaders: 1,
                sourceMap: isEnvProduction
                  ? shouldUseSourceMap
                  : isEnvDevelopment,
                modules: {
                  compileType: 'module',
                  // 默认情况下 CSS Modules 使用内置函数来生成 classname,也可以指定自定义 getLocalIdent 函数的绝对路径。
                  // require('react-dev-utils/getCSSModuleLocalIdent')
                  getLocalIdent: getCSSModuleLocalIdent,
                },
              }),
            },
            // 支持 sass(使用 .scss 或 .sass 扩展名)。
            // 默认支持的 sass Modules 扩展名: .module.scss 或 .module.sass
            {
              test: sassRegex,
              exclude: sassModuleRegex,
              use: getStyleLoaders(
                {
                  // resolve-url-loader 的作用: 帮助 sass-loader 找到对应的 url 资源。
                  // saas 没有提供 url 重写的功能,所以相关的资源都必须是相对于输出文件(ouput),此 loader 设置于 sass-loader 之前就可以重写 url。
                  // 详见
              

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

把时间冻结

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

新人笑

文章 0 评论 0

mb_vYjKhcd3

文章 0 评论 0

小高

文章 0 评论 0

来日方长

文章 0 评论 0

哄哄

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文