2024 年,如何不使用 create-react-app 从头为 React 项目配置 webpack

2024 年,如何不使用 create-react-app 从头为 React 项目配置 webpack

技术向约 4.8 千字

对于全新的 React 项目来说,一开始就使用 Next.jsRemixShopify HydrogenGatsby 等 React 元框架(Meta Framework)是最正确的选择,这些元框架替你解决了路由、数据加载、服务端渲染(SSR)、全静态页面导出(SSG)、边缘计算、打包器和编译器配置。然而,如果你需要迁移现有的、纯客户端渲染(Client-side Rendering)的项目到 React、或者从其他打包器(如 Vite 或 Parcel)迁移走时,即使在 2024 年,webpack 仍然不失为一种选择。

由于 Rspack 仍然缺失或不兼容各种 webpack 的 API(例如 acorn parser hooks、在 loader context 上添加自定义函数与 webpack plugin 实例直接沟通、直接修改 compilation 对象修改 code split 时模块的去向、配置文件中 cacheGroup 上 test 的参数的大量字段,等等),Rspack 不兼容市面上将近一半的 webpack 插件,修改这些 webpack 插件使其兼容 Rspack 也非常困难;以及在使用一些特定代码分割配置和 dynamic import 以实现减少首屏无效代码的效果上,Rspack 也远远不如 webpack。在可以预见的未来里,都不推荐迁移现有项目使用 Rspack。

在开始之前,首先在项目里安装 webpack、CLI、Dev Server 和类型声明:

pnpm i -D webpack webpack-cli webpack-dev-server @types/webpack

在 package.json 的 scripts 中添加

{
  "scrips": {
    "dev": "NODE_ENV=development webpack serve",
    "build": "NODE_ENV=production webpack",
    "build:analyze": "ANALYZE=true NODE_ENV=production webpack",
  }
}

创建 webpack 配置 webpack.config.js,使用 JSDoc 标注类型、启用自动补全和类型校验。

module.exports = /** @type {import('webpack').Configuration} */ ({
  // webpack config goes here
});

mode

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

webpack 内置了针对不同模式下的优化策略(例如代码分割时的 chunkname、DefinePlugin 内联配置、启用压缩和摇树等),可选值有 nonedevelopmentproduction

我们在 package.jsonscripts.devscripts.build 分别指定了不同的环境变量,我们可以在 webpack.config.js 中根据这个环境变量设置 mode

// webpack.config.js
const process = require('node:process');
const isDevelopment = process.env.NODE_ENV !== 'production';

module.exports = /** @type {import('webpack').Configuration} */ ({
  mode: isDevelopment ? 'development' : 'production'
});

entry

https://webpack.js.org/concepts/entry-points/

项目的入口文件。

output

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

output 控制 webpack 在什么位置和如何输出 dist(包括 bundle、assets 等)。

library

https://webpack.js.org/configuration/output/#outputlibrary

当使用 webpack 构建库而不是应用时,output.library 用来控制你的库的导出,虽然构建 React 应用时不需要考虑导出,但是 output.library 还有别的用途。

webpack 的 Runtime 会在全局对象上挂载和存储一些变量和方法(例如 Chunk Map self.webpackChunk)。为了避免页面上同时存在多个 webpack 构建的产物导致全局变量冲突,你可以通过 output.library 设置后缀:

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  output: {
    library: '_SKK',
  }
});

output.library 设为 _SKK 时,webpack 在全局对象上挂载的变量名就会使用 _SKK 作为后缀(例如 self.webpackChunk_SKK )。

filename

https://webpack.js.org/configuration/output/#outputfilename

