React + Webpack + React Router + TypeScript + Ant Design 多子项目工程化

187 阅读9分钟

本文手把手带你从零搭建一个包含 3 个 React 子项目(app-a、app-b、app-c)的工程,并具备:分环境 Webpack 配置、TypeScript、ESLint、Prettier、React Refresh、路由懒加载、细粒度拆包、(可选)图片压缩,以及 Nginx/Docker/CI-CD 的部署示例。


0. 目标与效果

  • 单独开发:3001/3002/3003 端口分别启动 app-a/app-b/app-c
  • 单独构建:产出 dist/app-a、dist/app-b、dist/app-c 三套独立产物
  • 共享代码:src/shared 组件与样式被三个子项目复用
  • 优化:React Refresh、懒加载、细粒度拆包、(可选)图片压缩

1. 环境准备

  • Node.js >= 18(建议 18.18+)
  • npm >= 8
  • macOS/Windows/Linux 均可

检查版本:

node -v
npm -v

2. 初始化项目

mkdir -p "webpack-build"
cd "webpack-build"
npm init -y

3. 安装依赖

运行时依赖:

npm i react@^19 react-dom@^19 react-router-dom@^6 antd@^5

注意:本文示例基于 React 19 与 Ant Design v5(示例中使用 antd/dist/reset.css)。

开发依赖:

npm i -D webpack webpack-cli webpack-dev-server \
  babel-loader@9.1.3 @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript \
  html-webpack-plugin css-loader style-loader \
  typescript @types/react @types/react-dom @types/node \
  fork-ts-checker-webpack-plugin \
  @pmmmwh/react-refresh-webpack-plugin react-refresh \
  eslint@8.57.0 @typescript-eslint/parser @typescript-eslint/eslint-plugin \
  eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-import \
  eslint-import-resolver-typescript eslint-config-prettier eslint-plugin-prettier prettier \
  webpack-merge

(可选)图片压缩(默认不启用,需手动开启):

npm i -D image-minimizer-webpack-plugin @squoosh/lib

4. 创建目录结构

mkdir -p public src/shared src/app-a/pages src/app-b/pages src/app-c/pages build

期望结构:

webpack-build/
  public/
    index.html
  src/
    app-a/
      App.tsx
      index.tsx
      pages/
        Home.tsx
        About.tsx
    app-b/
      App.tsx
      index.tsx
      pages/
        Home.tsx
        Docs.tsx
    app-c/
      App.tsx
      index.tsx
      pages/
        Dashboard.tsx
        Settings.tsx
    shared/
      Button.tsx
      global.css
  build/
    webpack.common.js
    webpack.dev.js
    webpack.prod.js
  .babelrc
  tsconfig.json
  .eslintrc.cjs
  .eslintignore
  .prettierrc.json
  .prettierignore
  webpack.config.js
  package.json
  .gitignore

5. 配置文件

.babelrc

{
  "presets": [
    ["@babel/preset-env", { "targets": ">0.2%, not dead, not op_mini all" }],
    ["@babel/preset-react", { "runtime": "automatic" }],
    ["@babel/preset-typescript", { "allowNamespaces": true }]
  ]
}

tsconfig.json(支持动态 import 与路径别名)

{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ES2019",
    "lib": ["DOM", "ES2019"],
    "moduleResolution": "Node",
    "jsx": "react-jsx",
    "baseUrl": ".",
    "paths": { "@shared/*": ["src/shared/*"] },
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true
  },
  "include": ["src"]
}

public/index.html

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

.eslintrc.cjs

module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true } },
  settings: {
    react: { version: 'detect' },
    'import/resolver': { typescript: { project: __dirname + '/tsconfig.json' } }
  },
  env: { browser: true, es2021: true, node: true },
  plugins: ['@typescript-eslint', 'react', 'react-hooks', 'import', 'prettier'],
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'plugin:import/recommended',
    'plugin:import/typescript',
    'plugin:prettier/recommended',
    'prettier'
  ],
  rules: {
    'prettier/prettier': 'warn',
    'react/react-in-jsx-scope': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
    'import/order': ['warn', { 'newlines-between': 'always', alphabetize: { order: 'asc' } }]
  }
};

.prettierrc.json

