1. 引言
本文以 umi 为技术底座,完整讲解一个后台管理项目的实战落地过程,覆盖从基本工程搭建、登录注册、菜单权限配置、页面跳转、用户操作埋点,到开发调试与生产构建的完整链路。通过本文,你可以掌握一套可复用的 umi 后台管理项目开发范式。
2. 基本工程搭建
2.1 初始化项目
umi 提供了官方脚手架,可以快速生成一个带路由、布局和权限模型的基础工程。推荐使用 pnpm 作为包管理器,初始化命令如下:
# 使用 pnpm 创建 umi 项目 pnpm create umi 选择需要的功能:antd、dva、qiankun、pro-layout 等 进入项目目录 cd my-admin 安装依赖 pnpm install 启动开发服务 pnpm start初始化完成后,项目会生成src、config、public等目录,其中config/config.ts是核心配置文件,负责路由、插件和构建相关的全局配置。
2.2 目录结构规划
一个清晰的后台管理项目目录结构,可以显著降低后续维护成本。推荐按业务模块划分:
src/ ├── pages/ # 页面组件,按路由组织 ├── components/ # 通用业务组件 ├── layouts/ # 全局布局 ├── services/ # 接口请求层 ├── models/ # 全局状态管理 ├── utils/ # 工具函数 ├── constants/ # 常量定义 └── access.ts # 权限定义2.3 配置路由与布局
umi 采用约定式路由与配置式路由相结合的方式。后台管理项目通常使用配置式路由,以便统一管理菜单和权限。在config/routes.ts中定义路由结构:
export default [ { path: '/user', layout: false, routes: [ { path: '/user/login', component: './User/Login' }, { path: '/user/register', component: './User/Register' }, ], }, { path: '/', component: '@/layouts/BasicLayout', routes: [ { path: '/dashboard', name: '工作台', icon: 'DashboardOutlined', component: './Dashboard' }, { path: '/system', name: '系统管理', icon: 'SettingOutlined' }, ], }, ];3. 登录注册
3.1 登录页实现
登录页是后台管理系统的入口。使用 antd 的Form组件实现表单校验,配合ProForm可以进一步简化开发。核心逻辑包括:表单校验、调用登录接口、存储 Token、跳转首页。
import { ProForm, ProFormText } from '@ant-design/pro-components'; import { history } from 'umi'; import { login } from '@/services/auth'; const LoginPage = () => { const handleSubmit = async (values: { username: string; password: string }) => { const { token } = await login(values); localStorage.setItem('token', token); history.push('/dashboard'); }; return ( <ProForm onFinish={handleSubmit}> <ProFormText name="username" label="用户名" rules={[{ required: true }]} /> <ProFormText.Password name="password" label="密码" rules={[{ required: true }]} /> </ProForm> ); }; export default LoginPage;3.2 注册页与验证码
注册页通常需要额外的校验逻辑,例如确认密码、图形验证码或短信验证码。建议将验证码逻辑封装为独立组件,便于复用:
const CaptchaInput = ({ onSend }: { onSend: (phone: string) => Promise<void> }) => { const [countdown, setCountdown] = useState(0); const handleSend = async (phone: string) => { await onSend(phone); setCountdown(60); const timer = setInterval(() => { setCountdown((prev) => { if (prev <= 1) { clearInterval(timer); return 0; } return prev - 1; }); }, 1000); }; return ( <Space> <Input placeholder="手机号" /> <Button disabled={countdown > 0} onClick={() => handleSend('')}> {countdown > 0 ? ${countdown}s 后重发 : '发送验证码'} </Button> </Space> ); };3.3 Token 管理与请求拦截
登录成功后,需要统一管理 Token。推荐在request.ts中封装请求拦截器,自动携带 Token,并统一处理 401 未授权场景:
import { request } from 'umi'; import { history } from 'umi'; const authRequest = request.extend({ requestInterceptors: [(url, options) => { const token = localStorage.getItem('token'); if (token) { options.headers = { ...options.headers, Authorization: Bearer ${token} }; } return { url, options }; }], responseInterceptors: [(response) => { if (response.status === 401) { localStorage.removeItem('token'); history.push('/user/login'); } return response; }], }); export default authRequest;4. 菜单权限配置
4.1 权限模型设计
后台管理系统的权限通常分为菜单权限和操作权限两层。菜单权限决定用户能看到哪些页面,操作权限决定用户能执行哪些按钮操作。推荐使用基于角色的权限模型(RBAC):
- 用户:系统使用者,可关联一个或多个角色。
- 角色:权限的集合,例如管理员、运营、访客。
- 菜单/操作:具体的页面入口或按钮动作。
4.2 动态路由与菜单生成
umi 支持通过access.ts定义权限规则,并结合initialState实现动态菜单。登录后从接口获取用户权限列表,动态生成菜单:
// access.ts export default function access(initialState: { currentUser?: API.CurrentUser }) { const { currentUser } = initialState || {}; return { canAdmin: currentUser && currentUser.roles.includes('admin'), canEdit: currentUser && currentUser.permissions.includes('edit'), }; }// 动态菜单配置 const menuData = [ { path: '/dashboard', name: '工作台', icon: 'DashboardOutlined' }, { path: '/system/user', name: '用户管理', icon: 'UserOutlined', access: 'canAdmin' }, { path: '/system/role', name: '角色管理', icon: 'TeamOutlined', access: 'canAdmin' }, { path: '/system/menu', name: '菜单管理', icon: 'MenuOutlined', access: 'canAdmin' }, ];4.3 按钮级权限控制
除了菜单权限,后台管理还经常需要控制页面内的按钮是否可见。可以通过自定义 Hook 或高阶组件实现:
import { useAccess } from 'umi'; const UserTable = () => { const access = useAccess(); return ( <Table columns={[ { title: '用户名', dataIndex: 'username' }, { title: '操作', render: (_, record) => ( <Space> {access.canEdit && <Button type="link">编辑</Button>} {access.canAdmin && <Button type="link" danger>删除</Button>} </Space> ) } ]} /> ); };5. 页面跳转
5.1 声明式跳转
umi 提供Link组件和history对象两种跳转方式。声明式跳转适合菜单、面包屑等静态场景:
import { Link } from 'umi'; const Breadcrumb = () => ( <Link to="/system/user">用户管理</Link> );5.2 编程式跳转
在表单提交、按钮点击等交互场景中,通常使用编程式跳转,并支持携带查询参数:
import { history } from 'umi'; // 跳转到详情页并携带 id history.push(/system/user/detail?id=${userId}); // 带 state 跳转,适合传递复杂对象 history.push('/system/user/detail', { userId, from: 'list' }); // 返回上一页 history.back();5.3 路由守卫与重定向
未登录用户访问受保护页面时,需要自动重定向到登录页。umi 的wrappers机制可以方便地实现路由守卫。下面给出一个更完整的守卫实现,支持免登录白名单、登录后重定向回原页面,以及全局路由鉴权配置:
// wrappers/auth.tsx import { Redirect, useLocation } from 'umi'; import { isLogin } from '@/utils/auth'; // 免登录白名单,这些页面无需登录即可访问 const WHITE_LIST = ['/user/login', '/user/register', '/user/forgot']; const AuthWrapper = ({ children }: { children: React.ReactNode }) => { const location = useLocation(); // 白名单页面直接放行 if (WHITE_LIST.includes(location.pathname)) { return <>{children}</>; } // 未登录则重定向到登录页,并携带来源路径,登录成功后跳回原页面 if (!isLogin()) { const redirect = encodeURIComponent(location.pathname + location.search); return <Redirect to={`/user/login?redirect=${redirect}`} />; } return <>{children}</>; }; export default AuthWrapper;// utils/auth.ts export const TOKEN_KEY = 'token'; export const isLogin = () => Boolean(localStorage.getItem(TOKEN_KEY)); export const getToken = () => localStorage.getItem(TOKEN_KEY); export const setToken = (token: string) => localStorage.setItem(TOKEN_KEY, token); export const clearToken = () => localStorage.removeItem(TOKEN_KEY);// 路由配置中应用守卫 { path: '/dashboard', component: './Dashboard', wrappers: ['@/wrappers/auth'], }登录成功后,根据redirect参数跳回原页面,实现免登录重定向闭环:
// 登录页提交成功后 const { token } = await login(values); setToken(token); const redirect = new URLSearchParams(location.search).get('redirect'); history.push(redirect || '/dashboard');6. 页面用户操作埋点
6.1 埋点方案设计
用户操作埋点用于采集用户在页面上的行为数据,为产品决策提供依据。常见的埋点类型包括:
- 页面浏览:PV/UV、停留时长、来源页面。
- 按钮点击:按钮名称、所在页面、点击次数。
- 表单提交:提交成功率、失败原因。
- 搜索行为:关键词、搜索结果数量。
6.2 统一埋点工具封装
建议封装统一的埋点工具函数,避免在每个页面重复写上报逻辑:
// utils/tracker.ts interface TrackEvent { event: string; page: string; params?: Record<string, unknown>; } export const track = ({ event, page, params }: TrackEvent) => { // 上报到埋点服务 fetch('/api/track', { method: 'POST', body: JSON.stringify({ event, page, params, timestamp: Date.now(), userId: localStorage.getItem('userId'), }), }); };6.3 页面浏览埋点
页面浏览埋点可以在路由切换时统一触发。umi 提供了onRouteChange配置,可以在路由变化时执行埋点逻辑:
// app.tsx import { track } from '@/utils/tracker'; export function onRouteChange({ location }: { location: { pathname: string } }) { track({ event: 'page_view', page: location.pathname, }); }6.4 按钮点击埋点
按钮点击埋点可以通过封装高阶组件或自定义 Hook 实现,减少业务代码侵入:
// hooks/useTrack.ts import { track } from '@/utils/tracker'; export const useTrack = (page: string) => { const trackClick = (event: string, params?: Record<string, unknown>) => { track({ event, page, params }); }; return { trackClick }; }; // 页面中使用 const { trackClick } = useTrack('/system/user'); const handleDelete = (id: number) => { trackClick('user_delete', { id }); // 执行删除逻辑 };7. 开发调试
7.1 本地开发代理
开发阶段前后端分离,需要配置代理解决跨域问题。在config/config.ts中配置proxy:
export default { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' }, }, }, };7.2 调试工具与日志
开发调试时,合理使用浏览器 DevTools 和日志输出可以快速定位问题。建议在utils/logger.ts中封装分级日志工具:
// utils/logger.ts const isDev = process.env.NODE_ENV === 'development'; export const logger = { info: (...args: unknown[]) => { if (isDev) console.info('[INFO]', ...args); }, warn: (...args: unknown[]) => { if (isDev) console.warn('[WARN]', ...args); }, error: (...args: unknown[]) => { console.error('[ERROR]', ...args); }, };7.3 状态调试
使用 dva 或 valtio 等状态管理方案时,可以借助 Redux DevTools 或自定义中间件查看状态变化。推荐在开发环境开启状态日志,方便追踪数据流:
// models/user.ts export default { namespace: 'user', state: { list: [], loading: false }, reducers: { save(state, { payload }) { logger.info('user/save', payload); return { ...state, ...payload }; }, }, };8. 开发生产构建
8.1 环境变量管理
不同环境(开发、测试、生产)通常需要不同的接口地址和配置。umi 支持通过.env文件管理环境变量:
# .env.development API_BASE_URL=http://localhost:8080/api .env.production API_BASE_URL=https://api.example.com/api// 代码中读取环境变量 const apiBase = process.env.API_BASE_URL;8.2 生产构建配置
生产构建需要关注代码压缩、资源分包、CDN 路径等。umi 默认集成了 webpack 优化,但部分场景需要手动调整:
export default { define: { 'process.env.API_BASE_URL': process.env.API_BASE_URL, }, hash: true, // 生成带 hash 的资源文件名 publicPath: process.env.NODE_ENV === 'production' ? 'https://cdn.example.com/' : '/', chunks: ['vendors', 'umi'], chainWebpack(config) { config.optimization.splitChunks({ cacheGroups: { vendors: { name: 'vendors', test: /[\\/]node_modules[\\/]/, priority: 10, chunks: 'all', }, }, }); }, };8.3 构建与部署
生产构建命令为pnpm build,构建产物输出到dist目录。部署时需要注意:
- 将
dist目录部署到 Nginx 或对象存储。 - 配置 SPA 路