控制 webpack 输出 JavaScript 和 CSS 文件的文件名格式(其中 cssFilename 控制的是 webpack 5 中仍处于试验性的 CSS 支持,而 MiniCssExtractPlugin 暂时不会读取这个配置项)。一般我习惯用 [name].[contenthash].js 或者 [contenthash].js、不暴露任何源码的路径、源码中依赖与模块的名称。

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  output: {
    filename: isDevelopment ? '[name].js' : '[contenthash].js',
    cssFilename: isDevelopment ? '[name].css' : '[contenthash].css',
    hotUpdateChunkFilename: '[id].[fullhash].hot-update.js',
    hotUpdateMainFilename: '[fullhash].[runtime].hot-update.json',
    webassemblyModuleFilename: '[contenthash].wasm'
  }
});

path

https://webpack.js.org/configuration/output/#outputpath

指定 webpack 的输出目录,必须是绝对路径。建议使用 __dirnamenode:path 拼接路径:

// webpack.config.js
const path = require('node:path');

module.exports = /** @type {import('webpack').Configuration} */ ({
  output: {
    path: path.resolve(__dirname, 'dist')
  }
});

asyncChunks

https://webpack.js.org/configuration/output/#outputasyncchunks

使 webpack 创建能按需加载的异步模块。

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  output: {
    asyncChunks: true
  }
});

crossOriginLoading

https://webpack.js.org/configuration/output/#outputcrossoriginloading

当 webpack 打包 Web 应用时会使用 JSONP 加载 Chunk,你可以使用 output.crossOriginLoading 控制 <<script /> 标签的 crossOrigin 属性,在为你的 Web App 启用基于 workbox 的 ServiceWorker + CacheStorage 离线缓存方案时,crossOrigin 属性是必须的。

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  output: {
    crossOriginLoading: 'anonymous'
  }
});

hashFunction

https://webpack.js.org/configuration/output/#outputhashfunction

选择 webpack 使用的哈希函数,,默认为 md4。这个配置将会传给 Node.js 的 crypto#createHash 方式。由于 Node.js 17 升级了 OpenSSL 大版本,默认的 md4 会 break 掉 Node.js 17+。而 webpack 5.54.0 新增了性能更好的 xxhash64 哈希函数的支持(并且将会成为 webpack 6 的默认哈希函数),这里推荐显式声明 output.hashFunctionxxhash64

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  output: {
    hashFunction: 'xxhash64'
  }
});

hashDigestLength

https://webpack.js.org/configuration/output/#outputhashdigestlength

控制哈希前缀长度,webpack 5 默认值取 20、而 webpack 6 的默认值将会改为 16。这里推荐显式声明 output.hashDigestLength 为 16:

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  output: {
    hashDigestLength: 16
  }
});

devtool

控制 webpack 内置的 SourceMapDevToolPluginEvalSourceMapDevToolPlugin 的行为。在开发中推荐使用 eval-source-map(source map 为原始源码),如果项目非常大(20k+ LOC)可以使用 eval-cheap-module-source-map、尽可能保留原始信息的同时改善性能;在生产中推荐完全关闭 source map 防止源码泄漏:

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  devtool: isDevelopment ? 'eval-source-map' /** eval-cheap-module-source-map */ : false
});

devServer

port

https://webpack.js.org/configuration/dev-server/#devserverport

webpack-dev-server 启动时监听的端口,推荐设置一个固定值而不是自动选择可用端口:

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  devServer: {
    port: 3000
  }
});

historyApiFallback

https://webpack.js.org/configuration/dev-server/#devserverhistoryapifallback

在开发 SPA(单页应用)时,你的源码中可能只有一个 index.html。你可以启用 devServer.historyApiFallback 避免 404:

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  devServer: {
    historyApiFallback: true
  }
});

external

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

将源码中导入的一些模块 map 到全局变量或者其他模块上。通过 external 还可以将一些 isomorphic 模块、polyfill(例如 isomorphic-fetchwhatwg-url)替换成浏览器原生 API:

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  externals: {
    'text-encoding': 'TextEncoder',
    'whatwg-url': 'window',
    '@trust/webcrypto': 'crypto',
    'isomorphic-fetch': 'fetch',
    'node-fetch': 'fetch',
    // Add this to bundle @undecaf/zbar.wasm
    module: 'module'
  }
});

