Files
chat-demo/DESIGN.md
T
2026-09-30 15:19:49 +08:00

200 lines
14 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.
# Chat Demo 前端设计书
## 1. 文档说明
本文档依据当前仓库中的前端代码,说明系统目标、技术选型、模块边界、代码结构、主要数据流和交互设计。项目是一个 Vue 单页聊天演示前端;当前仓库不包含服务端实现,因此服务端接口及消息协议以客户端实际调用和解析的内容为准。
## 2. 项目概述
系统提供账号注册、登录和一对一即时聊天页面。用户登录后可以查看最近会话、搜索最近联系人、浏览联系人在线状态、查看历史消息、加载更早消息,并通过 WebSocket 收发消息。聊天页面采用三栏布局:功能导航、会话/联系人列表、当前聊天内容。
前端职责包括:
- 通过 REST API 完成注册、登录以及用户、联系人、会话和历史消息数据读取。
- 保存登录令牌,并在后续 HTTP 请求中携带令牌。
- 使用 Vue Router 限制聊天路由的访问。
- 建立 WebSocket 连接,发送聊天消息、处理服务端推送并回传消息确认 ACK。
- 管理页面内的会话、消息、未读数、在线用户和输入状态。
## 3. 技术选型
| 分类 | 技术/模块 | 用途 |
| --- | --- | --- |
| UI 框架 | Vue 2.6 | 页面组件与响应式视图,采用 Options API |
| 路由 | Vue Router 3 | `/login`、`/register`、`/chat` 页面切换及导航守卫 |
| 状态管理 | Vuex 3 | 保存 token 和当前用户名 |
| UI 组件 | Element UI 2 | 登录/注册表单、输入框、按钮、提示消息 |
| HTTP | Axios 1 | REST 请求、认证请求头、统一响应与 401 处理 |
| 实时通信 | 浏览器原生 WebSocket | 即时消息、在线用户列表和 ACK |
| 构建工具 | Vue CLI 5、Babel 7 | 本地开发、构建、ES 转译和 Element UI 组件按需样式处理 |
项目未使用 TypeScript、独立 API 状态库或 WebSocket 第三方库;聊天页面自身维护主要业务状态。
## 4. 总体架构
```mermaid
flowchart LR
Browser[浏览器] --> Entry[src/main.js]
Entry --> App[src/App.vue]
Entry --> Router[src/router.js]
Entry --> Store[src/store/index.js]
Router --> Pages[登录 / 注册 / 聊天页面]
Pages --> API[src/utils/api.js]
API --> Request[src/utils/request.js]
Request -->|HTTP /api| Backend[后端 localhost:3000]
Pages --> WS[src/utils/websocket.js]
WS -->|WebSocket ws://localhost:3000| Backend
Request --> Store
Router --> Store
```
页面层负责交互、视图状态和流程编排;`api.js` 将业务接口映射为 HTTP 方法;`request.js` 统一管理 Axios 配置和拦截器;`websocket.js` 封装浏览器 WebSocket 的创建、发送、关闭和回调。Vuex 在入口、路由和请求层之间共享认证状态。
## 5. 代码结构与模块职责
```text
.
├── public/
│ └── index.html # SPA HTML 宿主
├── src/
│ ├── main.js # Vue、路由、store、Element UI 与全局 CSS 初始化
│ ├── App.vue # 根组件,通过 router-view 渲染当前页面
│ ├── router.js # 路由表与 token 路由守卫
│ ├── store/
│ │ └── index.js # token、username 与 mutations
│ ├── pages/
│ │ ├── login/
│ │ │ ├── LoginPage.vue # 登录表单和登录后跳转
│ │ │ └── RegisterPage.vue # 注册表单和密码确认
│ │ └── index/
│ │ └── ChatPage.vue # 聊天布局、会话状态及实时消息处理
│ ├── utils/
│ │ ├── api.js # 登录、注册、用户和聊天 REST API
│ │ ├── request.js # Axios 实例、认证头和响应拦截器
│ │ └── websocket.js # WebSocket 生命周期和 JSON 消息封装
│ └── assets/css/
│ └── style_public.css # 全局重置与基础样式
├── babel.config.js # Vue CLI Babel preset 与 Element UI 按需样式
├── jsconfig.json # JavaScript 配置及 @ -> src 别名
├── vue.config.js # Vue CLI 开发服务器代理
└── package.json # 依赖和开发/构建/lint 命令
```
### 5.1 应用启动与路由
`src/main.js` 加载 Element UI 及其主题样式、全局公共 CSS,并将 router 和 store 注入 Vue 根实例。`App.vue` 只提供 `router-view` 容器。路由采用 history 模式:根路径跳转 `/login`;`/login`、`/register` 和 `/chat` 分别对应登录、注册和聊天页面。`/chat` 标记 `requiresAuth`,导航守卫通过 `store.state.token` 判断是否允许进入。
### 5.2 登录、注册与认证状态
登录页收集用户名和密码,调用 `login()`;成功后从响应中读取 token,提交 `SET_TOKEN`、`SET_USERNAME` 并跳转聊天页。注册页在前端检查两次密码一致后调用 `register()`,成功后返回登录页。
Vuex 当前包含:
| 状态 | 初始值/持久化 | 用途 |
| --- | --- | --- |
| `token` | 初始化时从 `localStorage` 读取;`SET_TOKEN` 同步写入 localStorage | 路由鉴权及请求认证 |
| `username` | 空字符串,登录时写入 Vuex,不持久化 | 当前会话中的用户标识 |
HTTP 请求拦截器从 localStorage 读取 token 并设置 `Authorization` 请求头。响应状态为 401 时清理 token 和用户名并跳转登录页。退出登录时关闭 WebSocket、清空 Vuex token/username 并跳转登录。
### 5.3 REST API 封装
`src/utils/api.js` 提供以下调用:
| 函数 | 方法与路径 | 当前用途 |
| --- | --- | --- |
| `login(data)` | `POST /login` | 登录并取得 token |
| `register(data)` | `POST /register` | 创建账号 |
| `getUserInfo()` | `GET /userInfo` | 读取当前用户资料 |
| `getContactInfo()` | `GET /contactsInfo` | 读取联系人列表 |
| `getRecentMessages()` | `GET /recentChats` | 读取最近会话及预览信息 |
| `getContactMessages(data)` | `POST /contactMessagesInfo` | 分页读取指定联系人的消息 |
| `sendMessage(data)` | `POST /sendMessage` | HTTP 发送接口;当前聊天页未使用,实际发送走 WebSocket |
`src/utils/request.js` 创建 Axios 实例,默认 `baseURL` 为 `/api`,超时为 10 秒。开发服务器将 `/api` 转发到 `http://localhost:3000`。响应拦截器返回 `response.data`;页面代码随后通过 `response.data` 读取业务字段,因此当前前后端约定应在 HTTP 响应外层提供 `data` 字段(例如 Axios 响应体为 `{ "data": { ... } }`)。由于服务端代码不在本仓库,实际响应格式仍需与后端核对。
### 5.4 聊天页面与状态
`ChatPage.vue` 以一个组件承载聊天主界面及其业务流程,关键状态如下:
| 状态 | 含义 |
| --- | --- |
| `currentUser` | 当前用户资料 |
| `contactList` | 联系人列表 |
| `recentChats` | 最近会话列表,包含最近消息预览与未读数 |
| `onlineList` | 服务端推送的在线用户名列表 |
| `currentContact` | 当前正在查看的联系人 |
| `contactMessages` | 当前会话中展示的消息数组 |
| `searchText`、`messageText` | 联系人搜索关键字和编辑中的消息 |
| `activeMenu` | messages、contacts、settings 侧栏菜单状态 |
| `firstLoad`、`loadingMore`、`loadEnd` | 历史消息初次加载、分页加载和加载结束标记 |
组件创建时并行触发当前用户资料、最近会话和 WebSocket 初始化。最近会话加载完成后,如果列表非空,会自动选择第一条会话。切换至联系人菜单时另行请求联系人列表。选择联系人时清空当前消息并请求首批 10 条,查看后将该联系人未读数清零。
历史消息使用 `limit=10`,`offset` 取当前消息条数。加载更早消息时将返回数据倒序后置于现有列表之前,并依据加载前后的滚动区域高度调整 `scrollTop`,以尽量保持用户正在阅读的位置。发送或收到当前会话消息后,视图滚动到底部。
### 5.5 WebSocket 实时通信
`src/utils/websocket.js` 持有模块级 WebSocket 实例,连接地址固定为 `ws://localhost:3000`。聊天页面从 Vuex 读取 token,去除可能存在的 `Bearer ` 前缀并 URL 编码,通过查询参数 `token` 传递。连接建立后以 JSON 收发消息,并将解析后的数据交给页面回调。
客户端当前使用/处理的消息类型:
| 类型 | 方向 | 处理方式 |
| --- | --- | --- |
| `sendMessage` | 客户端 -> 服务端 | 包含 `messageId`、`senderName`、`receiverName`、`content`、`createdAt`;成功写入本地当前消息列表并更新最近会话 |
| `newMessage` | 服务端 -> 客户端 | 从 `message` 读取消息;当前联系人消息加入聊天列表,其他联系人的消息更新最近会话和未读数 |
| `ack` | 客户端 -> 服务端 | 收到 `newMessage` 后按 `messageId` 确认 |
| `sendError` | 服务端 -> 客户端 | 提示发送失败 |
| `userOnline` | 服务端 -> 客户端 | 使用 `userList` 更新在线列表 |
上述字段是前端代码所依赖的协议,不代表后端已实现或经过端到端验证。`connectWebSocket` 提供 open、close、error 回调;聊天页实现了 open 和 error 处理,未提供 close 回调。发送时要求连接处于 OPEN 状态;否则封装仅写控制台,不抛出异常,因此页面的 try/catch 不能据此判断 WebSocket 是否真正发送成功。
### 5.6 UI 与样式
聊天页面采用固定宽度左侧导航栏(78px)、联系人面板(300px)和弹性聊天面板。聊天面板包含会话标题、消息滚动区、输入区。消息区区分本人和对方消息气泡,并展示时间;未读数和联系人在线状态通过独立样式表达。页面 CSS 以 `ChatPage.vue` 内的 scoped 样式为主,公共 CSS 提供基础重置。
登录和注册页使用 Element UI 表单控件,页面样式局部定义,窄屏时缩小表单内边距。聊天容器设置 `min-width: 980px`,当前小屏行为是保留最小宽度并可能产生横向溢出,不属于完整的移动端适配。
## 6. 主要业务流程
### 6.1 登录进入聊天
1. 用户提交用户名和密码,页面调用 `POST /api/login`。
2. 成功响应中的 token 写入 Vuex 和 localStorage,用户名写入 Vuex。
3. 路由跳转 `/chat`;导航守卫检测到 token 后放行。
4. 聊天页读取用户资料、最近会话,并携带 token 建立 WebSocket。
5. 最近会话返回后默认打开第一位联系人并拉取最近 10 条消息。
### 6.2 发送与接收消息
1. 用户点击发送或按 Enter,页面去除首尾空白并检查消息及联系人。
2. 页面组装 `sendMessage` JSON,通过 WebSocket 发送,并乐观地将消息加入当前视图。
3. 服务端推送 `newMessage` 时,页面按发送者是否为当前联系人分流:更新当前消息列表,或更新会话预览和未读数。
4. 页面向服务端发送 `ack`,确认消息已接收。
### 6.3 认证失效或退出
HTTP 401 会清理认证状态并导航至登录页。用户主动退出会先关闭 WebSocket,再清理 Vuex 状态并返回登录页。当前认证守卫主要检查 token 是否存在,并不验证 token 有效性;有效性最终由服务端响应决定。
## 7. 构建与运行配置
- `npm install`:安装依赖。
- `npm run serve`:启动 Vue CLI 开发服务器。
- `npm run build`:生成生产构建。
- `npm run lint`:执行 ESLint 检查。
- 开发环境 `/api` 代理目标为 `http://localhost:3000`。
- WebSocket URL 在 `src/utils/websocket.js` 中固定为 `ws://localhost:3000`;部署到其他环境前需要配置化,并在 HTTPS 部署时切换到安全 WebSocket (`wss://`)。
## 8. 当前边界与改进建议
以下内容是根据当前代码观察到的限制,供后续迭代使用,并非已实现功能:
1. **运行配置集中化**:HTTP base URL 与 WebSocket 地址目前分散/硬编码,应按开发、测试、生产环境配置,减少部署时遗漏。(已解决)
2. **认证状态持久性**:token 会持久化,但 username 不持久化;刷新后需由用户信息接口恢复展示状态。还应评估 localStorage 对 token 的安全取舍及 token 过期刷新方案。(username已删除,token问题待解决)
3. **WebSocket 健壮性**:当前没有断线重连、心跳、待发送队列或连接状态 UI;发送前应明确连接不可用的反馈,并评估 ACK 超时及重复消息处理。
4. **会话切换与分页状态**:`loadEnd` 等分页标志需要在联系人切换时明确重置;异步消息请求也应避免旧联系人响应覆盖新联系人视图。(已解决)
5. **实时会话一致性**:收到当前联系人消息时会更新消息区,但当前实现没有同步刷新最近会话预览;接收新消息的未读判断及空会话创建逻辑也值得通过业务用例确认。
6. **响应契约与错误处理**:页面依赖响应中存在 `data` 字段。建议与服务端定义统一响应 DTO,并区分网络错误、认证错误、业务错误;当前错误主要由页面提示。
7. **UI 菜单与能力范围**:侧栏存在设置入口,但当前点击后主要改变菜单状态,未形成独立设置页;工具栏的表情和附件按钮也尚未绑定完整功能。
8. **移动端适配与无障碍**:聊天页最小宽度为 980px;联系人条目以鼠标点击交互为主。若支持移动设备或键盘操作,需补充响应式布局、语义按钮和焦点管理。
9. **自动化验证**:当前仓库配置了 lint/build 脚本,但未看到专门的单元测试或端到端测试。可优先为认证守卫、消息收发分流、未读计数和历史分页补充测试。