系统架构
理解纯前端商业版时,最重要的不是记住目录名称,而是理解请求、状态、路由和页面之间如何协作。本章从运行时流程出发,说明项目的主要分层和关键边界。
总体分层
┌──────────────────────────────────────────────┐
│ 页面层 src/views │
│ 页面、搜索组件、弹窗、图表、业务交互 │
├──────────────────────────────────────────────┤
│ 复用层 src/components + src/hooks │
│ ArtTable、ArtSearchBar、useTable、useAuth │
├──────────────────────────────────────────────┤
│ 应用层 src/store + src/router + src/services │
│ 状态、路由与可选业务流程编排 │
├──────────────────────────────────────────────┤
│ 数据层 src/api + src/mock + src/utils/http │
│ 真实 HTTP、预置数据、统一响应与错误处理 │
├──────────────────────────────────────────────┤
│ 配置层 src/config + .env* + vite.config.ts │
│ 品牌、布局、主题、代理、构建和菜单来源 │
└──────────────────────────────────────────────┘页面层不应该知道 Token 如何附加,也不应该自行判断当前使用本地还是远程菜单。每层只处理自己的职责,后续替换后端接口时才能保持改动范围可控。
应用启动流程
入口位于 src/main.ts:
createApp(App)
-> initStore(app)
-> initRouter(app)
-> setupGlobDirectives(app)
-> setupErrorHandle(app)
-> setupElementPlusLocale()
-> app.use(i18n)
-> app.mount('#app')
-> 异步加载站点公共配置src/App.vue 在根组件中完成主题初始化、存储兼容检查、版本更新检查和页面标题同步。业务预置数据不需要在启动时集中加载,各页面会在进入时按需调用 src/mock。
这种启动方式避免了“为了进入登录页就加载全部业务数据”,也让站点配置失败不会阻塞应用挂载。
登录与会话恢复
认证 HTTP 接口统一定义在 src/api/auth.ts。当前源码中的 src/services/auth.ts 只负责登录结果校验和会话恢复等流程语义,内部仍然调用 API 模块,不承担接口地址定义。
首次登录
登录页提交账号密码
-> authService.signIn()
-> POST /api/v1/auth/signin
-> 校验 accessToken 非空
-> userStore 保存 Token
-> 进入目标页面
-> 路由守卫请求 GET /api/v1/user/info
-> 保存用户、角色和按钮权限
-> 初始化菜单与动态路由页面刷新
Pinia 恢复持久化 accessToken
-> authService.restoreSession()
-> 标记登录态可继续恢复
-> GET /api/v1/user/info 校验 Token
-> 使用最新用户信息重建菜单与动态路由项目只持久化 Access Token,不长期持久化用户资料、角色和按钮权限。这样刷新页面时会重新获取服务端的最新权限,避免账号权限变更后浏览器仍长期使用旧数据。
当前没有 Refresh Token 流程
restoreSession() 只恢复本地 Access Token,不会刷新令牌。Token 过期时,请求层收到 401 后会清理登录态并返回登录页。如果客户后端需要双令牌协议,应在 src/api/auth.ts 中增加刷新接口,并在 src/utils/http 与用户状态中实现刷新和清理流程。
接口字段、401 清理和扩展边界见登录与会话。
HTTP 请求链路
统一请求入口位于 src/utils/http/index.ts:
src/api/*.ts
-> request.get/post/put/patch/del
-> Axios 请求拦截器
-> 添加 Authorization: Bearer <accessToken>
-> JSON 请求体序列化
-> 后端响应 { code, msg, data }
-> 响应拦截器
-> code === 200 返回 data
-> code === 401 统一登出
-> 其他错误转换为 HttpError默认超时时间为 15 秒,当前自动重试次数为 0。不要在页面中假设失败请求会自动重试,也不要在多个页面重复显示相同错误提示。
菜单与动态路由
菜单初始化由 src/router/guards/beforeEach.ts 触发,核心流程如下:
进入受保护页面
-> 获取当前用户
-> MenuProcessor.getMenuList()
-> local: 前端路由按 roles 过滤
-> remote: 请求 /api/v1/user/menus
-> 校验菜单结构与路径
-> 规范化嵌套路由路径和默认 redirect
-> RouteRegistry.register()
-> 菜单写入 menuStore
-> 恢复原目标地址远程菜单采用失败关闭策略:路由名称重复、组件路径不安全、外链不是 HTTP(S)、权限字段格式错误等情况都会拒绝注册整棵路由树,并进入错误页,而不是只注册一半菜单。
权限模型
当前用户接口返回两个关键字段:
interface UserInfo {
roles: string[]
buttons: string[]
}roles用于本地菜单过滤、v-roles和hasRole。buttons用于v-auth和hasAuth。- 路由访问还会持续与当前菜单树校验,不能仅靠 Vue Router 已经匹配就放行。
- 后端仍必须对每个数据接口和写操作执行真实鉴权。
完整配置见路由与权限。
数据来源架构
业务页面显式选择数据来源:
// 预置数据阶段
import { getDemoUserList } from '@/mock/system/organization'
// 接入后端后
import { fetchUserList } from '@/api/customer/user'项目没有自动 Provider、Local Adapter 或“接口失败后退回 Mock”的机制。这样设计可以避免生产接口异常被前端预置数据掩盖。
src/mock 中的读取函数通常返回深拷贝,并通过分页和筛选工具模拟异步查询;写操作调用统一提示,明确不持久化。详细约束见本地数据模式。
状态管理
Pinia 入口位于 src/store/index.ts,持久化键由 StorageKeyManager 统一管理。主要 Store 包括:
| Store | 职责 | 持久化重点 |
|---|---|---|
user | Token、用户、语言、锁屏和搜索状态 | Token、语言及有限用户偏好 |
menu | 当前菜单树和首页路径 | 运行时菜单为主 |
setting | 布局、主题、圆角和界面开关 | 完整用户设置 |
worktab | 已打开标签和保活排除 | 工作区体验 |
table | 表格列偏好 | 用户表格设置 |
site-settings | 站点品牌与公共配置 | 本地站点设置 |
notification | 通知展示状态 | 当前前端会话能力 |
不要把 Pinia 持久化扩展成生产业务数据库。业务数据需要多用户一致性、审计或备份时,应由后端持久化。
各 Store 的职责、持久化键和账号隔离规则见状态管理。
关键设计原则
- 认证接口最小化:首次运行只依赖登录、退出和当前用户。
- 页面渐进迁移:Mock 与真实 API 可以按页面并存。
- 失败不伪装成功:未接后端的写操作必须明确说明边界。
- 前端权限不越权:前端负责体验,后端负责安全。
- 接口目录统一:所有 HTTP 接口通过
src/api暴露,页面再结合 Hook 和 Store 使用,避免 URL 与请求逻辑散落。
理解这些原则后,再阅读项目结构会更容易建立“需求到文件”的映射。
