Files
rxthinkcmf-wiki/RXThinkCMF_ThinkPhp8_NaiveVue/zh/architecture/frontend-layer.md
T
2026-10-10 19:17:02 +08:00

446 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 4.4 前端分层架构
:::tip 概述
前端采用 Vue 3 + TypeScript 组件化架构,分为 View(视图)→ API(接口)→ Store(状态)→ HTTP(请求)四层。辅以 Router(路由)、Directives(指令)、Hooks(组合式函数)、Utils(工具)等支撑模块。
:::
## 架构全景
```text
┌─────────────────────────────────────────────────────────────────────┐
│ 支撑模块 │
│ │
│ Router(路由) Directives(指令) Hooks(组合式函数) │
│ src/router/ src/directives/ src/hooks/ │
│ │
│ Utils(工具) Enums(枚举) Styles(样式) │
│ src/utils/ src/enums/ src/styles/ │
└──────────────────────────────┬──────────────────────────────────────┘
│ 被主干层引用
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 主干链路 │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ View(视图层) │ │
│ │ 页面渲染、用户交互、调用 API 和 Store │ │
│ │ 文件:src/views/**/*.vue │ │
│ └──────────────────────────┬──────────────────────────────────┘ │
│ │ 调用 API 函数 │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ API(接口层) │ │
│ │ 封装后端 API 调用,定义请求参数和响应类型 │ │
│ │ 文件:src/api/**/*.ts │ │
│ └──────────────────────────┬──────────────────────────────────┘ │
│ │ 调用 http.request() │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ HTTP(请求层) │ │
│ │ Axios 封装:拦截器、Token 携带、错误处理 │ │
│ │ 文件:src/utils/http/axios/ │ │
│ └──────────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 后端 API(RESTful JSON) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Store(状态层) │ │
│ │ 全局状态管理:Token、用户信息、权限列表、动态路由 │ │
│ │ 文件:src/store/modules/*.ts │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
## 各层职责详解
### View 层(视图)
**文件**:`src/views/**/*.vue`
**职责**:页面渲染、用户交互、调用 API 获取数据
```text
src/views/
├── system/ 系统管理
│ ├── user/ 用户管理
│ │ ├── index.vue 列表页
│ │ └── edit.vue 编辑弹窗
│ ├── role/ 角色管理
│ └── menu/ 菜单管理
├── content/ 内容管理
│ ├── article/ 文章管理
│ └── category/ 分类管理
├── login.vue 登录页
├── dashboard/ 仪表盘
└── ...
```
**典型页面结构:**
```vue
<template>
<n-card :bordered="false" class="pt-3 mb-3 proCard">
<!-- 搜索区(BasicForm 驱动) -->
<BasicForm @register="registerForm" @submit="handleSubmit" @reset="handleReset" />
</n-card>
<n-card :bordered="false" class="proCard">
<!-- 操作栏 + 数据表格(BasicTable 驱动,分页内置) -->
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="basicTableRef"
:actionColumn="actionColumn"
>
<template #tableTitle>
<n-space>
<n-button v-perm="'sys:example:add'" type="primary" @click="handleAdd">
新增
</n-button>
</n-space>
</template>
</BasicTable>
</n-card>
</template>
<script setup lang="ts">
import { h } from 'vue';
import { NButton } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form';
import { getExamplePage, exampleDelete } from '@/api/tool/example';
const [registerForm, { handleSubmit, handleReset }] = useForm({
schemas: [
{ field: 'name', label: '名称', component: 'NInput' },
],
});
const columns = [
{ title: 'ID', key: 'id', width: 80 },
{ title: '名称', key: 'name' },
{ title: '状态', key: 'statusText' },
];
const actionColumn = {
width: 200,
title: '操作',
key: 'action',
render(record) {
return h(TableAction, {
actions: [
{ label: '编辑', type: 'warning', onClick: () => handleEdit(record), auth: 'sys:example:update' },
{ label: '删除', type: 'error', onClick: () => handleDelete(record.id), auth: 'sys:example:delete' },
],
});
},
};
// 分页数据请求(BasicTable 内置分页)
const loadDataTable = async (params) => {
const res = await getExamplePage({ ...params });
return { records: res.records, total: res.total };
};
</script>
```
### API 层(接口)
**文件**:`src/api/**/*.ts`
**职责**:封装后端 API 调用,一个后端模块对应一个 API 文件
**命名规范:**
| 操作 | 函数名 | HTTP 方法 | 参数传递 |
|------|--------|----------|----------|
| 分页查询 | `get{Module}Page` | GET | `params` |
| 全量列表 | `get{Module}List` | GET | `params` |
| 详情 | `get{Module}Detail` | GET | URL 参数 |
| 新增 | `{module}Add` | POST | `data` |
| 修改 | `{module}Update` | PUT | `data` |
| 删除 | `{module}Delete` | DELETE | URL 参数 |
| 批量删除 | `{module}BatchDelete` | DELETE | `data` |
| 导入 | `{module}Import` | POST | FormData |
| 导出 | `{module}Export` | GET | `params` |
**完整示例:**
```typescript
// src/api/system/user.ts
import { http } from '@/utils/http/axios';
// 分页查询
export function getUserPage(params?) {
return http.request({ url: '/user/page', method: 'GET', params });
}
// 详情
export function getUserDetail(id) {
return http.request({ url: '/user/detail/' + id, method: 'GET' });
}
// 新增
export function userAdd(data: any) {
return http.request({ url: '/user/add', method: 'POST', data });
}
// 修改
export function userUpdate(data: any) {
return http.request({ url: '/user/update', method: 'PUT', data });
}
// 删除
export function userDelete(id) {
return http.request({ url: '/user/delete/' + id, method: 'DELETE' });
}
// 批量删除
export function userBatchDelete(data: any) {
return http.request({ url: '/user/batchDelete', method: 'DELETE', data });
}
```
### HTTP 层(请求)
**文件**:`src/utils/http/axios/`
**职责**:Axios 封装,自动处理 Token、响应码、错误提示
```text
src/utils/http/axios/
├── index.ts Axios 实例配置和拦截器
├── Axios.ts VAxios 类封装
├── axiosTransform.ts 数据转换接口
├── checkStatus.ts HTTP 状态码处理
├── helper.ts 辅助函数
└── types.ts 类型定义
```
**核心拦截器:**
```text
请求拦截器:
┌──────────────────────────────────────────┐
│ 1. 从 Store 获取 Token │
│ 2. 自动添加 Authorization: Bearer <token> │
└──────────────────────────────────────────┘
响应拦截器:
┌──────────────────────────────────────────┐
│ code === 0 → 成功,返回 data │
│ code === 401 → Token 过期,跳转登录页 │
│ code === 403 → 权限不足,显示错误提示 │
│ 其它 code → 业务失败,显示 msg │
└──────────────────────────────────────────┘
```
### Store 层(状态)
**文件**:`src/store/modules/*.ts`
**职责**:全局状态管理(Pinia)
| Store | 文件 | 管理的状态 |
|-------|------|-----------|
| userStore | `user.ts` | Token、用户信息、权限列表、登录/登出 |
| asyncRouteStore | `asyncRoute.ts` | 动态路由、keep-alive 缓存列表 |
| projectSettingStore | `projectSetting.ts` | 项目配置(主题、布局等) |
| tabsViewStore | `tabsView.ts` | 标签页导航状态 |
| designSettingStore | `designSetting.ts` | 主题设计配置 |
| ossConfigStore | `ossConfig.ts` | 云存储配置 |
| lockscreenStore | `lockscreen.ts` | 锁屏状态 |
**userStore 核心流程:**
```text
登录:
userStore.login(userInfo)
→ POST /login → 获取 access_token
→ Storage.set(ACCESS_TOKEN, token)
→ this.setToken(token)
获取用户信息:
userStore.GetInfo()
→ GET /index/getUserInfo
→ this.setPermissions(res.permissions)
→ this.setUserInfo(res)
登出:
userStore.logout()
→ POST /logout
→ 清除 Token、用户信息、权限列表
→ 跳转登录页
```
## 路由与动态菜单
### 路由架构
```text
静态路由(固定,src/router/)
├── /login 登录页
├── / 根路径,重定向到首页
├── /redirect/:path 重定向路由
└── /:path(.*)* 404 错误页
动态路由(从后端菜单生成)
├── /dashboard 仪表盘
├── /system/user 用户管理
├── /system/role 角色管理
├── /content/article 文章管理
└── ...
```
### 动态路由生成流程
```text
1. 用户登录 → 获取 Token → 存入 Store
2. 路由守卫(router.beforeEach)检测到 Token
3. 调用 GET /index/getMenus 获取菜单树
4. generator-routers.ts 将菜单树转换为路由表
5. router.addRoute() 动态注入路由
6. 渲染侧边栏菜单
```
### 布局系统
```text
┌─────────────────────────────────────────────────────────────┐
│ 顶部导航栏 │
│ [Logo] [面包屑] [搜索] [通知] [头像] │
├──────────┬──────────────────────────────────────────────────┤
│ │ │
│ 侧边栏 │ 内容区域 │
│ 菜单 │ │
│ │ ┌──────────────────────────────────────────┐ │
│ ──────── │ │ 标签页导航 │ │
│ 系统管理 │ ├──────────────────────────────────────────┤ │
│ ├ 用户 │ │ │ │
│ ├ 角色 │ │ 页面内容(keep-alive 缓存) │ │
│ └ 菜单 │ │ │ │
│ │ └──────────────────────────────────────────┘ │
└──────────┴──────────────────────────────────────────────────┘
```
## 权限控制
### 按钮级权限指令
```vue
<!-- v-perm:单权限 -->
<n-button v-perm="'sys:user:add'" type="primary">新增</n-button>
<!-- v-perms:多权限(满足任一即可) -->
<n-button v-perms="['sys:user:add', 'sys:user:update']">操作</n-button>
```
无权限时元素直接从 DOM 移除,而非仅隐藏。
:::tip 超级管理员
用户权限列表包含 `*:*:*` 时,表示超级管理员,拥有所有权限,`v-perm`/`v-perms` 指令始终放行。
:::
## 支撑模块
### Hooks(组合式函数)
| Hook | 文件 | 说明 |
|------|------|------|
| `usePermission` | `hooks/web/usePermission.ts` | 权限判断(hasPermission、hasSomePermission) |
| `useMessage` / `useDialog` | `hooks/web/useMessage.ts`、`useDialog.ts` | 消息提示 / 确认弹窗(离散 API,可在非组件上下文调用) |
| `useLoadingBar` | `hooks/web/useLoadingBar.ts` | 顶部加载条 |
| `useLockFn` | `utils/useLockFn.ts` | 请求级防重复提交 |
| `useForm` | `components/Form/src/hooks/useForm.ts` | 表单通用逻辑(注册、提交、重置、校验) |
| 表格逻辑 | `components/Table/src/hooks/*` | BasicTable 内部钩子(分页、数据源、搜索) |
### Directives(自定义指令)
| 指令 | 说明 |
|------|------|
| `v-perm` | 单权限控制 |
| `v-perms` | 多权限控制(满足任一) |
### Utils(工具函数)
| 工具 | 文件 | 说明 |
|------|------|------|
| HTTP | `utils/http/axios/` | Axios 封装 |
| Storage | `utils/Storage.ts` | localStorage 封装 |
| Dict | `utils/dict.ts` | 字典工具(从 Store 获取字典值) |
## 数据流向
### 完整请求链路
```text
用户操作(点击搜索按钮)
│
▼
View: fetchData()
│ 调用 API 函数
▼
API: getExamplePage(params)
│ 调用 http.request()
▼
HTTP: 请求拦截器添加 Token
│ 发送 GET /example/page?pageNo=1&pageSize=20
▼
后端: Controller → Logic → Model → Database
│
▼
HTTP: 响应拦截器判断 code
│ code === 0 → 返回 data
▼
API: 返回 data
│
▼
View: tableData.value = res.records; total.value = res.total
│
▼
Vue 响应式 → 表格自动刷新
```
### Store 数据流
```text
登录成功
│
▼
userStore.setToken(token) → 响应式更新
userStore.setUserInfo(info) → 响应式更新
userStore.setPermissions(perm) → 响应式更新
│
▼
其他组件通过 userStore.username / userStore.permissions 读取
│
▼
v-perm 指令读取 permissions 列表 → 控制元素显隐
```
## 前后端对应关系
| 后端 | 前端 | 说明 |
|------|------|------|
| `app/controller/UserController.php` | `src/api/system/user.ts` | API 函数映射 |
| `app/logic/UserLogic.php` | `src/views/system/user/` | 业务页面 |
| `app/model/User.php` | — | 前端不直接操作模型 |
| `app/validate/UserValidate.php` | — | 前端校验由表单组件处理 |
| `app/validate/*Validate.php` 的场景 | 表单 `rules` | 前端表单校验规则 |
| `route/app.php` | `src/api/**/*.ts` | 路由 ↔ API 函数 |
| 后端字典 `DictService` | `src/enums/` + 字典 Store | 枚举常量 |
| 后端 `#[Permission]` 注解 | `v-perm` 指令 | 权限码对应 |
## 层间通信规范
| 调用方向 | 是否允许 | 说明 |
|----------|---------|------|
| View → API | ✅ | 标准调用 |
| View → Store | ✅ | 读取/修改全局状态 |
| View → Hooks | ✅ | 复用逻辑 |
| API → HTTP | ✅ | 调用 http.request() |
| API → Store | ❌ | API 层不应操作状态 |
| Store → API | ✅ | Store actions 中调用 API |
| HTTP → Store | ⚠️ | 仅在拦截器中读取 Token |
| Hooks → API | ✅ | Hooks 中可调用 API |