create-react-app 实现(下)- cra 中的 webpack 配置详解
上两篇中介绍了 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论