{ "singleQuote": true, "trailingComma": "all", "printWidth": 100, "semi": true, "arrowParens": "always" }

.eslintignore.prettierignore

dist/
node_modules/

.gitignore

node_modules/
dist/
.DS_Store
npm-debug.log*
yarn-error.log*
pnpm-debug.log*

package.json 中 browserslist(示例)

{
  "browserslist": [
    ">0.2%",
    "not dead",
    "not op_mini all"
  ]
}

6. Webpack 配置(分环境 + 根配置)

使用说明:

  • 开发只编译一个子项目:webpack serve --config build/webpack.dev.js --env TARGET=app-a
  • 构建全部:webpack --config build/webpack.prod.js
  • 根配置同理(传 --config webpack.config.js)。

6.1 build/webpack.common.js(通用配置)

// 通用基础配置:loader、alias、公共插件,dev/prod 共享
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

// 三个子项目元信息统一维护,便于扩展更多子项目
const apps = [
  { name: 'app-a', title: 'App A', entry: './src/app-a/index.tsx', port: 3001 },
  { name: 'app-b', title: 'App B', entry: './src/app-b/index.tsx', port: 3002 },
  { name: 'app-c', title: 'App C', entry: './src/app-c/index.tsx', port: 3003 }
];

// 生成单个子项目的通用配置(不含 mode/devtool/devServer/优化项)
// 说明:
// - output.publicPath 使用 'auto',便于部署到任意子路径;
// - 资源统一使用 Webpack5 asset 模块,无需额外 file/url-loader;
// - ForkTsChecker 将 TS 类型检查放到独立进程,避免阻塞构建主线程。
const makeCommon = ({ name, title, entry }) => ({
  name,
  entry: { [name]: entry },
  output: {
    path: path.resolve(__dirname, '..', 'dist', name),
    filename: 'static/js/[name].[contenthash:8].js',
    assetModuleFilename: 'static/media/[name].[contenthash:8][ext][query]',
    publicPath: 'auto',
    clean: true
  },
  module: {
    rules: [
      {
        // TS/JS 转译:交给 Babel,支持 TS + React JSX
        test: /(t|j)sx?$/,
        exclude: /node_modules/,
        use: { loader: 'babel-loader' }
      },
      {
        // 简单样式:style-loader 注入到页面,css-loader 解析 @import/url()
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        // 小图转 base64,大图发布到静态资源目录
        test: /\.(png|jpe?g|gif|svg|webp)$/i,
        type: 'asset',
        parser: { dataUrlCondition: { maxSize: 8 * 1024 } }
      },
      {
        // 字体等始终以文件形式产出
        test: /\.(woff2?|ttf|eot|otf)$/i,
        type: 'asset/resource'
      },
      {
        // 音视频资源
        test: /\.(mp4|mp3|wav|ogg)$/i,
        type: 'asset/resource'
      }
    ]
  },
  resolve: {
    // 支持导入省略后缀,提供共享目录别名
    extensions: ['.ts', '.tsx', '.js', '.jsx'],
    alias: { '@shared': path.resolve(__dirname, '..', 'src/shared') }
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: path.resolve(__dirname, '..', 'public/index.html'),
      title,
      chunks: [name]
    }),
    // TS 类型检查(单独进程)
    new ForkTsCheckerWebpackPlugin()
  ],
  stats: 'minimal'
});

module.exports = { apps, makeCommon };

6.2 build/webpack.dev.js(开发配置)

// 开发环境增强:React Refresh、高质量 SourceMap、DevServer
const { merge } = require('webpack-merge');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const path = require('path');
const { apps, makeCommon } = require('./webpack.common');

// 注入 React Refresh 的 babel 插件(与插件配合,实现状态保持的 HMR)
const withRefresh = (config) => {
  const rule = config.module.rules.find((r) => String(r.test) === '/(t|j)sx?$/');
  if (rule && rule.use && rule.use.loader === 'babel-loader') {
    rule.use.options = rule.use.options || {};
    rule.use.options.plugins = [require.resolve('react-refresh/babel')];
  }
  return config;
};

