一、环境准备与项目初始化
1.1 脚手架安装与项目创建
bash
复制
# 安装 Umi 脚手架
yarn global add umi
# 创建项目(推荐使用 pnpm 避免幽灵依赖)
pnpm create @umijs/umi-app agent-manage
# 进入项目目录
cd agent-manage
# 安装基础依赖
pnpm install
1.2 初始目录结构解析
bash
复制
├── config
│ └── config.ts # Umi 核心配置文件
├── mock # Mock 数据目录
├── src
│ ├── assets # 静态资源
│ ├── components # 公共组件
│ ├── layouts # 全局布局
│ ├── models # Dva 模型
│ ├── pages # 页面组件
│ └── utils # 工具类
└── .umirc.ts # Umi 运行时配置
1.3 首坑预警:Node 版本兼容问题
现象:安装依赖时出现 gyp ERR 错误
原因:Umi 4.x 需要 Node 16+ 环境
解决方案:
bash
复制
# 使用 nvm 管理 Node 版本
nvm install 16.14.0
nvm use 16.14.0
二、工程化配置实战
2.1 编辑器规范统一
.vscode/settings.json 配置:
json
复制
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"prettier.configPath": ".prettierrc"
}
2.2 代理配置与跨域处理
config/config.ts 核心配置:
typescript
复制
export default {
proxy: {
'/api': {
target: 'http://backend-service.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
// 解决 Cookie 丢失问题
onProxyRes: function (proxyRes) {
const cookies = proxyRes.headers['set-cookie'];
if (cookies) {
proxyRes.headers['set-cookie'] = cookies.map(cookie =>
cookie.replace(/; secure/gi, '').replace(/; SameSite=None/gi, '')
);
}
}
}
}
}
2.3 路径别名配置
tsconfig.json 配置:
json
复制
{
"compilerOptions": {
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
}
}
config/config.ts 同步配置:
typescript
复制
import { defineConfig } from 'umi';
export default defineConfig({
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils')
}
});
三、移动端适配方案
3.1 高清方案配置
安装 PostCSS 插件:
bash
复制
pnpm add postcss-pxtorem postcss-flexbugs-fixes -D
config/config.ts 配置:
typescript
复制
export default {
extraPostCSSPlugins: [
require('postcss-flexbugs-fixes'),
require('postcss-pxtorem')({
rootValue: 75, // 750 设计稿对应 75
propList: ['*'],
selectorBlackList: ['am-'] // 排除 antd-mobile 组件
})
]
}
3.2 动态 REM 方案
src/utils/rem.js:
javascript
复制
const baseSize = 75; // 与 postcss 配置一致
function setRem() {
const scale = document.documentElement.clientWidth / 750;
document.documentElement.style.fontSize =
baseSize * Math.min(scale, 2) + 'px';
}
window.addEventListener('resize', setRem);
setRem();
四、请求层封装实战
4.1 umi-request 二次封装
src/utils/request.ts:
typescript
复制
import { extend } from 'umi-request';
const request = extend({
prefix: '/api',
timeout: 30000,
errorHandler: (error) => {
console.error('Request Error:', error);
throw error;
}
});
// 请求拦截器
request.interceptors.request.use((url, options) => {
const token = localStorage.getItem('ACCESS_TOKEN');
return {
url,
options: {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
};
});
// 响应拦截器
request.interceptors.response.use(async (response) => {
const data = await response.clone().json();
if (data.code !== 200) {
throw new Error(data.message || 'Request Error');
}
return response;
});
export default request;
4.2 业务接口组织
src/services/agent.ts:
typescript
复制
import request from '@/utils/request';
export async function fetchAgentList(params: API.ListParams) {
return request<API.Response<API.AgentItem[]>>('/agent/list', {
method: 'POST',
data: params
});
}
export async function deleteAgent(id: string) {
return request(`/agent/${id}`, { method: 'DELETE' });
}
五、复杂列表页开发
5.1 Class 组件实现
tsx
复制
import { PullToRefresh, List, InfiniteScroll } from 'antd-mobile';
class AgentList extends React.Component {
state = {
data: [],
page: 1,
hasMore: true
};
async componentDidMount() {
await this.loadData();
}
loadData = async () => {
const { page, data } = this.state;
const res = await fetchAgentList({ page });
this.setState({
data: data.concat(res.data.list),
hasMore: res.data.hasMore
});
};
render() {
return (
<PullToRefresh onRefresh={this.handleRefresh}>
<List>
{this.state.data.map(item => (
<List.Item key={item.id}>{item.name}</List.Item>
))}
</List>
<InfiniteScroll
loadMore={this.loadData}
hasMore={this.state.hasMore}
/>
</PullToRefresh>
);
}
}
5.2 Hooks 重构方案
tsx
复制
import { useState, useEffect } from 'react';
function AgentList() {
const [data, setData] = useState<API.AgentItem[]>([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const loadData = async (isRefresh = false) => {
const currentPage = isRefresh ? 1 : page;
try {
const res = await fetchAgentList({ page: currentPage });
setData(prev =>
isRefresh ? res.data.list : [...prev, ...res.data.list]
);
setHasMore(res.data.hasMore);
setPage(currentPage + 1);
} catch (error) {
console.error('加载失败:', error);
}
};
useEffect(() => {
loadData(true);
}, []);
return (
<PullToRefresh onRefresh={() => loadData(true)}>
<List>
{data.map(item => (
<List.Item key={item.id}>{item.name}</List.Item>
))}
</List>
<InfiniteScroll
loadMore={() => loadData()}
hasMore={hasMore}
/>
</PullToRefresh>
);
}
5.3 性能优化技巧
tsx
复制
// 使用 React.memo 优化列表项
const MemoListItem = React.memo(({ item }) => (
<List.Item>
<Avatar src={item.avatar} />
<span>{item.name}</span>
</List.Item>
));
// 虚拟滚动优化(针对长列表)
import { VirtualList } from 'antd-mobile';
function LongList() {
return (
<VirtualList
data={data}
itemHeight={80}
renderItem={(item, index) => (
<MemoListItem item={item} />
)}
/>
);
}
六、环境变量与多环境配置
6.1 环境区分方案
.umirc.prod.ts 生产环境配置:
typescript
复制
import { defineConfig } from 'umi';
export default defineConfig({
define: {
API_BASE: 'https://prod-api.example.com'
},
// 开启压缩
jsMinifier: 'terser',
// 关闭 sourcemap
devtool: false
});
6.2 动态加载配置
src/utils/env.ts:
typescript
复制
export const getEnvConfig = () => {
switch(process.env.NODE_ENV) {
case 'development':
return { API_BASE: '/api' };
case 'production':
return { API_BASE: 'https://prod-api.example.com' };
default:
return { API_BASE: window.location.origin };
}
};
七、典型踩坑记录
7.1 样式污染问题
现象:antd-mobile 组件样式被全局覆盖
解决方案:
typescript
复制
// config/config.ts
export default {
mobile: {
// 关闭移动端自适应
adaptive: false,
// 开启 CSS Modules
cssModules: true
}
}
7.2 页面刷新白屏
现象:路由跳转后刷新页面出现 404
原因:History 路由模式需要服务端支持
解决方案:
typescript
复制
export default {
history: {
type: 'hash' // 改为 hash 路由
}
}
7.3 图片加载失败
现象:生产环境图片路径错误
解决方案:
typescript
复制
// 使用 require 引入本地资源
<img src={require('@/assets/logo.png')} />
// 动态拼接 CDN 地址
const getImageUrl = (path) =>
`${process.env.IMAGE_CDN}${path}`;
八、项目优化实践
8.1 构建速度优化
typescript
复制
// config/config.ts
export default {
mfsu: {
strategy: 'eager', // 开启 MFSU
},
// 按需编译
extraBabelPresets: [
['babel-preset-react-app', { runtime: 'automatic' }]
]
}
8.2 首屏加载优化
typescript
复制
// 动态加载组件
import dynamic from 'umi/dynamic';
const Chart = dynamic({
loader: () => import('@/components/Chart'),
loading: () => <Loading />
});
// 预加载关键资源
<link rel="preload" href="/fonts/iconfont.woff2" as="font" />
九、项目部署指南
9.1 Docker 部署配置
Dockerfile:
dockerfile
复制
FROM node:16-alpine as builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install
COPY . .
RUN pnpm build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
9.2 Nginx 配置模板
nginx.conf:
nginx
复制
server {
listen 80;
gzip on;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend-service;
proxy_set_header Host $host;
}
}
十、扩展功能展望
10.1 状态管理集成
bash
复制
# 安装 Dva
pnpm add @umijs/plugin-dva
src/models/agent.ts:
typescript
复制
export default {
namespace: 'agent',
state: {
list: [],
},
effects: {
*fetchList({ payload }, { call, put }) {
const res = yield call(fetchAgentList, payload);
yield put({ type: 'save', payload: res.data });
}
},
reducers: {
save(state, { payload }) {
return { ...state, list: payload };
}
}
};
10.2 微前端集成
typescript
复制
// config/qiankun.ts
export default {
apps: [
{
name: 'subApp',
entry: '//localhost:7100',
base: '/sub',
routes: ['/sub/*']
}
]
}
通过以上 10 个章节的系统讲解,我们完成了从零开始构建 Umi+React 企业级项目的完整流程。本文不仅覆盖了基础配置、核心功能开发,还深入探讨了性能优化、异常处理等高级主题。建议开发者在实践中持续关注 Umi 官方更新动态,结合业务需求灵活调整架构方案。