基于react-scripts源码,仿写自定义开发工具包

create-react-app(CRA)工具生成的模板工程中react-scripts承担着工程脚本的基本功能,例如运行、打包、测试等,接下来我们基于 react-scripts (v4.0.3)的源码,分析其实现逻辑,并且尝试在eject之后对其再次封装。


1. 概述 react-scripts

react-scriptscreate-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 脚本,根据传入的命令(如 startbuildtest)调用对应的功能模块。我们将逐一分析这些场景的实现逻辑。


2. 启动开发环境(react-scripts start

功能概述

运行 npm startreact-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);
    });
  })
.......
  • 核心步骤
    1. 环境变量设置

      • 设置 NODE_ENV=development,确保开发模式下的配置生效。
      • 读取 .env 文件(通过 dotenvdotenv-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 环境变量,决定是否打开浏览器以及使用哪个浏览器。
    2. 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-pluginstylelint-webpack-plugin)进行代码检查。
    3. 开发服务器启动

      • 使用 webpack-dev-serverstart 方法启动服务器,默认监听 localhost:3000(端口可通过 PORT 环境变量自定义)。
      • 支持 HTTPS(通过 HTTPS=true 启用)。
      • 配置代理(通过 proxy 字段在 package.json 中定义)。

源码细节

  • Webpack 配置动态性webpack.config.js 是一个函数,根据 NODE_ENV 和环境变量动态生成配置。例如:

    javascript 复制代码
    module.exports = function (webpackEnv) {
      const isEnvDevelopment = webpackEnv === 'development';
      return {
        mode: isEnvDevelopment ? 'development' : 'production',
        // 其他配置
      };
    };
  • HMR 实现 :通过 webpack.hot 模块和 react-refresh(React Fast Refresh)实现组件级别的热更新,而非页面刷新。

  • 文件监听webpack-dev-server 监听 srcpublic 文件夹的变化,触发重新编译。

  • webpack.config.js 具体配置分析参考 附录一


