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 工程。

相关推荐
AI大模型-小华18 分钟前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子3 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅3 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni3 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学4 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端
赵庆明老师5 小时前
Vben精讲:21-详解web-antd:tsconfig.json
前端·json·vim
wc885 小时前
微软EDGE浏览器功能学习
前端·学习·edge
Csvn5 小时前
🎯 原生 `<dialog>` 元素:终于可以扔掉一半的自定义弹窗组件了?
前端
Csvn5 小时前
🎨 CSS @layer:用「层叠优先级」终结样式打架的世纪难题
前端