本文手把手带你从零搭建一个包含 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.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 工程。