3. 打包工程(react-scripts build

功能概述

运行 npm run buildreact-scripts build 会生成生产环境的静态文件(HTML、CSS、JS 等),优化后的代码将输出到 build 目录,用于部署到静态服务器或 CDN。

实现逻辑

  • 入口react-scripts/bin/react-scripts.js 调用 react-scripts/scripts/build.js
  • 核心步骤
    1. 环境变量设置
      • 设置 NODE_ENV=production,启用生产模式优化。
      • 加载 .env.production.env 文件中的环境变量。
    2. Webpack 配置
      • 使用 react-scripts/config/webpack.config.js 的生产模式配置。
      • 配置特点:
        • 使用 TerserPlugin 压缩 JS 代码,移除开发模式的警告和调试信息。
        • 使用 MiniCssExtractPlugin 提取 CSS 到单独文件,并通过 OptimizeCSSAssetsPlugin 压缩。
        • 设置 devtool: 'source-map',生成独立源码映射文件,便于生产环境调试。
        • 启用代码分割(Code Splitting),通过 splitChunks 优化公共依赖。
        • 使用 DefinePluginprocess.env.NODE_ENV 设置为 "production",移除开发模式条件代码(如 if (process.env.NODE_ENV !== 'production'))。
    3. 构建过程
      • 调用 Webpack 的 compiler.run 方法执行编译。
      • 输出文件到 build 目录,默认包含:
        • index.html(由 html-webpack-plugin 生成,注入 JS 和 CSS)。
        • static/js/static/css/(压缩后的资源文件)。
        • asset-manifest.json(资源清单,便于服务器端渲染或动态加载)。
    4. 构建检查
      • 检查文件大小,若超过阈值(如 244 KiB),发出性能警告。
      • 支持 CI=true 环境变量,在 CI 环境中强制检查 lint 错误。

源码细节

  • 优化逻辑TerserPlugin 的配置移除注释和 console.log:

    javascript 复制代码
    new TerserPlugin({
      terserOptions: {
        compress: {
          drop_console: false, // 可通过环境变量启用
        },
      },
    });
  • 文件名哈希 :使用 [contenthash] 占位符生成带哈希的文件名(如 main.123abc.js),确保缓存失效。

  • 公共路径 :支持 PUBLIC_URL 环境变量自定义资源前缀,适合部署到子目录。


4. 测试工程(react-scripts test

功能概述

运行 npm testreact-scripts test 会启动 Jest 测试运行器,默认以交互式 watch 模式运行,执行 src 目录下以 .test.js.spec.js 结尾的测试文件。

实现逻辑

  • 入口react-scripts/bin/react-scripts.js 调用 react-scripts/scripts/test.js
  • 核心步骤
    1. 环境变量设置
      • 设置 NODE_ENV=test,启用测试模式。
      • 加载 .env.test.env 文件中的环境变量。
    2. 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)。
    3. 测试执行
      • 调用 Jest 的 CLI 接口(jest/bin/jest.js),传入动态生成的配置。
      • 默认启用 watch 模式,监控文件变化并重新运行相关测试。
      • 支持 CI=true 环境变量,禁用 watch 模式并一次性运行所有测试。
    4. 测试覆盖率
      • 支持 --coverage 参数,生成覆盖率报告到 coverage 目录。

源码细节

  • Jest 配置动态性react-scripts/scripts/test.js 根据环境变量调整参数:

    javascript 复制代码
    const jestConfig = {
      roots: [paths.appSrc],
      testEnvironment: 'jsdom',
      // 其他配置
    };
  • 文件匹配 :使用 testMatch 匹配测试文件,默认包括 **/__tests__/**/*.[jt]s?(x)


5. 其他功能(react-scripts eject

功能概述

运行 react-scripts eject 将所有隐藏的配置和脚本暴露到项目根目录,允许开发者完全自定义构建流程。

实现逻辑

  • 入口react-scripts/scripts/eject.js
  • 核心步骤
    1. 复制 react-scripts/configreact-scripts/scripts 到项目根目录。
    2. 更新 package.json,移除 react-scripts 依赖,添加 Webpack、Babel 等底层依赖。
    3. 修改 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. 封装与发布

  1. 测试工具包

    • 在本地项目中运行 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"
        }
      }
    • 验证功能是否正常。

  2. 发布到 npm

    • 更新 package.json 的版本号。
    • 运行 npm publish
  3. 提供扩展点

    • 支持用户传入自定义配置(如 my-scripts.config.js):

      javascript 复制代码
      const 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');
  • 核心模块fspath 用于文件操作;webpack 是构建核心。
  • 插件
    • PnpWebpackPlugin:支持 Yarn Plug'n'Play。
    • HtmlWebpackPlugin:生成 HTML 文件。
    • TerserPluginOptimizeCSSAssetsPlugin:生产模式压缩 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 参数,动态生成配置。
  • isEnvDevelopmentisEnvProduction:环境判断。
  • 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-loaderpostcss-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。
  • splitChunksruntimeChunk:代码分割和运行时分离。

模块解析与加载规则

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:禁用性能提示。

相关推荐
范哥来了17 分钟前
python web开发flask库安装与使用
前端·python·flask
顾林海36 分钟前
Flutter Dart 泛型详解
android·前端·flutter
百慕大三角40 分钟前
如何用AI工具设计出令人惊艳的页面(附截图)
前端·trae·ai 编程
唐某人丶44 分钟前
如何优化 React 组件?
前端·react.js·前端框架
树上有只程序猿1 小时前
Vue3组合式API从原理到实战终极指南
前端
code_Bo1 小时前
vue2使用el-cascader在table中下拉框不跟随滚动问题
前端·vue.js·element
王小菲1 小时前
JavaScript 装箱机制与解构赋值深度解析
前端·javascript·面试
用户49430538293801 小时前
基于GIS数据的即时建筑模型编辑软件
前端·算法·gis
Shawn5901 小时前
如何使用useCallback优化React性能?
前端·react.js
Suckerbin1 小时前
PHP前置知识-HTML学习
前端·学习·html