// 为单个子项目组装开发配置
const makeDevConfig = ({ name, title, entry, port }) => {
  const base = makeCommon({ name, title, entry });
  const merged = merge(base, {
    mode: 'development',
    // 更快的增量构建,同时具备较好的调试体验
    devtool: 'cheap-module-source-map',
    plugins: [new ReactRefreshWebpackPlugin()],
    devServer: {
      // 使用 public 目录提供静态资源;打包产物走内存
      static: { directory: path.resolve(__dirname, '..', 'public') },
      compress: true,
      port,
      open: true,
      hot: true,
      // 前端路由深链直达
      historyApiFallback: true
    }
  });
  return withRefresh(merged);
};

module.exports = (env = {}) => {
  if (env.TARGET) {
    const meta = apps.find((a) => a.name === env.TARGET);
    if (!meta) throw new Error(`Unknown TARGET "${env.TARGET}"`);
    return makeDevConfig(meta);
  }
  return apps.map((meta) => makeDevConfig(meta));
};

6.3 build/webpack.prod.js(生产配置)

说明:已新增构建进度条与体积告警屏蔽

  • 进度条:webpack.ProgressPlugin()
  • 屏蔽体积告警:performance: { hints: false }
// 生产环境优化:细粒度拆包、runtime 抽离、(可选)图片压缩
const { merge } = require('webpack-merge');
const webpack = require('webpack');
const { apps, makeCommon } = require('./webpack.common');
// 可选的图片压缩(避免在部分环境下安装/运行失败),通过环境变量开启
let ImageMinimizerPlugin;
try {
  ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
} catch (e) {
  ImageMinimizerPlugin = null;
}

const makeProdConfig = ({ name, title, entry }) =>
  merge(makeCommon({ name, title, entry }), {
    mode: 'production',
    devtool: 'source-map',
    // 放宽/屏蔽性能告警
    performance: { hints: false },
    optimization: {
      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          // 将 react 生态拆分为单独 chunk,提升复用与缓存命中
          react: {
            test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
            name: 'react-vendor',
            chunks: 'all',
            priority: 30
          },
          // 将 antd 相关拆分
          antd: {
            test: /[\\/]node_modules[\\/]antd[\\/]/,
            name: 'antd-vendor',
            chunks: 'all',
            priority: 20
          },
          defaultVendors: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all'
          }
        }
      },
      // 独立 runtime 提升缓存命中
      runtimeChunk: 'single'
    },
    plugins: [
      // 构建进度条
      new webpack.ProgressPlugin(),
      // 仅当设置 USE_IMG_MIN=true 且依赖可用时,启用图片压缩
      ...(process.env.USE_IMG_MIN && ImageMinimizerPlugin
        ? [
            new ImageMinimizerPlugin({
              minimizer: {
                implementation: ImageMinimizerPlugin.squooshMinify,
                options: {
                  encodeOptions: {
                    mozjpeg: { quality: 76 },
                    webp: { quality: 76 },
                    avif: { cqLevel: 35 },
                    oxipng: { level: 2 }
                  }
                }
              }
            })
          ]
        : [])
    ]
  });

module.exports = (env = {}) => {
  if (env.TARGET) {
    const meta = apps.find((a) => a.name === env.TARGET);
    if (!meta) throw new Error(`Unknown TARGET "${env.TARGET}"`);
    return makeProdConfig(meta);
  }
  return apps.map((meta) => makeProdConfig(meta));
};

6.4 webpack.config.js(根配置)

/**
 * Webpack 多应用构建配置文件
 * 
 * 功能特性:
 * 1. 支持多个独立React应用同时构建(app-a、app-b、app-c)
 * 2. 开发环境支持React Refresh热更新(保留组件状态)
 * 3. 生产环境自动代码分割和优化
 * 4. TypeScript支持(独立进程类型检查)
 * 5. 资源优化处理(图片、字体等)
 */

// 核心依赖引入
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

// React 组件热更新(保留组件状态),仅在开发环境启用
// 作用:实现开发时的"热模块替换",修改代码后页面不刷新,直接更新组件,保持状态
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

/**
 * 应用配置列表
 * 每个应用都有:
 * - name: 应用唯一标识,用于构建输出目录命名
 * - title: HTML页面标题
 * - entry: 应用入口文件路径
 * - port: 开发服务器端口号
 */