module - rules

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

虽然 webpack 支持打包任意文件格式的模块,但是 webpack 只内置了基于 acorn 的 JavaScript parser,因此任何非 JavaScript 的模块(webpack 5 新增了试验性的 CSS 支持、将在 webpack 6 起默认启用)都需要使用 webpack loader 加以处理、转换为 webpack 可以识别的 JavaScript。

CSS

推荐使用 lightningcss-loader 代替 postcss-loaderautoprefixerpostcss-preset-env。当然如果有使用其他 PostCSS 插件的必要,也可以 postcss-loaderlightningcss-loader 一起使用。

webpack 5 默认不支持 CSS,可以使用 webpack 内置的试验性的 CSS 支持,或者使用目前常用的 css-loadermini-css-extract-plugin 的组合。

pnpm i lightningcss lightningcss-loader -D
# 如果需要使用 PostCSS 插件
pnpm i postcss postcss-loader -D
# 如果不使用 webpack 试验性的 CSS 支持
pnpm i css-loader mini-css-extract-plugin -D
// webpack.config.js
const LightningCSS = require('lightningcss');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = /** @type {import('webpack').Configuration} */ ({
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 不启用 webpack 内置的试验性 CSS 支持时,需要使用 css-loader 和 MiniCssExtractPlugin.loader
          MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'lightningcss-loader',
            options: {
              implementation: LightningCSS
            }
          },
          // 只有需要用 PostCSS 插件时,才添加下面的 postcss-loader
          {
            loader: 'postcss-loader'
            // postcss-loader 可以自动寻找 PostCSS 的配置文件,也可以在 options 中手动指定
          }
        ]
      },
    ]
  }
});

二进制文件

不需要编译转换的二进制文件(图片、字体等)可以标记为 type: asset/resource,webpack 会自动处理。推荐将这些文件放在单独一个目录(如 assets)下方便匹配。

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  module: {
    rules: [
      {
        test: /assets\//,
        type: 'asset/resource',
        generator: {
          filename: 'assets/[hash][ext][query]'
        }
      },
    ]
  }
});

TypeScript、TSX、JSX

推荐使用 swc-loader 或者 @swc-node/loader 代替 babel-loader。其中 swc-loader 支持 SWC 的配置,适合单独配置 SWC 编译行为;@swc-node/loader 支持自动寻找和读取 tsconfig.json 文件、并将其中的 compilerOptions 转换成 SWC 的格式配置传给 SWC。

我自己偏向于 TypeScript 只用于类型检查、在 tsconfig.json 中设置了 module: preservejsx: preservemoduleResolution: bundler,因此使用 swc-loader、并搭配 browserslist 的 JS API 将本地的 browserslist 配置文件传递给 SWC:

pnpm i swc-loader @swc/core @swc/helpers core-js browserslist -D
// webpack.config.js

/**
 * @param {string} [dir]
 */
const getSupportedBrowsers = (dir = __dirname) => {
  try {
    return browserslist.loadConfig({
      path: dir,
      env: isDevelopment ? 'development' : 'production'
    });
  } catch { }
};

/**
 * @param {boolean} useTypeScript
 */
const getSwcOptions = (useTypeScript) => {
  const supportedBrowsers = getSupportedBrowsers();

  return /** @type {import('@swc/core').Options} */ ({
    jsc: {
      parser: useTypeScript
        ? {
          syntax: 'typescript',
          tsx: true
        }
        : {
          syntax: 'ecmascript',
          jsx: true,
          importAttributes: true
        },
      externalHelpers: true,
      loose: false,
      transform: {
        react: {
          runtime: 'automatic',
          refresh: isDevelopment,
          development: isDevelopment
        },
        optimizer: {
          simplify: true,
          globals: {
            typeofs: {
              window: 'object'
            },
            envs: {
              NODE_ENV: isDevelopment ? '"development"' : '"production"'
            }
          }
        }
      },
    },
    env: {
      // swc-loader don't read browserslist config file, manually specify targets
      targets: supportedBrowsers?.length > 0 ? supportedBrowsers : 'defaults, chrome > 70, edge >= 79, firefox esr, safari >= 11, not dead, not ie > 0, not ie_mob > 0, not OperaMini all',
      mode: 'usage',
      loose: false,
      coreJs: require('core-js/package.json').version,
      shippedProposals: false
    }
  });
};

