Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_EleVue/zh/architecture/frontend-layer.md
T
2026-10-08 17:50:29 +08:00

436 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(工具)等支撑模块。前端为本仓库 `ui/` 目录内的 EleVue 管理后台。
:::
## 架构全景
```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,{code, ok, msg, data}) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 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/ 登录页
├── dashboard/ 仪表盘
└── ...
```
**典型页面结构:**
```vue
<template>
<!-- 搜索区 -->
<el-form :model="searchForm" inline>
<el-form-item label="名称">
<el-input v-model="searchForm.name" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchData">搜索</el-button>
</el-form-item>
</el-form>
<!-- 操作栏 -->
<el-button v-perm="'sys:example:add'" type="primary" @click="handleAdd">新增</el-button>
<!-- 数据表格 -->
<el-table :data="tableData">
<el-table-column prop="id" label="ID" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="statusText" label="状态" />
<el-table-column label="操作">
<template #default="{ row }">
<el-button v-perm="'sys:example:update'" @click="handleEdit(row)">编辑</el-button>
<el-button v-perm="'sys:example:delete'" type="danger" @click="handleDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页(后端固定返回 records / total / current / size / pages) -->
<el-pagination v-model:current-page="pageNo" v-model:page-size="pageSize"
:total="total" @change="fetchData" />
</template>
<script setup lang="ts">
import { getExamplePage, exampleDelete } from '@/api/tool/example';
const tableData = ref([]);
const pageNo = ref(1);
const pageSize = ref(20);
const total = ref(0);
const fetchData = async () => {
const res = await getExamplePage({ pageNo: pageNo.value, pageSize: pageSize.value });
tableData.value = res.records;
total.value = res.total;
};
onMounted(() => fetchData());
</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**(带 body) | `data`(`{ids:[]}`) |
| 导入 | `{module}Import` | POST | FormData |
| 导出 | `{module}Export` | GET | `params` |
:::warning 批量删除方法为 DELETE
本项目 `batchDelete` 路由为 **DELETE**(请求体仍传 `ids` 数组——DELETE 方法携带 body),见 [3.7 路由注册](/zh/guide/route.html)。
:::
**完整示例:**
```typescript
// src/api/tool/example.ts
import { http } from '@/utils/http/axios';
// 分页查询
export function getExamplePage(params?) {
return http.request({ url: '/example/page', method: 'GET', params });
}
// 详情
export function getExampleDetail(id) {
return http.request({ url: '/example/detail/' + id, method: 'GET' });
}
// 新增
export function exampleAdd(data: any) {
return http.request({ url: '/example/add', method: 'POST', data });
}
// 修改
export function exampleUpdate(data: any) {
return http.request({ url: '/example/update', method: 'PUT', data });
}
// 删除
export function exampleDelete(id) {
return http.request({ url: '/example/delete/' + id, method: 'DELETE' });
}
// 批量删除(DELETE + 请求体 ids 数组)
export function exampleBatchDelete(data: any) {
return http.request({ url: '/example/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(含 key/code 验证码参数)
→ 获取 access_token
→ Storage.set(ACCESS_TOKEN, token)
获取用户信息:
userStore.GetInfo()
→ GET /index/getUser
→ setPermissions(res.permissions)
→ setUserInfo(res)
获取菜单(动态路由):
→ GET /index/getMenus
登出:
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:单权限 -->
<el-button v-perm="'sys:user:add'" type="primary">新增</el-button>
<!-- v-perms:多权限(满足任一即可) -->
<el-button v-perms="['sys:user:add', 'sys:user:update']">操作</el-button>
```
无权限时元素直接从 DOM 移除,而非仅隐藏。
:::tip 超级管理员
用户权限列表包含 `*:*:*` 时,表示超级管理员,拥有所有权限,`v-perm`/`v-perms` 指令始终放行。
:::
:::warning 前端权限不是安全防线
`v-perm` 只是体验层(隐藏按钮);真正的安全校验在后端 `#[Permission]` 注解 + `auth.jwt` 中间件(见 [3.8 权限节点配置](/zh/guide/permission.html))。
:::
## 支撑模块
### Hooks(组合式函数)
| Hook | 文件 | 说明 |
|------|------|------|
| `usePermission` | `hooks/web/usePermission.ts` | 权限判断(hasPermission、hasSomePermission) |
| `useTable` | `hooks/web/useTable.ts` | 表格通用逻辑(分页、搜索、刷新) |
| `useForm` | `hooks/web/useForm.ts` | 表单通用逻辑(提交、重置、校验) |
### 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 → Service → Model → Database
│(响应 {code, ok, msg, data},snake→camel 已在后端完成)
▼
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 列表 → 控制元素显隐
```
## 前后端对应关系
| 后端 | 前端 | 说明 |
|------|------|------|
| `Http/Controllers/Api/UserController.php` | `src/api/system/user.ts` | API 函数映射 |
| `Services/UserService.php` | `src/views/system/user/` | 业务页面 |
| `Http/Requests/UserRequest.php` 的 rules | 表单 `rules` | 前端表单校验规则(体验层) |
| `Models/User.php` | — | 前端不直接操作模型 |
| `routes/api.php` | `src/api/**/*.ts` | 路由 ↔ API 函数 |
| 字典数据(`dict` 表) | `src/enums/` + 字典 Store | 枚举常量 |
| `#[Permission]` 注解 | `v-perm` 指令 | 权限码对应 |
| 分页 `records/total/current/size/pages` | el-pagination 绑定 | 固定分页结构 |
## 层间通信规范
| 调用方向 | 是否允许 | 说明 |
|----------|---------|------|
| 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 |