umi后台管理项目实战:从工程搭建到生产构建

1. 引言

本文以 umi 为技术底座,完整讲解一个后台管理项目的实战落地过程,覆盖从基本工程搭建、登录注册、菜单权限配置、页面跳转、用户操作埋点,到开发调试与生产构建的完整链路。通过本文,你可以掌握一套可复用的 umi 后台管理项目开发范式。

2. 基本工程搭建

2.1 初始化项目

umi 提供了官方脚手架,可以快速生成一个带路由、布局和权限模型的基础工程。推荐使用 pnpm 作为包管理器,初始化命令如下:

bash 复制代码
# 使用 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 目录结构规划

一个清晰的后台管理项目目录结构,可以显著降低后续维护成本。推荐按业务模块划分:

text 复制代码
src/
  ├── pages/          # 页面组件,按路由组织
  ├── components/     # 通用业务组件
  ├── layouts/        # 全局布局
  ├── services/       # 接口请求层
  ├── models/         # 全局状态管理
  ├── utils/          # 工具函数
  ├── constants/      # 常量定义
  └── access.ts       # 权限定义

2.3 配置路由与布局

umi 采用约定式路由与配置式路由相结合的方式。后台管理项目通常使用配置式路由,以便统一管理菜单和权限。在 config/routes.ts 中定义路由结构:

typescript 复制代码
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、跳转首页。

typescript 复制代码
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 注册页与验证码

注册页通常需要额外的校验逻辑,例如确认密码、图形验证码或短信验证码。建议将验证码逻辑封装为独立组件,便于复用:

typescript 复制代码
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 未授权场景:

typescript 复制代码
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 实现动态菜单。登录后从接口获取用户权限列表,动态生成菜单:

typescript 复制代码
// 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'),
  };
}
typescript 复制代码
// 动态菜单配置
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 或高阶组件实现:

typescript 复制代码
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 对象两种跳转方式。声明式跳转适合菜单、面包屑等静态场景:

tsx 复制代码
import { Link } from 'umi';
const Breadcrumb = () => (
<Link to="/system/user">用户管理</Link>
);

5.2 编程式跳转

在表单提交、按钮点击等交互场景中,通常使用编程式跳转,并支持携带查询参数:

typescript 复制代码
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 机制可以方便地实现路由守卫。下面给出一个更完整的守卫实现,支持免登录白名单、登录后重定向回原页面,以及全局路由鉴权配置:

typescript 复制代码
// 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;
typescript 复制代码
// 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);
typescript 复制代码
// 路由配置中应用守卫
{
  path: '/dashboard',
  component: './Dashboard',
  wrappers: ['@/wrappers/auth'],
}

登录成功后,根据 redirect 参数跳回原页面,实现免登录重定向闭环:

typescript 复制代码
// 登录页提交成功后
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 统一埋点工具封装

建议封装统一的埋点工具函数,避免在每个页面重复写上报逻辑:

typescript 复制代码
// 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 配置,可以在路由变化时执行埋点逻辑:

typescript 复制代码
// app.tsx
import { track } from '@/utils/tracker';
export function onRouteChange({ location }: { location: { pathname: string } }) {
track({
event: 'page_view',
page: location.pathname,
});
}

6.4 按钮点击埋点

按钮点击埋点可以通过封装高阶组件或自定义 Hook 实现,减少业务代码侵入:

typescript 复制代码
// 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:

typescript 复制代码
export default {
  proxy: {
    '/api': {
      target: 'http://localhost:8080',
      changeOrigin: true,
      pathRewrite: { '^/api': '' },
    },
  },
};

7.2 调试工具与日志

开发调试时,合理使用浏览器 DevTools 和日志输出可以快速定位问题。建议在 utils/logger.ts 中封装分级日志工具:

typescript 复制代码
// 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 或自定义中间件查看状态变化。推荐在开发环境开启状态日志,方便追踪数据流:

typescript 复制代码
// 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 文件管理环境变量:

text 复制代码
# .env.development
API_BASE_URL=http://localhost:8080/api
.env.production
API_BASE_URL=https://api.example.com/api
typescript 复制代码
// 代码中读取环境变量
const apiBase = process.env.API_BASE_URL;

8.2 生产构建配置

生产构建需要关注代码压缩、资源分包、CDN 路径等。umi 默认集成了 webpack 优化,但部分场景需要手动调整:

typescript 复制代码
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 路
相关推荐
haerapi1 小时前
仓库拣货路线不只靠最短边:S 形启发式的反例记录
android·开发语言·javascript
Joe_Wang51 小时前
【从0到1学习JVM · 14】堆内存各区域分工与Xms和Xmx设为一样的真相
java·jvm·学习·垃圾回收
派小心.1 小时前
App埋点排查:iOS冷启动事件漏报先看初始化还是上报
前端·数据分析
事圆则缓1 小时前
Flutter 状态管理框架对比(六):同一个购物车,四种方案怎么落地?
前端·javascript·flutter
.道阻且长.1 小时前
C++11 :右值引用的作用,引用折叠和完美转发
java·开发语言·c++
code2cat1 小时前
Java进阶篇之StampedLock:先读取,再校验,必要时退回读锁
java·开发语言
光依旧1 小时前
MCP实战手记(九):生产化MCP Server的6层安全防护
java·人工智能·spring boot·安全·网络安全·ai agent·mcp
peter67681 小时前
css揭秘-背景和边框
前端·css
500841 小时前
React Native for OpenHarmony 实战:三方库 react-native-torch 的鸿蒙化适配指南
javascript·react native·react.js·harmonyos