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

本文手把手带你从零搭建一个包含 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 均可

检查版本:

bash 复制代码
node -v
npm -v

2. 初始化项目

bash 复制代码
mkdir -p "webpack-build"
cd "webpack-build"
npm init -y

3. 安装依赖

运行时依赖:

bash 复制代码
npm i react@^19 react-dom@^19 react-router-dom@^6 antd@^5

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

开发依赖:

bash 复制代码
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

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

bash 复制代码
npm i -D image-minimizer-webpack-plugin @squoosh/lib

4. 创建目录结构

bash 复制代码
mkdir -p public src/shared src/app-a/pages src/app-b/pages src/app-c/pages build

期望结构:

text 复制代码
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

json 复制代码
{
  "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 与路径别名)

json 复制代码
{
  "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

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

js 复制代码
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

json 复制代码
{ "singleQuote": true, "trailingComma": "all", "printWidth": 100, "semi": true, "arrowParens": "always" }

.eslintignore.prettierignore

text 复制代码
dist/
node_modules/

.gitignore

gitignore 复制代码
node_modules/
dist/
.DS_Store
npm-debug.log*
yarn-error.log*
pnpm-debug.log*

package.json 中 browserslist(示例)

json 复制代码
{
  "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(通用配置)

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(开发配置)

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 }
js 复制代码
// 生产环境优化:细粒度拆包、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(根配置)

js 复制代码
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');
// 生产环境更细粒度拆包(图片压缩请参考 build/webpack.prod.js 的可选配置)

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 配置
// 说明:
// - dev 模式:启用 cheap-module-source-map 与 React Refresh(babel 插件 + plugin)
// - prod 模式:启用 source-map 与细粒度拆包(react/antd/vendors/runtime)
const makeConfig = ({ name, title, entry, port }, isDev) => ({
  name,
  mode: isDev ? 'development' : 'production',
  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
  },
  devtool: isDev ? 'cheap-module-source-map' : 'source-map',
  // 放宽/屏蔽性能告警
  performance: { hints: false },
  module: {
    rules: [
      {
        // TS/JS 交给 Babel,开发态注入 React Refresh 的 babel 插件
        test: /(t|j)sx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            // 在开发环境注入 React Refresh 的 Babel 插件,实现"保留状态"的热更新
            plugins: [
              isDev && require.resolve('react-refresh/babel')
            ].filter(Boolean)
          }
        }
      },
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { 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: 'public/index.html', title, chunks: [name] }),
    // TypeScript 类型检查在独立进程进行,避免阻塞编译
    new ForkTsCheckerWebpackPlugin(),
    // 构建进度条
    new webpack.ProgressPlugin(),
    // 仅开发环境启用 React Refresh 插件
    ...(isDev ? [new ReactRefreshWebpackPlugin()] : [])
  ],
  // 生产环境的通用优化:抽取公共依赖、分离运行时代码,提升缓存命中率
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // 将 react、antd 拆分为独立 vendor,便于浏览器长缓存
        react: { test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/, name: 'react-vendor', chunks: 'all', priority: 30 },
        antd: { test: /[\\/]node_modules[\\/]antd[\\/]/, name: 'antd-vendor', chunks: 'all', priority: 20 },
        defaultVendors: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all' }
      }
    },
    runtimeChunk: 'single'
  },
  devServer: {
    // 开发环境由内存提供产物,静态资源来自 public 目录
    static: { directory: path.resolve(__dirname, 'public') },
    compress: true,
    port,
    open: true,
    hot: true,
    // 前端路由深链接 404 回退到 index.html
    historyApiFallback: true
  },
  stats: 'minimal'
});

module.exports = (env = {}, argv = {}) => {
  const isDev = argv.mode === 'development';
  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);
  }
  return apps.map(meta => makeConfig(meta, isDev));
};

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

共享样式 src/shared/global.css

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

共享组件 src/shared/Button.tsx

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

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 类似;下文附上三子项目页面文件示例,按需新建)

tsx 复制代码
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

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

src/app-a/pages/About.tsx

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

src/app-b/pages/Home.tsx

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

src/app-b/pages/Docs.tsx

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

src/app-c/pages/Dashboard.tsx

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

src/app-c/pages/Settings.tsx

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

8. NPM Scripts(两套)

推荐(分环境配置):

json 复制代码
{
  "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}\""
  }
}

根配置(可选):

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. 运行与构建

开发:

bash 复制代码
npm run start:a
npm run start:b
npm run start:c

构建:

bash 复制代码
npm run build:all

(可选)启用图片压缩:

bash 复制代码
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)

json 复制代码
{
  "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(多子路径部署)

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 若部署在子路径,入口需:

tsx 复制代码
<BrowserRouter basename="/app-a">
  <App />
</BrowserRouter>

10.2 Docker(Nginx 镜像)

Dockerfile

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

nginx 复制代码
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;
  }
}

构建并运行:

bash 复制代码
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

yaml 复制代码
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

yaml 复制代码
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

yaml 复制代码
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 工程。

相关推荐
路修远i13 小时前
项目中JSSDK封装方案
前端·架构
一蓑烟雨,一任平生13 小时前
h5实现内嵌微信小程序&支付宝 --截图保存海报分享功能
开发语言·前端·javascript
他们都不看好你,偏偏你最不争气13 小时前
【iOS】MVC架构
前端·ios·mvc·objective-c·面向对象
苏纪云13 小时前
Ajax笔记(下)
前端·javascript·笔记·ajax
三十_A14 小时前
【NestJS】HTTP 接口传参的 5 种方式(含前端调用与后端接收)
前端·网络协议·http
几个高兴14 小时前
ES6和CommonJS模块区别
前端
渊不语14 小时前
Webpack 深入学习指南
前端·webpack
北京_宏哥14 小时前
《刚刚问世》系列初窥篇-Java+Playwright自动化测试-38-屏幕截图利器-上篇(详细教程)
java·前端·面试
子兮曰14 小时前
🚀别再被JSON.parse坑了!这个深度克隆方案解决了我3年的前端痛点
前端·javascript·全栈