const apps = [
  { name: 'app-a', title: 'App A', entry: './src/app-a/index.tsx', port: 3001 },
  { name: 'app-b', title: 'App B', entry: './src/app-b/index.tsx', port: 3002 },
  { name: 'app-c', title: 'App C', entry: './src/app-c/index.tsx', port: 3003 }
];

/**
 * 生成单个应用的Webpack配置
 * 
 * @param {Object} appConfig - 应用配置对象
 * @param {string} appConfig.name - 应用名称
 * @param {string} appConfig.title - 页面标题
 * @param {string} appConfig.entry - 入口文件路径
 * @param {number} appConfig.port - 开发服务器端口
 * @param {boolean} isDev - 是否为开发模式
 * @returns {Object} Webpack配置对象
 * 
 * 设计原则:
 * 1. 开发模式:追求构建速度,启用source map和HMR
 * 2. 生产模式:追求优化和性能,启用代码分割和压缩
 */
const makeConfig = ({ name, title, entry, port }, isDev) => ({
  // 配置名称,用于webpack多配置构建时的标识
  name,
  
  // 构建模式:development(开发)或 production(生产)
  // 开发模式:启用调试工具,不压缩代码,追求构建速度
  // 生产模式:启用优化,压缩代码,追求性能和包体积
  mode: isDev ? 'development' : 'production',
  
  // 入口配置
  // 使用对象语法,key为chunk名称(与应用名称一致),value为入口文件路径
  entry: { [name]: entry },
  
  // 输出配置
  output: {
    // 输出目录:dist/应用名称/
    // 每个应用独立输出到dist下的子目录,避免冲突
    path: path.resolve(__dirname, 'dist', name),
    
    // 输出文件名模板
    // [name]: chunk名称(即应用名称)
    // [contenthash:8]: 基于内容生成的8位hash,用于缓存控制
    filename: 'static/js/[name].[contenthash:8].js',
    
    // 资源文件(图片、字体等)输出路径模板
    // 自动将小于8KB的图片转为base64内联,大于的单独输出
    assetModuleFilename: 'static/media/[name].[contenthash:8][ext][query]',
    
    // 公共资源路径:'auto'表示根据请求自动判断
    // 确保资源引用路径正确,支持CDN部署
    publicPath: 'auto',
    
    // 构建前清理输出目录,避免旧文件残留
    clean: true
  },
  
  // Source Map配置
  // 开发模式:cheap-module-source-map(构建速度快,足够调试)
  // 生产模式:source-map(完整source map,便于线上调试)
  devtool: isDev ? 'cheap-module-source-map' : 'source-map',
  
  // 性能配置
  // 关闭webpack的性能提示(如包体积过大等),避免开发时的干扰
  performance: { hints: false },
  
  // 模块处理规则
  module: {
    rules: [
      {
        // 处理TypeScript和JavaScript文件
        // 匹配.ts、.tsx、.js、.jsx文件
        test: /(t|j)sx?$/,
        // 排除node_modules,提升构建速度
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            // Babel插件配置
            plugins: [
              // 开发环境启用React Refresh插件
              // 实现热更新时保留React组件状态
              isDev && require.resolve('react-refresh/babel')
            ].filter(Boolean) // 过滤掉false值(生产环境时为undefined)
          }
        }
      },
      
      // CSS文件处理
      // style-loader:将CSS注入到页面
      // css-loader:解析CSS文件中的@import和url()
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      
      // 图片资源处理
      // type: 'asset'表示根据大小自动选择内联或单独文件
      // 小于8KB的图片自动转为base64内联到JS中,减少HTTP请求
      { 
        test: /\.(png|jpe?g|gif|svg|webp)$/i, 
        type: 'asset', 
        parser: { 
          dataUrlCondition: { 
            maxSize: 8 * 1024 // 8KB阈值
          } 
        } 
      },
      
      // 字体文件处理
      // type: 'asset/resource'表示始终作为单独文件输出
      { test: /\.(woff2?|ttf|eot|otf)$/i, type: 'asset/resource' },
      
      // 音视频文件处理
      // 大文件始终作为单独资源输出
      { test: /\.(mp4|mp3|wav|ogg)$/i, type: 'asset/resource' }
    ]
  },
  
  // 解析配置
  resolve: {
    // 自动解析的文件扩展名
    // 引入模块时可省略这些扩展名
    extensions: ['.ts', '.tsx', '.js', '.jsx'],
    
    // 路径别名配置
    // 使用@shared代替相对路径,提升代码可读性和维护性
    alias: { 
      '@shared': path.resolve(__dirname, 'src/shared')
    }
  },
  
  // 插件配置
  plugins: [
    // HTML模板处理插件
    // 基于public/index.html模板生成最终的HTML文件
    new HtmlWebpackPlugin({ 
      filename: 'index.html', // 输出文件名
      template: 'public/index.html', // 模板文件
      title, // 页面标题(从应用配置获取)
      chunks: [name] // 只引入当前应用的chunk
    }),
    
    // TypeScript类型检查插件
    // 在独立进程进行类型检查,不阻塞主构建流程
    new ForkTsCheckerWebpackPlugin(),
    
    // 构建进度显示插件
    // 在控制台显示构建进度百分比
    new webpack.ProgressPlugin(),
    
    // 开发环境专用插件
    // React Refresh插件:实现组件热更新
    ...(isDev ? [new ReactRefreshWebpackPlugin()] : [])
  ],
  
  // 优化配置(仅生产环境生效)
  optimization: {
    // 代码分割配置
    splitChunks: {
      chunks: 'all', // 对所有chunk生效(包括同步和异步)
      cacheGroups: {
        // React相关库单独打包
        // 包含react、react-dom、scheduler等核心库
        // 优先级最高(30),确保优先匹配
        react: { 
          test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/, 
          name: 'react-vendor', 
          chunks: 'all', 
          priority: 30 
        },
        
        // Ant Design组件库单独打包
        // 优先级次高(20)
        antd: { 
          test: /[\\/]node_modules[\\/]antd[\\/]/, 
          name: 'antd-vendor', 
          chunks: 'all', 
          priority: 20 
        },
        
        // 其他node_modules库统一打包
        // 默认优先级(10),匹配剩余的所有第三方库
        defaultVendors: { 
          test: /[\\/]node_modules[\\/]/, 
          name: 'vendors', 
          chunks: 'all' 
        }
      }
    },
    
    // 运行时代码单独打包
    // 将webpack的运行时代码单独提取,便于浏览器缓存
    runtimeChunk: 'single'
  },
  
  // 开发服务器配置(仅开发环境使用)
  devServer: {
    // 静态资源服务配置
    // 将public目录作为静态资源根目录
    static: { 
      directory: path.resolve(__dirname, 'public') 
    },
    
    // 启用gzip压缩,提升传输效率
    compress: true,
    
    // 服务端口(从应用配置获取)
    port,
    
    // 自动打开浏览器
    open: true,
    
    // 启用热模块替换(HMR)
    hot: true,
    
    // 前端路由支持
    // 所有404请求重定向到index.html,支持React Router等前端路由
    historyApiFallback: true
  },
  
  // 构建日志配置
  // 'minimal'表示只显示关键信息,减少控制台输出
  stats: 'minimal'
});