module.exports = /** @type {import('webpack').Configuration} */ ({
  module: {
    rules: [
      {
        test: /\.[cm]?tsx?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'swc-loader',
            options: getSwcOptions(true)
          }
        ]
      },
      {
        test: /\.[cm]?t=jsx?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'swc-loader',
            options: getSwcOptions(false)
          }
        ]
      }
    ]
  }
});

不论是 SWC 还是 Babel、只简单地指定 coreJs: 3 时编译器只会使用 core-js@3.0.0 提供的特性和 polyfill、无视后续所有新版本,所以在指定 SWC 的 core-js 版本时,一定要指定完整的 semver 版本号,或者直接 require('core-js/package.json').version

React Compiler

截至本文写就,React 官方暂时没有提供 React Compiler 的 webpack 插件/loader,我自己实现了 react-compiler-webpack 项目可以使用:

pnpm add -D react-compiler-webpack
// webpack.config.js
const { defineReactCompilerLoaderOption, reactCompilerLoader } = require('react-compiler-webpack');

module.exports = /** @type {import('webpack').Configuration} */ ({
  module: {
    rules: [
      {
        test: /\.[cm]?tsx?$/,
        exclude: /node_modules/,
        use: [
          // swc-loader ...
          {
            loader: reactCompilerLoader,
            options: defineReactCompilerLoaderOption({
              // React Compiler options goes here
            })
          }
        ]
      },
      {
        test: /\.[cm]?t=jsx?$/,
        exclude: /node_modules/,
        use: [
          // swc-loader ...
          {
            loader: reactCompilerLoader,
            options: defineReactCompilerLoaderOption({
              // React Compiler options goes here
            })
          }
        ]
      }
    ]
  }
});

plugins

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

使用 webpack 插件扩展 webpack 的行为。由于 webpack 提供给插件的 API 非常丰富、webpack 插件可以控制所有 webpack 的行为。

MiniCssExtractPlugin

pnpm add -D mini-css-extract-plugin

如果没有启用 webpack 内置的试验性 CSS 支持,那么就需要使用 MiniCssExtractPlugin 去收集 CSS 并输出 CSS Chunk。注意 MiniCssExtractPlugin 暂时不支持读取output.cssFilename、需要单独配置:

// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = /** @type {import('webpack').Configuration} */ ({
  plugins: [
    new MiniCssExtractPlugin({
      filename: isDevelopment ? '[name].css' : '[contenthash].css'
    })
  ]
});

CleanWebpackPlugin

pnpm i clean-webpack-plugin -D

删除 output.path 目录中的所有文件、以及 webpack 重新构建后不再需要的过时文件。

// webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = /** @type {import('webpack').Configuration} */ ({
  plugins: [
    !isDevelopment && new CleanWebpackPlugin(),
  ]
});

ReactRefreshWebpackPlugin

pnpm add -D @pmmmwh/react-refresh-webpack-plugin react-refresh

使用 React Fast Refresh。注意我们不再需要 react-fresh/babel Babel 插件和 babel-loader,因为 SWC 已经内置了 React Fresh 转译支持。

// webpack.config.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = /** @type {import('webpack').Configuration} */ ({
  plugins: [
    isDevelopment && new ReactRefreshWebpackPlugin(),
  ]
});

DefinePlugin

