本文手把手带你从零搭建一个包含 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.json中browserslist为数组 - 动态 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. 快速复现步骤清单
- 克隆或创建目录,执行第 2/3 步
- 按第 4/5 步创建目录与配置
- 粘贴第 7 步源码(共享与三子项目)
- 运行:
npm run start:a/start:b/start:c - 构建:
npm run build:all(或单子项目构建) - 部署:按第 10 步 Nginx 或 Docker
- CI/CD:按第 11 步复制对应平台脚本
至此,你已获得一个可用、可扩展、可部署的多子项目 React 工程。