/**
 * Webpack配置导出函数
 * 
 * 支持两种使用方式:
 * 1. 同时构建所有应用:直接运行webpack
 * 2. 单独构建某个应用:webpack --env TARGET=app-a
 * 
 * @param {Object} env - 环境变量对象
 * @param {string} env.TARGET - 指定构建的应用名称
 * @param {Object} argv - webpack命令行参数
 * @param {string} argv.mode - 构建模式(development/production)
 */
module.exports = (env = {}, argv = {}) => {
  // 判断是否为开发模式
  const isDev = argv.mode === 'development';
  
  // 如果指定了TARGET,只构建单个应用
  if (env.TARGET) {
    const meta = apps.find(a => a.name === env.TARGET);
    if (!meta) {
      // 友好的错误提示,列出所有可用应用
      throw new Error(
        `Unknown TARGET "${env.TARGET}". Use one of: ${apps.map(a => a.name).join(', ')}`
      );
    }
    return makeConfig(meta, isDev);
  }
  
  // 默认构建所有应用
  // 返回数组配置,webpack会并行构建所有应用
  return apps.map(meta => makeConfig(meta, isDev));
};


7. 源码(共享模块与三子应用)

共享样式 src/shared/global.css

html, body, #root { height: 100%; margin: 0; }
* { box-sizing: border-box; }
.container { padding: 24px; }