启用 webpack 内置的 DefinePlugin 进行一些编译器替换。除了实现编译条件以外,也可以搭配 dotenv 一起使用、内联公开的环境变量:

// webpack.config.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = /** @type {import('webpack').Configuration} */ ({
  plugins: [
    new DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      'import.meta.env.DEV': isDevelopment.toString(),
      'import.meta.env.PROD': (!isDevelopment).toString(),
      'typeof window': JSON.stringify('object'),
      // 将所有以 PUBLIC_ 开头的环境变量内联到应用中
      ...Object.entries(process.env).reduce((acc, [key, value]) => {
        if (key.startsWith('PUBLIC_')) {
          acc[`process.env.${key}`] = JSON.stringify(value);
        }
        return acc;
      }, /** @type {Record<string, string>} */({}))
    })
  ]
});

HtmlWebpackPlugin

pnpm i -D html-webpack-plugin

在提供的 HTML 模板中自动添加 <script /> 标签引入 webpack 输出的 JS 模块。

webpack 内置的 CSS 支持与 MiniCssExtractPlugin 只能自动加载非 Initial Chunk 中 import 的 CSS。对于 Initial Chunk 中 import 的 CSS(例如全局样式), HtmlWebpackPlugin 也能自动在 HTML 中添加 <link rel="stylesheet" /> 标签。

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = /** @type {import('webpack').Configuration} */ ({
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
  ]
});

BundleAnalyzerPlugin

pnpm i webpack-bundle-analyzer -D

通过 webpack-bundle-analyzer 直观地分析各个 Chunk 的组成与体积,从而针对性的修改 code split 配置、或使用 dynamic import 实现按需加载。

// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

const isAnalyze = !!process.env.ANALYZE;

module.exports = /** @type {import('webpack').Configuration} */ ({
  plugins: [
    isAnalyze && new BundleAnalyzerPlugin({
      analyzerMode: 'static'
    })
  ]
});

WebpackBar

pnpm i webpackbar -D

将 webpack 晦涩难懂的编译日志输出 变成直观的进度条,推荐在构建时开启:

// webpack.config.js
const WebpackBarPlugin = require('webpackbar');

module.exports = /** @type {import('webpack').Configuration} */ ({
  plugins: [
    !isDevelopment && new WebpackBarPlugin()
  ]
});

resolve

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

webpack 自己实现了一套模块路径解析(即 enhanced-resolve),通过 resolve 配置去修改或者扩展解析的行为。

extensions

webpack 默认只会寻找 .js.json.wasm 为扩展名的文件,因此为了打包 TypeScript 或者 JSX/TSX,需要手动指定支持的扩展名和顺序:

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  resolve: {
    extensions: ['.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.js', '.json'],
  }
});

alias

顾名思义,让 webpack 在解析模块路径时,将匹配到的模块 map 到指定的路径上。在解决一些编译问题时 alias 选项也非常有用:

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  resolve: {
    alias: {
      // '@undecaf/zbar-wasm': path.join(path.dirname(require.resolve('@undecaf/zbar-wasm/package.json')), 'dist/index.mjs')
    },
  }
});

cache / unsafeCache

是否让 webpack 缓存模块路径解析的结果以供后续使用,推荐启用 cache 并禁用 unsafeCache(激进缓存):

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  resolve: {
    cache: true,
    unsafeCache: false
  }
});

extensionAlias

在源码中 import 的模块带有文件扩展名时,让 webpack 在源码中去寻找其他后缀名的模块。这个选项在编译 TypeScript 到 ES Modules 非常有用。由于 ES Modules 规范和 TypeScript 推荐,所有的 import 的路径都需要/强烈建议携带 .js 的扩展名,但文件系统上的源码自然有可能是以 .jsx 或者 .ts(x) 的,通过配置 extensionAlias,可以在源码中手写 import 'foo.js' 时让 webpack 寻找文件系统上 foo.ts 的源码文件。

