在 create-react-app
(CRA)工具生成的模板工程中react-scripts
承担着工程脚本的基本功能,例如运行、打包、测试等,接下来我们基于 react-scripts
(v4.0.3)的源码,分析其实现逻辑,并且尝试在eject之后对其再次封装。
1. 概述 react-scripts
react-scripts
是 create-react-app
的核心依赖包,负责封装和管理 React 项目的构建工具链(如 Webpack、Babel、ESLint、Jest 等)的配置和脚本。它旨在为开发者提供"零配置"的开发体验,隐藏复杂的底层配置细节,让开发者专注于业务代码开发。
在 CRA 生成的模板工程中,package.json
中的 scripts
字段通常包含以下命令:
json
{
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
}
这些命令分别对应开发环境启动、生产打包、测试运行和配置暴露功能,背后都由 react-scripts
的实现驱动。
源码入口位于 react-scripts/bin/react-scripts.js
,这是一个 CLI 脚本,根据传入的命令(如 start
、build
、test
)调用对应的功能模块。我们将逐一分析这些场景的实现逻辑。
2. 启动开发环境(react-scripts start
)
功能概述
运行 npm start
或 react-scripts start
会启动一个开发服务器,提供热更新(Hot Module Replacement, HMR)、错误报告和浏览器自动刷新等功能。这是开发者最常用的命令,用于本地开发和调试。
实现逻辑
- 入口 :
react-scripts/bin/react-scripts.js
检查命令参数为start
,然后调用react-scripts/scripts/start.js
。
js
# start.js 大概逻辑
....
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const configFactory = require('../config/webpack.config');
const createDevServerConfig = require('../config/webpackDevServer.config');
const getClientEnvironment = require('../config/env');
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
.then(port => {
const config = configFactory('development');
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const appName = require(paths.appPackageJson).name;
const useTypeScript = fs.existsSync(paths.appTsConfig);
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
const urls = prepareUrls(
protocol,
HOST,
port,
paths.publicUrlOrPath.slice(0, -1)
);
const devSocket = {
warnings: warnings =>
devServer.sockWrite(devServer.sockets, 'warnings', warnings),
errors: errors =>
devServer.sockWrite(devServer.sockets, 'errors', errors),
};
// Create a webpack compiler that is configured with custom messages.
const compiler = createCompiler({
appName,
config,
devSocket,
urls,
useYarn,
useTypeScript,
tscCompileOnError,
webpack,
});
// Load proxy config
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(
proxySetting,
paths.appPublic,
paths.publicUrlOrPath
);
// Serve webpack assets generated by the compiler over a web server.
const serverConfig = createDevServerConfig(
proxyConfig,
urls.lanUrlForConfig
);
const devServer = new WebpackDevServer(compiler, serverConfig);
// Launch WebpackDevServer.
devServer.listen(port, HOST, err => {
if (err) {
return console.log(err);
}
openBrowser(urls.localUrlForBrowser);
});
})
.......
- 核心步骤 :
-
环境变量设置 :
- 设置
NODE_ENV=development
,确保开发模式下的配置生效。 - 读取
.env
文件(通过dotenv
和dotenv-expand
),调用react-scripts/config/env.js 初始化process.env环境变量,加载自定义环境变量(如REACT_APP_*
)。
js# env.js function getClientEnvironment(publicUrl) { const raw = Object.keys(process.env) .filter(key => REACT_APP.test(key)) .reduce( (env, key) => { env[key] = process.env[key]; return env; }, { NODE_ENV: process.env.NODE_ENV || 'development', ... ... } ); const stringified = { 'process.env': Object.keys(raw).reduce((env, key) => { env[key] = JSON.stringify(raw[key]); return env; }, {}), }; return { raw, stringified }; }
- 检查
BROWSER
环境变量,决定是否打开浏览器以及使用哪个浏览器。
- 设置
-
Webpack 配置 :
- 调用
react-scripts/config/webpack.config.js
,加载 Webpack 通用配置。调用react-scripts/config/webpackDevServer.config.js
, 加载 DevServer 配置 - 配置特点:
- 使用
webpack-dev-server
提供开发服务器。 - 启用 HMR(通过
HotModuleReplacementPlugin
)。 - 使用
babel-loader
编译 JSX 和现代 JavaScript。 - 设置
devtool: 'cheap-module-source-map'
,提供快速且轻量的源码映射。 - 配置 ESLint 和 Stylelint(通过
eslint-webpack-plugin
和stylelint-webpack-plugin
)进行代码检查。
- 使用
- 调用
-
开发服务器启动 :
- 使用
webpack-dev-server
的start
方法启动服务器,默认监听localhost:3000
(端口可通过PORT
环境变量自定义)。 - 支持 HTTPS(通过
HTTPS=true
启用)。 - 配置代理(通过
proxy
字段在package.json
中定义)。
- 使用
-
源码细节
-
Webpack 配置动态性 :
webpack.config.js
是一个函数,根据NODE_ENV
和环境变量动态生成配置。例如:javascriptmodule.exports = function (webpackEnv) { const isEnvDevelopment = webpackEnv === 'development'; return { mode: isEnvDevelopment ? 'development' : 'production', // 其他配置 }; };
-
HMR 实现 :通过
webpack.hot
模块和react-refresh
(React Fast Refresh)实现组件级别的热更新,而非页面刷新。 -
文件监听 :
webpack-dev-server
监听src
和public
文件夹的变化,触发重新编译。 -
webpack.config.js 具体配置分析参考 附录一
3. 打包工程(react-scripts build
)
功能概述
运行 npm run build
或 react-scripts build
会生成生产环境的静态文件(HTML、CSS、JS 等),优化后的代码将输出到 build
目录,用于部署到静态服务器或 CDN。
实现逻辑
- 入口 :
react-scripts/bin/react-scripts.js
调用react-scripts/scripts/build.js
。 - 核心步骤 :
- 环境变量设置 :
- 设置
NODE_ENV=production
,启用生产模式优化。 - 加载
.env.production
或.env
文件中的环境变量。
- 设置
- Webpack 配置 :
- 使用
react-scripts/config/webpack.config.js
的生产模式配置。 - 配置特点:
- 使用
TerserPlugin
压缩 JS 代码,移除开发模式的警告和调试信息。 - 使用
MiniCssExtractPlugin
提取 CSS 到单独文件,并通过OptimizeCSSAssetsPlugin
压缩。 - 设置
devtool: 'source-map'
,生成独立源码映射文件,便于生产环境调试。 - 启用代码分割(Code Splitting),通过
splitChunks
优化公共依赖。 - 使用
DefinePlugin
将process.env.NODE_ENV
设置为"production"
,移除开发模式条件代码(如if (process.env.NODE_ENV !== 'production')
)。
- 使用
- 使用
- 构建过程 :
- 调用 Webpack 的
compiler.run
方法执行编译。 - 输出文件到
build
目录,默认包含:index.html
(由html-webpack-plugin
生成,注入 JS 和 CSS)。static/js/
和static/css/
(压缩后的资源文件)。asset-manifest.json
(资源清单,便于服务器端渲染或动态加载)。
- 调用 Webpack 的
- 构建检查 :
- 检查文件大小,若超过阈值(如 244 KiB),发出性能警告。
- 支持
CI=true
环境变量,在 CI 环境中强制检查 lint 错误。
- 环境变量设置 :
源码细节
-
优化逻辑 :
TerserPlugin
的配置移除注释和 console.log:javascriptnew TerserPlugin({ terserOptions: { compress: { drop_console: false, // 可通过环境变量启用 }, }, });
-
文件名哈希 :使用
[contenthash]
占位符生成带哈希的文件名(如main.123abc.js
),确保缓存失效。 -
公共路径 :支持
PUBLIC_URL
环境变量自定义资源前缀,适合部署到子目录。
4. 测试工程(react-scripts test
)
功能概述
运行 npm test
或 react-scripts test
会启动 Jest 测试运行器,默认以交互式 watch 模式运行,执行 src
目录下以 .test.js
或 .spec.js
结尾的测试文件。
实现逻辑
- 入口 :
react-scripts/bin/react-scripts.js
调用react-scripts/scripts/test.js
。 - 核心步骤 :
- 环境变量设置 :
- 设置
NODE_ENV=test
,启用测试模式。 - 加载
.env.test
或.env
文件中的环境变量。
- 设置
- Jest 配置 :
- 使用
react-scripts/config/jest
目录下的配置。 - 配置特点:
- 使用
jsdom
作为测试环境,模拟浏览器 DOM。 - 通过
babel-jest
转换 JSX 和 ES6+ 代码。(config/jest/babelTransform.js
) - 设置
setupFilesAfterEnv
加载react-scripts/config/setupTests.js
,初始化测试工具(如@testing-library/react
)。 - 配置快照序列化(
jest-serializer
)和模块映射(moduleNameMapper
)。
- 使用
- 使用
- 测试执行 :
- 调用 Jest 的 CLI 接口(
jest/bin/jest.js
),传入动态生成的配置。 - 默认启用 watch 模式,监控文件变化并重新运行相关测试。
- 支持
CI=true
环境变量,禁用 watch 模式并一次性运行所有测试。
- 调用 Jest 的 CLI 接口(
- 测试覆盖率 :
- 支持
--coverage
参数,生成覆盖率报告到coverage
目录。
- 支持
- 环境变量设置 :
源码细节
-
Jest 配置动态性 :
react-scripts/scripts/test.js
根据环境变量调整参数:javascriptconst jestConfig = { roots: [paths.appSrc], testEnvironment: 'jsdom', // 其他配置 };
-
文件匹配 :使用
testMatch
匹配测试文件,默认包括**/__tests__/**/*.[jt]s?(x)
。
5. 其他功能(react-scripts eject
)
功能概述
运行 react-scripts eject
将所有隐藏的配置和脚本暴露到项目根目录,允许开发者完全自定义构建流程。
实现逻辑
- 入口 :
react-scripts/scripts/eject.js
。 - 核心步骤 :
- 复制
react-scripts/config
和react-scripts/scripts
到项目根目录。 - 更新
package.json
,移除react-scripts
依赖,添加 Webpack、Babel 等底层依赖。 - 修改
scripts
字段,指向本地的scripts/*.js
文件。
- 复制
应用场景分析
- 自定义需求:适用于需要调整 Webpack 配置或添加特殊插件的项目。
- 学习用途:帮助开发者理解 CRA 的构建流程。
通过对 react-scripts
源码的分析,我们已经了解了它在 create-react-app
(CRA)中的核心作用:封装 Webpack、Babel、Jest 等工具链,提供启动开发环境、打包工程和测试工程的"零配置"体验。当我们对 CRA 初始化项目执行 react-scripts eject
后,所有的配置文件(如 Webpack 配置)和脚本会被暴露出来。一直以来这都是一个不可逆操作,下面我们尝试下如何基于 eject 后的配置进行再次封装。
1. 目标与设计理念
目标
创建一个名为 my-react-scripts
的工具包,具备以下功能:
- 支持启动开发环境(类似
react-scripts start
)。 - 支持生产环境打包(类似
react-scripts build
)。 - 支持测试运行(类似
react-scripts test
)。 - 可扩展,允许用户自定义配置。
- 提供 CLI 接口,简化命令调用。
设计理念
- 模块化:将开发、构建、测试功能分开,便于维护和扩展。
- 动态配置:通过函数式配置支持环境变量和用户自定义选项。
- 开发者友好:保留零配置体验,同时提供扩展点。
- 现代化:基于 eject 后的 Webpack 配置,优化性能并支持现代工具(如 ES Modules、TypeScript)。
2. 项目结构规划
基于 react-scripts
的源码结构,设计 my-react-scripts
的目录如下:
perl
my-react-scripts/
├── bin/
│ └── my-react-scripts.js # CLI 入口脚本
├── config/
│ ├── webpack.config.js # Webpack 配置(开发和生产模式)
│ ├── jest.config.js # Jest 配置
│ └── env.js # 环境变量处理
├── scripts/
│ ├── start.js # 启动开发环境
│ ├── build.js # 打包生产环境
│ └── test.js # 运行测试
├── utils/
│ ├── paths.js # 项目路径工具
│ └── logger.js # 日志工具
├── package.json # 依赖和脚本定义
└── README.md # 使用说明
3. 实现步骤
3.1 创建 CLI 入口
在 bin/my-react-scripts.js
中实现命令分发逻辑,参考 react-scripts/bin/react-scripts.js
:
javascript
#!/usr/bin/env node
const args = process.argv.slice(2);
const script = args[0];
const scripts = ['start', 'build', 'test'];
if (!scripts.includes(script)) {
console.error(`Unknown script "${script}". Supported scripts: ${scripts.join(', ')}`);
process.exit(1);
}
require(`../scripts/${script}`);
在 package.json
中配置:
json
{
"name": "my-react-scripts",
"version": "1.0.0",
"bin": {
"my-react-scripts": "./bin/my-react-scripts.js"
},
"dependencies": {
"webpack": "^5.x.x",
"webpack-dev-server": "^4.x.x",
"babel-loader": "^9.x.x",
"jest": "^29.x.x",
// 其他依赖
}
}
3.2 处理环境变量
在 config/env.js
中实现环境变量加载,参考 react-scripts
的逻辑:
javascript
const fs = require('fs');
const path = require('path');
const dotenv = require('dotenv');
module.exports = function getEnvConfig(env) {
const NODE_ENV = env || process.env.NODE_ENV || 'development';
const envFiles = [
path.resolve(process.cwd(), `.env.${NODE_ENV}`),
path.resolve(process.cwd(), '.env'),
];
const envConfig = {};
envFiles.forEach(file => {
if (fs.existsSync(file)) {
const result = dotenv.parse(fs.readFileSync(file));
Object.assign(envConfig, result);
}
});
return {
NODE_ENV,
PUBLIC_URL: envConfig.PUBLIC_URL || '',
raw: envConfig,
stringified: Object.keys(envConfig).reduce((acc, key) => {
acc[`process.env.${key}`] = JSON.stringify(envConfig[key]);
return acc;
}, { 'process.env.NODE_ENV': JSON.stringify(NODE_ENV) }),
};
};
3.3 配置 Webpack
在 config/webpack.config.js
中基于 eject 后的配置进行改造,添加动态性:
javascript
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { DefinePlugin } = require('webpack');
const getEnvConfig = require('./env');
module.exports = function (env) {
const isDev = env === 'development';
const envConfig = getEnvConfig(env);
return {
mode: isDev ? 'development' : 'production',
entry: path.resolve(process.cwd(), 'src/index.js'),
output: {
path: path.resolve(process.cwd(), 'build'),
filename: isDev ? 'static/js/bundle.js' : 'static/js/[name].[contenthash:8].js',
publicPath: envConfig.PUBLIC_URL || '/',
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(process.cwd(), 'public/index.html'),
}),
new DefinePlugin(envConfig.stringified),
],
devServer: isDev
? {
port: process.env.PORT || 3000,
hot: true,
open: true,
}
: undefined,
devtool: isDev ? 'cheap-module-source-map' : 'source-map',
};
};
3.4 实现开发环境启动
在 scripts/start.js
中调用 Webpack Dev Server:
javascript
const Webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const webpackConfig = require('../config/webpack.config');
const compiler = Webpack(webpackConfig('development'));
const devServerOptions = { ...webpackConfig('development').devServer };
const server = new WebpackDevServer(devServerOptions, compiler);
server.startCallback(err => {
if (err) {
console.error(err);
process.exit(1);
}
console.log('Dev server running at http://localhost:3000');
});
3.5 实现生产环境打包
在 scripts/build.js
中执行 Webpack 编译:
javascript
const Webpack = require('webpack');
const webpackConfig = require('../config/webpack.config');
const compiler = Webpack(webpackConfig('production'));
compiler.run((err, stats) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(stats.toString({ colors: true }));
console.log('Build completed successfully!');
});
3.6 实现测试运行
在 scripts/test.js
中配置 Jest:
javascript
const Jest = require('jest');
const getEnvConfig = require('../config/env');
const envConfig = getEnvConfig('test');
process.env = { ...process.env, ...envConfig.raw };
Jest.run(process.argv.slice(2));
在 config/jest.config.js
中定义:
javascript
module.exports = {
roots: ['<rootDir>/src'],
testEnvironment: 'jsdom',
testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
};
3.7 添加工具函数
在 utils/paths.js
中定义路径:
javascript
const path = require('path');
module.exports = {
appSrc: path.resolve(process.cwd(), 'src'),
appPublic: path.resolve(process.cwd(), 'public'),
appBuild: path.resolve(process.cwd(), 'build'),
};
4. 封装与发布
-
测试工具包:
-
在本地项目中运行
npm link
,链接my-react-scripts
。 -
初始化一个 React 项目,修改
package.json
:json{ "scripts": { "start": "my-react-scripts start", "build": "my-react-scripts build", "test": "my-react-scripts test" } }
-
验证功能是否正常。
-
-
发布到 npm:
- 更新
package.json
的版本号。 - 运行
npm publish
。
- 更新
-
提供扩展点:
-
支持用户传入自定义配置(如
my-scripts.config.js
):javascriptconst userConfig = require(path.resolve(process.cwd(), 'my-scripts.config.js')); const finalConfig = merge(webpackConfig(env), userConfig.webpack || {});
-
5. 与 react-scripts
的差异与改进
差异
- 精简性:去掉不必要的插件(如 ESLint、Stylelint),按需添加。
- 现代化:支持 TypeScript、ES Modules 等新特性。
- 可控性:无需 eject,提供配置覆盖机制。
改进
- 性能优化 :集成更快的工具(如
esbuild
替代 Babel)。 - 模块化扩展:支持插件系统,动态加载功能。
- 文档支持 :在
README.md
中详细说明用法和配置项。
附录一 webpack.config.js
文件内容分析
依赖引入
javascript
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 ESLintPlugin = require('eslint-webpack-plugin');
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 ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
// @remove-on-eject-begin
const getCacheIdentifier = require('react-dev-utils/getCacheIdentifier');
// @remove-on-eject-end
const postcssNormalize = require('postcss-normalize');
- 核心模块 :
fs
和path
用于文件操作;webpack
是构建核心。 - 插件 :
PnpWebpackPlugin
:支持 Yarn Plug'n'Play。HtmlWebpackPlugin
:生成 HTML 文件。TerserPlugin
和OptimizeCSSAssetsPlugin
:生产模式压缩 JS 和 CSS。MiniCssExtractPlugin
:提取 CSS 到单独文件。WorkboxWebpackPlugin
:生成服务工作线程。ReactRefreshWebpackPlugin
:React Fast Refresh 热更新。
- 自定义工具 :如
paths
(路径定义)、getClientEnvironment
(环境变量处理)。 - 条件移除 :
getCacheIdentifier
在 eject 后移除,用于缓存标识。
常量与环境变量
javascript
const appPackageJson = require(paths.appPackageJson);
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
const webpackDevClientEntry = require.resolve('react-dev-utils/webpackHotDevClient');
const reactRefreshOverlayEntry = require.resolve('react-dev-utils/refreshOverlayInterop');
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
const emitErrorsAsWarnings = process.env.ESLINT_NO_DEV_ERRORS === 'true';
const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === 'true';
const imageInlineSizeLimit = parseInt(process.env.IMAGE_INLINE_SIZE_LIMIT || '10000');
const useTypeScript = fs.existsSync(paths.appTsConfig);
const swSrc = paths.swSrc;
appPackageJson
:加载项目package.json
。shouldUseSourceMap
:是否生成源码映射,默认启用。webpackDevClientEntry
:开发模式的 Webpack 客户端入口。reactRefreshOverlayEntry
:错误覆盖层入口。shouldInlineRuntimeChunk
:是否内联运行时 chunk,默认启用。emitErrorsAsWarnings
:开发模式下将 ESLint 错误转为警告。disableESLintPlugin
:禁用 ESLint 插件。imageInlineSizeLimit
:图片内联大小限制,默认 10KB。useTypeScript
:检查是否存在 TypeScript 配置文件。swSrc
:服务工作线程源文件路径。
文件正则与 JSX 检查
javascript
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
const hasJsxRuntime = (() => {
if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
return false;
}
try {
require.resolve('react/jsx-runtime');
return true;
} catch (e) {
return false;
}
})();
- 正则:匹配 CSS 和 Sass 文件,区分普通和模块化文件。
hasJsxRuntime
:检查是否支持新 JSX 转换(React 17+),影响 Babel 配置。
主配置函数
javascript
module.exports = function (webpackEnv) {
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';
const isEnvProductionProfile = isEnvProduction && process.argv.includes('--profile');
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
const shouldUseReactRefresh = env.raw.FAST_REFRESH;
- 导出函数 :接收
webpackEnv
参数,动态生成配置。 isEnvDevelopment
和isEnvProduction
:环境判断。isEnvProductionProfile
:生产模式下是否启用性能分析。env
:获取环境变量。shouldUseReactRefresh
:是否启用 React Fast Refresh。
样式加载器函数
javascript
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
options: paths.publicUrlOrPath.startsWith('.') ? { publicPath: '../../' } : {},
},
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
{
loader: require.resolve('postcss-loader'),
options: {
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({ autoprefixer: { flexbox: 'no-2009' }, stage: 3 }),
postcssNormalize(),
],
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
},
},
].filter(Boolean);
if (preProcessor) {
loaders.push(
{
loader: require.resolve('resolve-url-loader'),
options: { sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment, root: paths.appSrc },
},
{
loader: require.resolve(preProcessor),
options: { sourceMap: true },
}
);
}
return loaders;
};
getStyleLoaders
:动态生成样式加载器。- 开发模式:使用
style-loader
。 - 生产模式:使用
MiniCssExtractPlugin.loader
。 - 通用:
css-loader
和postcss-loader
(添加前缀和规范化)。 - 如果有预处理器(如
sass-loader
),添加resolve-url-loader
和对应加载器。
- 开发模式:使用
配置主体
javascript
return {
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
bail: isEnvProduction,
devtool: isEnvProduction
? shouldUseSourceMap ? 'source-map' : false
: isEnvDevelopment && 'cheap-module-source-map',
mode
:生产或开发模式。bail
:生产模式下出错即退出。devtool
:源码映射设置。
入口与输出
javascript
entry:
isEnvDevelopment && !shouldUseReactRefresh
? [webpackDevClientEntry, paths.appIndexJs]
: paths.appIndexJs,
output: {
path: isEnvProduction ? paths.appBuild : undefined,
pathinfo: isEnvDevelopment,
filename: isEnvProduction ? 'static/js/[name].[contenthash:8].js' : isEnvDevelopment && 'static/js/bundle.js',
futureEmitAssets: true,
chunkFilename: isEnvProduction ? 'static/js/[name].[contenthash:8].chunk.js' : isEnvDevelopment && 'static/js/[name].chunk.js',
publicPath: paths.publicUrlOrPath,
devtoolModuleFilenameTemplate: isEnvProduction
? info => path.relative(paths.appSrc, info.absoluteResourcePath).replace(/\\/g, '/')
: isEnvDevelopment && (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
jsonpFunction: `webpackJsonp${appPackageJson.name}`,
globalObject: 'this',
},
entry
:开发模式下若禁用 Fast Refresh,包含客户端入口;否则仅为应用入口。output
:futureEmitAssets
:Webpack 4 兼容性选项。jsonpFunction
:避免多个 Webpack 运行时冲突。globalObject
:支持 Web Worker。
优化配置
javascript
optimization: {
minimize: isEnvProduction,
minimizer: [
new TerserPlugin({
terserOptions: {
parse: { ecma: 8 },
compress: { ecma: 5, warnings: false, comparisons: false, inline: 2 },
mangle: { safari10: true },
keep_classnames: isEnvProductionProfile,
keep_fnames: isEnvProductionProfile,
output: { ecma: 5, comments: false, ascii_only: true },
},
sourceMap: shouldUseSourceMap,
}),
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
parser: safePostCssParser,
map: shouldUseSourceMap ? { inline: false, annotation: true } : false,
},
cssProcessorPluginOptions: { preset: ['default', { minifyFontValues: { removeQuotes: false } }] },
}),
],
splitChunks: { chunks: 'all', name: isEnvDevelopment },
runtimeChunk: { name: entrypoint => `runtime-${entrypoint.name}` },
},
minimizer
:生产模式下压缩 JS 和 CSS。splitChunks
和runtimeChunk
:代码分割和运行时分离。
模块解析与加载规则
javascript
resolve: { /* ... */ },
resolveLoader: { /* ... */ },
module: {
strictExportPresence: true,
rules: [
{ parser: { requireEnsure: false } },
{
oneOf: [
{ test: [/\.avif$/], loader: require.resolve('url-loader'), /* ... */ },
{ test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], loader: require.resolve('url-loader'), /* ... */ },
{ test: /\.(js|mjs|jsx|ts|tsx)$/, include: paths.appSrc, loader: require.resolve('babel-loader'), /* ... */ },
{ test: /\.(js|mjs)$/, exclude: /@babel(?:\/|\\{1,2})runtime/, loader: require.resolve('babel-loader'), /* ... */ },
{ test: cssRegex, exclude: cssModuleRegex, use: getStyleLoaders({ /* ... */ }), sideEffects: true },
{ test: cssModuleRegex, use: getStyleLoaders({ /* ... */ }) },
{ test: sassRegex, exclude: sassModuleRegex, use: getStyleLoaders({ /* ... */ }, 'sass-loader'), sideEffects: true },
{ test: sassModuleRegex, use: getStyleLoaders({ /* ... */ }, 'sass-loader') },
{ loader: require.resolve('file-loader'), exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/], /* ... */ },
],
},
],
},
- 解析与加载:支持图片、JS/TS、CSS/Sass 等多种资源类型。
插件配置
javascript
plugins: [
new HtmlWebpackPlugin(/* ... */),
isEnvProduction && shouldInlineRuntimeChunk && new InlineChunkHtmlPlugin(/* ... */),
new InterpolateHtmlPlugin(/* ... */),
new ModuleNotFoundPlugin(/* ... */),
new webpack.DefinePlugin(/* ... */),
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
isEnvDevelopment && shouldUseReactRefresh && new ReactRefreshWebpackPlugin(/* ... */),
isEnvDevelopment && new CaseSensitivePathsPlugin(),
isEnvDevelopment && new WatchMissingNodeModulesPlugin(/* ... */),
isEnvProduction && new MiniCssExtractPlugin(/* ... */),
new ManifestPlugin(/* ... */),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
isEnvProduction && fs.existsSync(swSrc) && new WorkboxWebpackPlugin.InjectManifest(/* ... */),
useTypeScript && new ForkTsCheckerWebpackPlugin(/* ... */),
!disableESLintPlugin && new ESLintPlugin(/* ... */),
].filter(Boolean),
- 插件:支持 HTML 生成、热更新、服务工作线程、类型检查等功能。
其他配置
javascript
node: { /* ... */ },
performance: false,
node
:为 Node.js 模块提供空值实现。performance
:禁用性能提示。