共享组件 src/shared/Button.tsx

import { Button as AntButton } from 'antd';
import type { ReactNode } from 'react';

type Props = { onClick?: () => void; children?: ReactNode };

export default function Button({ onClick, children }: Props) {
  return (
    <AntButton type="primary" onClick={onClick}>
      {children}
    </AntButton>
  );
}

入口 src/app-*/index.tsx

import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';

import App from './App';
import 'antd/dist/reset.css';

const container = document.getElementById('root');
if (container) {
  const root = createRoot(container);
  root.render(
    <BrowserRouter>
      <App />
    </BrowserRouter>,
  );
}

页面(示例 src/app-a/App.tsx,B/C 类似;下文附上三子项目页面文件示例,按需新建)

import { Layout, Menu } from 'antd';
import { Link, Route, Routes } from 'react-router-dom';
import { Suspense, lazy } from 'react';
import '@shared/global.css';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

const { Header, Content, Footer } = Layout;

export default function App() {
  return (
    <Layout style={{ minHeight: '100%' }}>
      <Header style={{ display: 'flex', alignItems: 'center' }}>
        <div style={{ color: '#fff', marginRight: 24, fontWeight: 600 }}>App A</div>
        <Menu
          theme="dark"
          mode="horizontal"
          selectable={false}
          items={[
            { key: 'home', label: <Link to="/">首页</Link> },
            { key: 'about', label: <Link to="/about">关于</Link> },
          ]}
        />
      </Header>
      <Content className="container">
        <Suspense fallback={<div>页面加载中...</div>}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
          </Routes>
        </Suspense>
      </Content>
      <Footer style={{ textAlign: 'center' }}>App A ©2025</Footer>
    </Layout>
  );
}

页面文件(按需创建)

src/app-a/pages/Home.tsx

export default function Home() {
  return (
    <div>
      <h2>首页</h2>
      <p>这是 App A 的首页。</p>
    </div>
  );
}

src/app-a/pages/About.tsx

export default function About() {
  return (
    <div>
      <h2>关于</h2>
      <p>这里是 App A 的关于页面。</p>
    </div>
  );
}

src/app-b/pages/Home.tsx

export default function Home() {
  return (
    <div>
      <h2>首页</h2>
      <p>这是 App B 的首页。</p>
    </div>
  );
}

src/app-b/pages/Docs.tsx

export default function Docs() {
  return (
    <div>
      <h2>文档</h2>
      <p>这里是 App B 的文档页面。</p>
    </div>
  );
}

src/app-c/pages/Dashboard.tsx

export default function Dashboard() {
  return (
    <div>
      <h2>仪表盘</h2>
      <p>这里是 App C 的仪表盘。</p>
    </div>
  );
}

src/app-c/pages/Settings.tsx

export default function Settings() {
  return (
    <div>
      <h2>设置</h2>
      <p>这里是 App C 的设置页面。</p>
    </div>
  );
}

8. NPM Scripts(两套)

推荐(分环境配置):

{
  "scripts": {
    "start:a": "webpack serve --config build/webpack.dev.js --env TARGET=app-a",
    "start:b": "webpack serve --config build/webpack.dev.js --env TARGET=app-b",
    "start:c": "webpack serve --config build/webpack.dev.js --env TARGET=app-c",
    "build:a": "webpack --config build/webpack.prod.js --env TARGET=app-a",
    "build:b": "webpack --config build/webpack.prod.js --env TARGET=app-b",
    "build:c": "webpack --config build/webpack.prod.js --env TARGET=app-c",
    "build:all": "webpack --config build/webpack.prod.js",
    "lint": "eslint \"src/**/*.{ts,tsx,js,jsx}\" --max-warnings=0",
    "lint:fix": "eslint \"src/**/*.{ts,tsx,js,jsx}\" --fix",
    "format": "prettier --write \"**/*.{ts,tsx,js,jsx,css,md,json}\""
  }
}