而当编译目标是 Web 应用、CommonJS 模块 等其他绝大部分场景时,extensionAlias 配置就毫无用处了。

conditionNames

Node.js 12.16.0 起支持在 package.json 中的 exports 字段声明条件导出。如果想要使条件的导出在 webpack 中也生效,则需要配置 conditionNames

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  resolve: {
    conditionNames: ['import', 'require', 'default']
  }
});

plugins

引入插件来修改 webpack 解析模块路径的行为。其中最常用的有 tsconfig-paths-webpack-plugin

pnpm i -D tsconfig-paths-webpack-plugin
// webpack.config.js
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');

module.exports = /** @type {import('webpack').Configuration} */ ({
  resolve: {
    plugins: [
      new TsconfigPathsPlugin({
        // tsconfig-paths-webpack-plugin can't access `resolve.extensions`
        // have to provide again
        extensions: ['.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.js', '.json']
      })
    ]
  }
});

optimization

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

modeproduction 时,webpack 会启用一系列优化、去改善生产环境产物的性能。而 optimizations 配置就可以去修改这些行为。

splitChunks

https://webpack.js.org/plugins/split-chunks-plugin/

webpack 的 splitChunks 的默认代码分割配置已经足够满足绝大部分应用的需要了,但是我们可以在这基础上添加一些代码分割规则(optmization.splitChunks.cacheGroups)或者修改一些参数,以便一些模块更好的动作,以及更好的和其他基建(例如 CDN)配合起来。

例如,Facebook 的 AoT CSS-in-JS 方案依赖生成的 CSS 文件只能有一个,我们可以将所有 StyleX 生成的 CSS 合并进同一个 _stylex_generated Chunk 之中:

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  optimization: {
    splitChunks: {
      cacheGroups: {
        stylex: {
          name: '_stylex_generated',
          test: /stylex\.virtual\.css/,
          type: 'css/mini-extract',
          chunks: 'all',
          enforce: true
        }
      }
    }
  }
});

再例如,虽然应用不断迭代更新、但是 React 版本并不经常改变,可以单独将 reactreact-dom 单独打进一个 framework chunk、这个 chunk 的 contenthash 几乎不会改变、可以大幅提升浏览器和 CDN 的缓存命中率:

// webpack.config.js
const topLevelFrameworkPaths = isDevelopment ? [] : getTopLevelFrameworkPaths();

module.exports = /** @type {import('webpack').Configuration} */ ({
  optimization: {
    splitChunks: {
      cacheGroups: {
        framework: {
          chunks: 'all',
          name: 'framework',
          test(module) {
            const resource = module.nameForCondition?.();
            return resource
              ? topLevelFrameworkPaths.some((pkgPath) => resource.startsWith(pkgPath))
              : false;
          },
          priority: 40,
          enforce: true
        }
      }
    }
  }
});

function getTopLevelFrameworkPaths(frameworkPackages = ['react', 'react-dom'], dir = path.resolve(__dirname)) {
  // Only top-level packages are included, e.g. nested copies like
  // 'node_modules/meow/node_modules/react' are not included.
  const topLevelFrameworkPaths = [];
  const visitedFrameworkPackages = new Set();

  // Adds package-paths of dependencies recursively
  const addPackagePath = (packageName, relativeToPath) => {
    try {
      if (visitedFrameworkPackages.has(packageName)) return;
      visitedFrameworkPackages.add(packageName);

      const packageJsonPath = require.resolve(`${packageName}/package.json`, {
        paths: [relativeToPath]
      });

      // Include a trailing slash so that a `.startsWith(packagePath)` check avoids false positives
      // when one package name starts with the full name of a different package.
      // For example:
      //   "node_modules/react-slider".startsWith("node_modules/react")  // true
      //   "node_modules/react-slider".startsWith("node_modules/react/") // false
      const directory = path.join(packageJsonPath, '../');

      // Returning from the function in case the directory has already been added and traversed
      if (topLevelFrameworkPaths.includes(directory)) return;
      topLevelFrameworkPaths.push(directory);

      const dependencies = require(packageJsonPath).dependencies || {};
      for (const name of Object.keys(dependencies)) {
        addPackagePath(name, directory);
      }
    } catch {
      // don't error on failing to resolve framework packages
    }
  };

  for (const packageName of frameworkPackages) {
    addPackagePath(packageName, dir);
  }

  return topLevelFrameworkPaths;
}

runtimeChunk

webpack 包含自己的 runtime 代码、用于加载和缓存模块等。通过 optimization.runtimeChunk 选项可以配置是否将 webpack 的 runtime 拆成独立的 chunk:

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  optimization: {
    runtimeChunk: {
      name: 'webpack'
    }
  }
});

minimizer

替换 webpack 内置的压缩器为你的自定义的压缩器。可以将 TerserPlugin 中的压缩器替换为 SWC、以及用 LightningCssMinifyPlugin 代替 CssMinimizerPlugin 压缩 CSS:

pnpm i -D terser-webpack-plugin lightningcss-loader
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
const { LightningCssMinifyPlugin } = require('lightningcss-loader');

module.exports = /** @type {import('webpack').Configuration} */ ({
  optimization: {
    minimizer: [
      new TerserPlugin({
        minify: TerserPlugin.swcMinify,
        terserOptions: {
          compress: {
            ecma: 5,
            comparisons: false,
            inline: 2 // https://github.com/vercel/next.js/issues/7178#issuecomment-493048965
          },
          mangle: { safari10: true },
          format: {
            // use ecma 2015 to enable minify like shorthand object
            ecma: 2015,
            safari10: true,
            comments: false,
            // Fixes usage of Emoji and certain Regex
            ascii_only: true
          }
        }
      }),
      new LightningCssMinifyPlugin()
    ]
  }
});

cache

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

启用 webpack 内置的缓存来加速后续构建。当 modedevelopment 时 webpack 会自动启用内存缓存,可以通过 cache 配置指定使用文件系统缓存:

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  cache: {
    type: 'filesystem',
    maxMemoryGenerations: isDevelopment ? 5 : Infinity,
    cacheDirectory: path.join(__dirname, 'node_modules', '.cache', 'webpack'),
    compression: isDevelopment ? 'gzip' : false,
  }
});

注意 compression 设置为 compression: isDevelopment ? 'gzip' : false,在开发环境下主动压缩、避免 IO 瓶颈;在 CI 构建环境中、由 CI runner 去压缩。

experiments

webpack 中包含了一些试验性特性,其中的一些功能会在 webpack 6 中正式推出,其中包括内置 CSS 支持、缓存机制优化、ESM 输出支持等:

// webpack.config.js
module.exports = /** @type {import('webpack').Configuration} */ ({
  experiments: {
    css: false, // 不能和 css-loader 和 mini-css-extract-plugin 一起启用
    cacheUnaffected: true
  }
});

以上便是一个可以用于构建 React 应用的 webpack 配置,你可以从 SukkaW/monsterbation-linter - PR #2 中看到一个完整的从 Parcel 迁移到 webpack 的例子。当然,在不同场景下需要添加一些额外的 webpack 配置,例如使用 CopyWebpackPlugin 在构建时拷贝一些不能使用 new URL() 方式加载的二进制文件(常见的有 Tensorflow.js 的模型文件),或者根据所使用的 CSS-in-JS 方案添加额外的 loader 和 webpack 插件(如 stylex-webpackstyle9-webpack)。

2024 年,如何不使用 create-react-app 从头为 React 项目配置 webpack
本文作者
Sukka
发布于
2024-09-19
更新于
2024-10-13
许可协议
转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!
如果你喜欢我的文章,或者我的文章有帮到你,可以考虑一下打赏作者
评论加载中...