根配置(可选):

{
  "scripts": {
    "start:cfg:a": "webpack serve --config webpack.config.js --mode development --env TARGET=app-a",
    "start:cfg:b": "webpack serve --config webpack.config.js --mode development --env TARGET=app-b",
    "start:cfg:c": "webpack serve --config webpack.config.js --mode development --env TARGET=app-c",
    "build:cfg:a": "webpack --config webpack.config.js --mode production --env TARGET=app-a",
    "build:cfg:b": "webpack --config webpack.config.js --mode production --env TARGET=app-b",
    "build:cfg:c": "webpack --config webpack.config.js --mode production --env TARGET=app-c"
  }
}

9. 运行与构建

开发:

npm run start:a
npm run start:b
npm run start:c

构建:

npm run build:all

(可选)启用图片压缩:

USE_IMG_MIN=true npm run build:all

注意(Windows):

  • PowerShell: $env:USE_IMG_MIN="true"; npm run build:all
  • CMD: set USE_IMG_MIN=true && npm run build:all

9.1 完整的 package.json 示例(React 19)

{
  "name": "webpack-build",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start:a": "webpack serve --config build/webpack.dev.js --env TARGET=app-a",
    "start:b": "webpack serve --config build/webpack.dev.js --env TARGET=app-b",
    "start:c": "webpack serve --config build/webpack.dev.js --env TARGET=app-c",
    "build:a": "webpack --config build/webpack.prod.js --env TARGET=app-a",
    "build:b": "webpack --config build/webpack.prod.js --env TARGET=app-b",
    "build:c": "webpack --config build/webpack.prod.js --env TARGET=app-c",
    "build:all": "webpack --config build/webpack.prod.js",
    "start:cfg:a": "webpack serve --config webpack.config.js --mode development --env TARGET=app-a",
    "start:cfg:b": "webpack serve --config webpack.config.js --mode development --env TARGET=app-b",
    "start:cfg:c": "webpack serve --config webpack.config.js --mode development --env TARGET=app-c",
    "build:cfg:a": "webpack --config webpack.config.js --mode production --env TARGET=app-a",
    "build:cfg:b": "webpack --config webpack.config.js --mode production --env TARGET=app-b",
    "build:cfg:c": "webpack --config webpack.config.js --mode production --env TARGET=app-c",
    "lint": "eslint \"src/**/*.{ts,tsx,js,jsx}\" --max-warnings=0",
    "lint:fix": "eslint \"src/**/*.{ts,tsx,js,jsx}\" --fix",
    "format": "prettier --write \"**/*.{ts,tsx,js,jsx,css,md,json}\""
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "antd": "^5.27.2",
    "react": "^19.1.1",
    "react-dom": "^19.1.1",
    "react-router": "^6.30.1",
    "react-router-dom": "^6.30.1"
  },
  "devDependencies": {
    "@babel/core": "^7.28.3",
    "@babel/preset-env": "^7.28.3",
    "@babel/preset-react": "^7.27.1",
    "@babel/preset-typescript": "^7.27.1",
    "@pmmmwh/react-refresh-webpack-plugin": "^0.6.1",
    "@squoosh/lib": "^0.5.3",
    "@types/node": "^24.3.0",
    "@types/react": "^19.1.12",
    "@types/react-dom": "^19.1.9",
    "@typescript-eslint/eslint-plugin": "^8.41.0",
    "@typescript-eslint/parser": "^8.41.0",
    "babel-loader": "^9.1.3",
    "css-loader": "^7.1.2",
    "eslint": "^8.57.0",
    "eslint-config-prettier": "^10.1.8",
    "eslint-import-resolver-typescript": "^4.4.4",
    "eslint-plugin-import": "^2.32.0",
    "eslint-plugin-prettier": "^5.5.4",
    "eslint-plugin-react": "^7.37.5",
    "eslint-plugin-react-hooks": "^5.2.0",
    "fork-ts-checker-webpack-plugin": "^9.1.0",
    "html-webpack-plugin": "^5.6.4",
    "image-minimizer-webpack-plugin": "^4.1.4",
    "prettier": "^3.6.2",
    "react-refresh": "^0.17.0",
    "style-loader": "^4.0.0",
    "typescript": "^5.9.2",
    "webpack": "^5.101.3",
    "webpack-cli": "^6.0.1",
    "webpack-dev-server": "^5.2.2",
    "webpack-merge": "^6.0.1"
  },
  "engines": {
    "node": ">=18"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not op_mini all"
  ]
}

10. 部署方案

10.1 Nginx(多子路径部署)

server {
  listen 80;
  server_name your.domain.com;

  location /app-a/ {
    alias /var/www/dist/app-a/;
    try_files $uri /index.html;
  }

  location /app-b/ {
    alias /var/www/dist/app-b/;
    try_files $uri /index.html;
  }

  location /app-c/ {
    alias /var/www/dist/app-c/;
    try_files $uri /index.html;
  }
}

React Router 若部署在子路径,入口需:

<BrowserRouter basename="/app-a">
  <App />
</BrowserRouter>

10.2 Docker(Nginx 镜像)

Dockerfile

FROM nginx:1.25-alpine
# 拷贝构建产物(先执行 npm run build:all)
COPY dist/app-a /usr/share/nginx/html/app-a
COPY dist/app-b /usr/share/nginx/html/app-b
COPY dist/app-c /usr/share/nginx/html/app-c
# 拷贝自定义 nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

nginx.conf

server {
  listen 80;
  server_name localhost;

  location /app-a/ {
    root   /usr/share/nginx/html;
    index  app-a/index.html;
    try_files $uri /app-a/index.html;
  }

  location /app-b/ {
    root   /usr/share/nginx/html;
    index  app-b/index.html;
    try_files $uri /app-b/index.html;
  }

  location /app-c/ {
    root   /usr/share/nginx/html;
    index  app-c/index.html;
    try_files $uri /app-c/index.html;
  }
}

构建并运行:

docker build -t multi-react-nginx .
docker run -p 8080:80 multi-react-nginx
# 访问: http://localhost:8080/app-a/  等

11. CI/CD 示例

11.1 GitHub Actions

.github/workflows/build.yml

name: build
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18
      - run: npm ci
      - run: npm run lint
      - run: npm run build:all
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist

11.2 GitLab CI

.gitlab-ci.yml

stages:
  - install
  - lint
  - build

cache:
  paths:
    - node_modules/

install:
  stage: install
  image: node:18
  script:
    - npm ci

lint:
  stage: lint
  image: node:18
  script:
    - npm run lint

build:
  stage: build
  image: node:18
  script:
    - npm run build:all
  artifacts:
    paths:
      - dist/

11.3 Docker 构建 + 推送(GitHub Actions 示例)

.github/workflows/docker.yml

name: docker
on:
  push:
    tags: [ 'v*.*.*' ]

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          file: ./Dockerfile
          push: true
          tags: ghcr.io/${{ github.repository }}:latest

12. 质量与格式化

  • 规范检查:npm run lint
  • 自动修复:npm run lint:fix
  • 统一格式:npm run format

13. 常见问题

  • BrowserslistError:确保 package.jsonbrowserslist 为数组
  • 动态 import 报错 TS1323:tsconfig.json 中设置 module: "ESNext"
  • Router 版本:使用 react-router-dom@^6
  • 图片压缩失败:默认关闭;需启用时 USE_IMG_MIN=true npm run build:all

14. 进一步优化(可选)

  • 页面/组件更细懒加载与按需引入 AntD
  • 构建缓存:在 dev/prod 配置加入 cache: { type: 'filesystem' }
  • 更严格 TS:开启 noUncheckedIndexedAccess
  • 提交前校验:Husky + lint-staged
  • 监控:接入 Web Vitals 与错误上报(Sentry)

15. 快速复现步骤清单

  1. 克隆或创建目录,执行第 2/3 步
  2. 按第 4/5 步创建目录与配置
  3. 粘贴第 7 步源码(共享与三子项目)
  4. 运行:npm run start:a/start:b/start:c
  5. 构建:npm run build:all(或单子项目构建)
  6. 部署:按第 10 步 Nginx 或 Docker
  7. CI/CD:按第 11 步复制对应平台脚本

至此,你已获得一个可用、可扩展、可部署的多子项目 React 工程。