1139 lines
30 KiB
Markdown
1139 lines
30 KiB
Markdown
# 情绪博物馆Web端技术方案
|
||
|
||
## 技术选型说明
|
||
|
||
### WebSocket通信方案选择
|
||
|
||
本方案选择 **STOMP + 原生WebSocket** 而非 Socket.io 的原因:
|
||
|
||
1. **后端集成优势**: 项目后端使用Spring Boot,STOMP是Spring WebSocket的原生支持协议
|
||
2. **标准化协议**: STOMP是标准的消息传递协议,不依赖特定实现
|
||
3. **消息队列支持**: 天然支持点对点和发布订阅模式,适合聊天和通知场景
|
||
4. **轻量级**: 相比Socket.io更轻量,减少前端包体积
|
||
5. **Token认证支持**: 原生WebSocket支持在握手时传递自定义请求头进行Token认证
|
||
|
||
### 技术栈版本策略
|
||
|
||
- **稳定性优先**: 选择经过验证的稳定版本
|
||
- **生态兼容**: 确保各组件间良好兼容
|
||
- **长期支持**: 优先选择有LTS支持的版本
|
||
- **性能考虑**: 新版本的性能优化和bug修复
|
||
|
||
## 1. 核心技术栈(推荐版本)
|
||
|
||
### 1.1 前端框架
|
||
|
||
- **Vue.js**: `3.4.21` (最新稳定版)
|
||
- **TypeScript**: `5.4.2` (最新稳定版)
|
||
- **Vite**: `5.1.6` (最新稳定版,更好的构建性能)
|
||
|
||
### 1.2 UI框架与样式
|
||
|
||
- **Element Plus**: `2.6.1` (最新稳定版,更好的Vue3支持)
|
||
- **Tailwind CSS**: `3.4.1` (最新稳定版)
|
||
- **@tailwindcss/forms**: `0.5.7` (表单样式增强)
|
||
- **@tailwindcss/typography**: `0.5.10` (文本排版增强)
|
||
|
||
### 1.3 状态管理与路由
|
||
|
||
- **Pinia**: `2.1.7` (保持现有版本,稳定可靠)
|
||
- **Vue Router**: `4.3.0` (最新稳定版)
|
||
- **@pinia/nuxt**: `0.5.1` (如果需要SSR支持)
|
||
|
||
### 1.4 HTTP客户端与实时通信
|
||
|
||
- **Axios**: `1.6.8` (最新稳定版)
|
||
- **@stomp/stompjs**: `7.1.1` (WebSocket通信,与Spring Boot后端集成,支持Token认证)
|
||
|
||
### 1.5 数据可视化
|
||
|
||
- **ECharts**: `5.5.0` (最新稳定版)
|
||
- **vue-echarts**: `6.7.3` (Vue3专用ECharts组件)
|
||
- **@antv/g2**: `5.1.15` (备选图表库,更现代化)
|
||
|
||
### 1.6 工具库
|
||
|
||
- **Day.js**: `1.11.10` (最新稳定版)
|
||
- **Lodash-es**: `4.17.21` (ES模块版本)
|
||
- **Zod**: `3.22.4` (数据验证)
|
||
- **VueUse**: `10.9.0` (Vue组合式API工具集)
|
||
|
||
### 1.7 开发工具
|
||
|
||
- **@vitejs/plugin-vue**: `5.0.4`
|
||
- **@vue/tsconfig**: `0.5.1`
|
||
- **vue-tsc**: `2.0.6`
|
||
- **unplugin-auto-import**: `0.17.5` (自动导入)
|
||
- **unplugin-vue-components**: `0.26.0` (组件自动导入)
|
||
|
||
## 2. 新增推荐技术栈
|
||
|
||
### 2.1 表单处理
|
||
|
||
- **@vuelidate/core**: `2.0.3` (表单验证)
|
||
- **@vuelidate/validators**: `2.0.4`
|
||
- **vue-hooks-form**: `0.8.6` (表单状态管理)
|
||
|
||
### 2.2 动画与交互
|
||
|
||
- **@vueuse/motion**: `2.0.0` (动画库)
|
||
- **vue-toastification**: `2.0.0-rc.5` (通知组件)
|
||
- **nprogress**: `0.2.0` (页面加载进度条)
|
||
|
||
### 2.3 文件处理
|
||
- **vue-upload-component**: `3.1.4` (文件上传)
|
||
- **cropperjs**: `1.6.1` (图片裁剪)
|
||
- **file-saver**: `2.0.5` (文件下载)
|
||
|
||
### 2.4 富文本编辑
|
||
- **@tiptap/vue-3**: `2.2.4` (现代富文本编辑器)
|
||
- **@tiptap/starter-kit**: `2.2.4`
|
||
- **@tiptap/extension-image**: `2.2.4`
|
||
|
||
### 2.5 PWA支持
|
||
- **vite-plugin-pwa**: `0.19.2` (PWA支持)
|
||
- **workbox-window**: `7.0.0` (Service Worker管理)
|
||
|
||
## 3. 开发工具与代码质量
|
||
|
||
### 3.1 代码规范
|
||
- **ESLint**: `8.57.0`
|
||
- **@vue/eslint-config-typescript**: `12.0.0`
|
||
- **@vue/eslint-config-prettier**: `9.0.0`
|
||
- **Prettier**: `3.2.5`
|
||
- **lint-staged**: `15.2.2`
|
||
- **husky**: `9.0.11`
|
||
|
||
### 3.2 测试框架
|
||
- **Vitest**: `1.4.0` (单元测试)
|
||
- **@vue/test-utils**: `2.4.5` (Vue组件测试)
|
||
- **jsdom**: `24.0.0` (DOM环境模拟)
|
||
- **Cypress**: `13.7.1` (E2E测试)
|
||
|
||
### 3.3 构建优化
|
||
- **rollup-plugin-visualizer**: `5.12.0` (构建分析)
|
||
- **vite-plugin-compression**: `0.5.1` (Gzip压缩)
|
||
- **vite-plugin-mock**: `3.0.1` (Mock数据)
|
||
|
||
## 4. 项目结构设计
|
||
|
||
```
|
||
src/
|
||
├── api/ # API接口定义
|
||
│ ├── auth.ts # 认证相关接口
|
||
│ ├── chat.ts # 聊天相关接口
|
||
│ ├── diary.ts # 日记相关接口
|
||
│ └── user.ts # 用户相关接口
|
||
├── assets/ # 静态资源
|
||
│ ├── images/ # 图片资源
|
||
│ ├── icons/ # 图标资源
|
||
│ └── styles/ # 全局样式
|
||
├── components/ # 公共组件
|
||
│ ├── common/ # 通用组件
|
||
│ ├── forms/ # 表单组件
|
||
│ ├── charts/ # 图表组件
|
||
│ └── layout/ # 布局组件
|
||
├── composables/ # 组合式API
|
||
│ ├── useAuth.ts # 认证逻辑
|
||
│ ├── useChat.ts # 聊天逻辑
|
||
│ ├── useWebSocket.ts # WebSocket逻辑
|
||
│ └── useApi.ts # API调用逻辑
|
||
├── config/ # 配置文件
|
||
│ ├── env.ts # 环境配置
|
||
│ ├── constants.ts # 常量定义
|
||
│ └── routes.ts # 路由配置
|
||
├── layouts/ # 页面布局
|
||
│ ├── DefaultLayout.vue # 默认布局
|
||
│ ├── AuthLayout.vue # 认证布局
|
||
│ └── ChatLayout.vue # 聊天布局
|
||
├── pages/ # 页面组件
|
||
│ ├── auth/ # 认证页面
|
||
│ ├── chat/ # 聊天页面
|
||
│ ├── diary/ # 日记页面
|
||
│ └── dashboard/ # 仪表盘页面
|
||
├── stores/ # 状态管理
|
||
│ ├── auth.ts # 认证状态
|
||
│ ├── chat.ts # 聊天状态
|
||
│ ├── user.ts # 用户状态
|
||
│ └── app.ts # 应用状态
|
||
├── types/ # 类型定义
|
||
│ ├── api.ts # API类型
|
||
│ ├── user.ts # 用户类型
|
||
│ ├── chat.ts # 聊天类型
|
||
│ └── global.d.ts # 全局类型
|
||
├── utils/ # 工具函数
|
||
│ ├── request.ts # HTTP请求工具
|
||
│ ├── websocket.ts # WebSocket工具
|
||
│ ├── storage.ts # 存储工具
|
||
│ ├── validation.ts # 验证工具
|
||
│ └── format.ts # 格式化工具
|
||
└── views/ # 页面视图
|
||
├── Home.vue # 首页
|
||
├── Login.vue # 登录页
|
||
├── Chat.vue # 聊天页
|
||
└── Dashboard.vue # 仪表盘
|
||
```
|
||
|
||
## 5. 核心功能实现方案
|
||
|
||
### 5.1 认证系统
|
||
- **JWT Token管理**: 使用Pinia存储,自动刷新机制
|
||
- **路由守卫**: 基于Vue Router的权限控制
|
||
- **第三方登录**: 支持微信、QQ、GitHub等
|
||
- **验证码**: 图形验证码 + 短信验证码
|
||
|
||
### 5.2 实时通信
|
||
- **STOMP协议**: 基于@stomp/stompjs,与Spring Boot WebSocket集成
|
||
- **原生WebSocket**: 支持Token认证,无需降级方案
|
||
- **连接管理**: 自动重连、心跳检测、连接状态监控
|
||
- **消息队列**: 支持点对点和发布订阅模式
|
||
- **Token认证**: 在WebSocket握手时传递Authorization头部
|
||
- **消息类型**: 文本、图片、表情、文件、系统通知
|
||
- **离线处理**: 离线消息缓存和同步机制
|
||
|
||
### 5.3 数据可视化
|
||
- **情绪趋势图**: 基于ECharts的时间序列图
|
||
- **情绪雷达图**: 多维度情绪分析
|
||
- **成长轨迹**: 交互式时间轴
|
||
- **数据导出**: 支持PDF、Excel导出
|
||
|
||
### 5.4 响应式设计
|
||
- **移动端适配**: 基于Tailwind CSS的响应式布局
|
||
- **触摸手势**: 支持滑动、缩放等手势操作
|
||
- **PWA支持**: 离线缓存、桌面安装
|
||
- **性能优化**: 虚拟滚动、懒加载
|
||
|
||
## 6. 环境配置优化
|
||
|
||
### 6.1 多环境配置
|
||
```typescript
|
||
// config/env.ts
|
||
export const envConfigs = {
|
||
local: {
|
||
name: '本地环境',
|
||
apiBaseUrl: 'http://localhost:19089/api',
|
||
wsBaseUrl: 'ws://localhost:19089',
|
||
uploadUrl: 'http://localhost:19089/api/upload',
|
||
debug: true,
|
||
mock: false
|
||
},
|
||
dev: {
|
||
name: '开发环境',
|
||
apiBaseUrl: 'https://dev-api.emotion-museum.com/api',
|
||
wsBaseUrl: 'wss://dev-api.emotion-museum.com',
|
||
uploadUrl: 'https://dev-api.emotion-museum.com/api/upload',
|
||
debug: true,
|
||
mock: false
|
||
},
|
||
test: {
|
||
name: '测试环境',
|
||
apiBaseUrl: 'https://test-api.emotion-museum.com/api',
|
||
wsBaseUrl: 'wss://test-api.emotion-museum.com',
|
||
uploadUrl: 'https://test-api.emotion-museum.com/api/upload',
|
||
debug: false,
|
||
mock: false
|
||
},
|
||
prod: {
|
||
name: '生产环境',
|
||
apiBaseUrl: 'https://api.emotion-museum.com/api',
|
||
wsBaseUrl: 'wss://api.emotion-museum.com',
|
||
uploadUrl: 'https://api.emotion-museum.com/api/upload',
|
||
debug: false,
|
||
mock: false
|
||
}
|
||
}
|
||
```
|
||
|
||
### 6.2 构建优化配置
|
||
```typescript
|
||
// vite.config.ts
|
||
export default defineConfig({
|
||
plugins: [
|
||
vue(),
|
||
AutoImport({
|
||
imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'],
|
||
dts: true
|
||
}),
|
||
Components({
|
||
resolvers: [ElementPlusResolver()],
|
||
dts: true
|
||
}),
|
||
VitePWA({
|
||
registerType: 'autoUpdate',
|
||
workbox: {
|
||
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
|
||
}
|
||
})
|
||
],
|
||
build: {
|
||
rollupOptions: {
|
||
output: {
|
||
manualChunks: {
|
||
vendor: ['vue', 'vue-router', 'pinia'],
|
||
elementPlus: ['element-plus'],
|
||
echarts: ['echarts'],
|
||
utils: ['axios', 'dayjs', 'lodash-es']
|
||
}
|
||
}
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
## 7. 部署方案
|
||
|
||
### 7.1 Docker部署
|
||
```dockerfile
|
||
# Dockerfile
|
||
FROM node:18-alpine as builder
|
||
WORKDIR /app
|
||
COPY package*.json ./
|
||
RUN npm ci --only=production
|
||
COPY . .
|
||
RUN npm run build
|
||
|
||
FROM nginx:alpine
|
||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||
COPY nginx.conf /etc/nginx/nginx.conf
|
||
EXPOSE 80
|
||
CMD ["nginx", "-g", "daemon off;"]
|
||
```
|
||
|
||
### 7.2 CI/CD配置
|
||
```yaml
|
||
# .github/workflows/deploy.yml
|
||
name: Deploy
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
jobs:
|
||
deploy:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: '18'
|
||
- run: npm ci
|
||
- run: npm run build
|
||
- run: npm run test
|
||
- name: Deploy to server
|
||
run: |
|
||
# 部署脚本
|
||
```
|
||
|
||
### 7.3 Nginx配置
|
||
```nginx
|
||
# nginx.conf
|
||
server {
|
||
listen 80;
|
||
server_name localhost;
|
||
root /usr/share/nginx/html;
|
||
index index.html;
|
||
|
||
# 处理Vue Router的history模式
|
||
location / {
|
||
try_files $uri $uri/ /index.html;
|
||
}
|
||
|
||
# 静态资源缓存
|
||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||
expires 1y;
|
||
add_header Cache-Control "public, immutable";
|
||
}
|
||
|
||
# API代理
|
||
location /api/ {
|
||
proxy_pass http://backend:19089/api/;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
}
|
||
|
||
# WebSocket代理
|
||
location /ws/ {
|
||
proxy_pass http://backend:19089/ws/;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
}
|
||
}
|
||
```
|
||
|
||
## 8. 性能优化策略
|
||
|
||
### 8.1 代码分割
|
||
- **路由级别懒加载**: 使用动态import()
|
||
- **组件级别动态导入**: 按需加载大型组件
|
||
- **第三方库按需加载**: Tree-shaking优化
|
||
- **图片懒加载**: Intersection Observer API
|
||
|
||
### 8.2 缓存策略
|
||
- **HTTP缓存配置**: 静态资源长期缓存
|
||
- **Service Worker缓存**: 离线访问支持
|
||
- **本地存储优化**: IndexedDB存储大量数据
|
||
- **CDN加速**: 静态资源CDN分发
|
||
|
||
### 8.3 监控与分析
|
||
- **Sentry**: `7.108.0` (错误监控)
|
||
- **Google Analytics**: `gtag` (用户行为分析)
|
||
- **Web Vitals**: `3.5.2` (性能指标监控)
|
||
- **Bundle Analyzer**: 构建分析工具
|
||
|
||
### 8.4 性能指标
|
||
- **首屏加载时间**: < 2秒
|
||
- **交互响应时间**: < 100ms
|
||
- **代码分割**: 单个chunk < 250KB
|
||
- **图片优化**: WebP格式,响应式图片
|
||
|
||
## 9. 安全考虑
|
||
|
||
### 9.1 前端安全
|
||
- **XSS防护**: 内容安全策略(CSP)
|
||
- **CSRF防护**: Token验证
|
||
- **敏感信息加密**: 本地存储加密
|
||
- **依赖安全**: 定期更新依赖包
|
||
|
||
### 9.2 API安全
|
||
- **Token自动刷新**: JWT令牌管理
|
||
- **请求签名验证**: HMAC签名
|
||
- **接口限流**: 防止恶意请求
|
||
- **数据加密传输**: HTTPS强制
|
||
|
||
### 9.3 内容安全策略
|
||
```html
|
||
<!-- CSP配置 -->
|
||
<meta http-equiv="Content-Security-Policy"
|
||
content="default-src 'self';
|
||
script-src 'self' 'unsafe-inline' 'unsafe-eval';
|
||
style-src 'self' 'unsafe-inline';
|
||
img-src 'self' data: https:;
|
||
connect-src 'self' wss: https:;">
|
||
```
|
||
|
||
## 10. 开发规范
|
||
|
||
### 10.1 代码规范
|
||
- **命名规范**:
|
||
- 组件: PascalCase (UserProfile.vue)
|
||
- 文件: kebab-case (user-profile.ts)
|
||
- 变量: camelCase (userName)
|
||
- 常量: UPPER_SNAKE_CASE (API_BASE_URL)
|
||
|
||
### 10.2 Git提交规范
|
||
```
|
||
feat: 新功能
|
||
fix: 修复bug
|
||
docs: 文档更新
|
||
style: 代码格式调整
|
||
refactor: 代码重构
|
||
test: 测试相关
|
||
chore: 构建工具或辅助工具的变动
|
||
```
|
||
|
||
### 10.3 组件开发规范
|
||
```vue
|
||
<template>
|
||
<!-- 模板内容 -->
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
// 导入
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import type { User } from '@/types/user'
|
||
|
||
// Props定义
|
||
interface Props {
|
||
user: User
|
||
readonly?: boolean
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
readonly: false
|
||
})
|
||
|
||
// Emits定义
|
||
interface Emits {
|
||
update: [user: User]
|
||
delete: [id: string]
|
||
}
|
||
|
||
const emit = defineEmits<Emits>()
|
||
|
||
// 响应式数据
|
||
const loading = ref(false)
|
||
|
||
// 计算属性
|
||
const displayName = computed(() => {
|
||
return props.user.nickname || props.user.username
|
||
})
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
// 初始化逻辑
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 组件样式 */
|
||
</style>
|
||
```
|
||
|
||
### 10.4 API接口规范
|
||
```typescript
|
||
// api/user.ts
|
||
import type { User, UserProfile, UpdateUserRequest } from '@/types/user'
|
||
import { request } from '@/utils/request'
|
||
|
||
export const userApi = {
|
||
// 获取用户信息
|
||
getProfile(): Promise<UserProfile> {
|
||
return request.get('/user/profile')
|
||
},
|
||
|
||
// 更新用户信息
|
||
updateProfile(data: UpdateUserRequest): Promise<User> {
|
||
return request.put('/user/profile', data)
|
||
},
|
||
|
||
// 上传头像
|
||
uploadAvatar(file: File): Promise<{ url: string }> {
|
||
const formData = new FormData()
|
||
formData.append('avatar', file)
|
||
return request.post('/user/avatar/upload', formData, {
|
||
headers: { 'Content-Type': 'multipart/form-data' }
|
||
})
|
||
}
|
||
}
|
||
```
|
||
|
||
### 10.5 WebSocket实现方案
|
||
|
||
#### 为什么选择STOMP而不是Socket.io?
|
||
|
||
1. **后端集成**: 项目后端使用Spring Boot,STOMP是Spring WebSocket的标准协议
|
||
2. **消息队列**: STOMP天然支持消息队列模式,适合聊天和通知场景
|
||
3. **标准化**: STOMP是标准协议,不依赖特定实现
|
||
4. **轻量级**: 相比Socket.io更轻量,减少包体积
|
||
|
||
#### WebSocket工具类实现
|
||
```typescript
|
||
// utils/websocket.ts
|
||
import { Client } from '@stomp/stompjs'
|
||
import { envConfig } from '@/config/env'
|
||
|
||
export class WebSocketService {
|
||
private client: Client
|
||
private connected = false
|
||
private reconnectAttempts = 0
|
||
private maxReconnectAttempts = 5
|
||
private currentToken = ''
|
||
|
||
constructor() {
|
||
this.client = new Client({
|
||
// 使用原生WebSocket,支持Token认证
|
||
brokerURL: `${envConfig.wsBaseUrl}/ws`,
|
||
|
||
// 心跳检测
|
||
heartbeatIncoming: 4000,
|
||
heartbeatOutgoing: 4000,
|
||
|
||
// 重连配置
|
||
reconnectDelay: 5000,
|
||
|
||
// 调试模式
|
||
debug: envConfig.debug ? console.log : undefined,
|
||
|
||
onConnect: () => {
|
||
this.connected = true
|
||
this.reconnectAttempts = 0
|
||
console.log('WebSocket连接成功')
|
||
},
|
||
|
||
onDisconnect: () => {
|
||
this.connected = false
|
||
console.log('WebSocket连接断开')
|
||
},
|
||
|
||
onStompError: (frame) => {
|
||
console.error('STOMP错误:', frame)
|
||
this.handleReconnect()
|
||
},
|
||
|
||
// WebSocket连接前的配置
|
||
beforeConnect: () => {
|
||
// 在WebSocket握手时添加Token
|
||
if (this.currentToken) {
|
||
this.client.configure({
|
||
connectHeaders: {
|
||
Authorization: `Bearer ${this.currentToken}`,
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
}
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 连接WebSocket
|
||
connect(token: string) {
|
||
this.currentToken = token
|
||
this.client.configure({
|
||
connectHeaders: {
|
||
Authorization: `Bearer ${token}`,
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
}
|
||
})
|
||
this.client.activate()
|
||
}
|
||
|
||
// 更新Token(用于Token刷新场景)
|
||
updateToken(newToken: string) {
|
||
this.currentToken = newToken
|
||
if (this.connected) {
|
||
// 断开当前连接
|
||
this.disconnect()
|
||
// 使用新Token重新连接
|
||
setTimeout(() => {
|
||
this.connect(newToken)
|
||
}, 1000)
|
||
}
|
||
}
|
||
|
||
// 断开连接
|
||
disconnect() {
|
||
this.client.deactivate()
|
||
}
|
||
|
||
// 订阅消息
|
||
subscribe(destination: string, callback: (message: any) => void) {
|
||
if (!this.connected) {
|
||
console.warn('WebSocket未连接')
|
||
return
|
||
}
|
||
|
||
return this.client.subscribe(destination, (message) => {
|
||
try {
|
||
const data = JSON.parse(message.body)
|
||
callback(data)
|
||
} catch (error) {
|
||
console.error('消息解析失败:', error)
|
||
}
|
||
})
|
||
}
|
||
|
||
// 发送消息
|
||
send(destination: string, body: any) {
|
||
if (!this.connected) {
|
||
console.warn('WebSocket未连接,消息将被缓存')
|
||
// 这里可以实现消息缓存逻辑
|
||
return
|
||
}
|
||
|
||
this.client.publish({
|
||
destination,
|
||
body: JSON.stringify(body)
|
||
})
|
||
}
|
||
|
||
// 处理重连
|
||
private handleReconnect() {
|
||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||
this.reconnectAttempts++
|
||
setTimeout(() => {
|
||
console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
|
||
this.client.activate()
|
||
}, 5000 * this.reconnectAttempts)
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 聊天功能使用示例
|
||
```typescript
|
||
// composables/useChat.ts
|
||
import { ref, onMounted, onUnmounted } from 'vue'
|
||
import { WebSocketService } from '@/utils/websocket'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
|
||
export function useChat() {
|
||
const authStore = useAuthStore()
|
||
const wsService = new WebSocketService()
|
||
const messages = ref<any[]>([])
|
||
const isConnected = ref(false)
|
||
|
||
// 连接WebSocket
|
||
const connect = () => {
|
||
if (authStore.token) {
|
||
wsService.connect(authStore.token)
|
||
|
||
// 订阅个人消息
|
||
wsService.subscribe(`/user/${authStore.user.id}/queue/messages`, (message) => {
|
||
messages.value.push(message)
|
||
})
|
||
|
||
// 订阅聊天室消息
|
||
wsService.subscribe('/topic/chat', (message) => {
|
||
messages.value.push(message)
|
||
})
|
||
|
||
isConnected.value = true
|
||
}
|
||
}
|
||
|
||
// 发送消息
|
||
const sendMessage = (content: string, type: 'text' | 'image' = 'text') => {
|
||
const message = {
|
||
content,
|
||
type,
|
||
timestamp: Date.now(),
|
||
userId: authStore.user.id
|
||
}
|
||
|
||
wsService.send('/app/chat.send', message)
|
||
}
|
||
|
||
// 组件挂载时连接
|
||
onMounted(() => {
|
||
connect()
|
||
})
|
||
|
||
// 组件卸载时断开连接
|
||
onUnmounted(() => {
|
||
wsService.disconnect()
|
||
})
|
||
|
||
return {
|
||
messages,
|
||
isConnected,
|
||
sendMessage,
|
||
connect
|
||
}
|
||
}
|
||
```
|
||
|
||
### 10.6 WebSocket Token认证详解
|
||
|
||
#### Token传递方式
|
||
原生WebSocket + STOMP协议支持Token认证:
|
||
|
||
1. **WebSocket握手时传递** (推荐)
|
||
```typescript
|
||
// 在WebSocket连接建立时传递Token
|
||
brokerURL: `${envConfig.wsBaseUrl}/ws`,
|
||
connectHeaders: {
|
||
Authorization: `Bearer ${token}`,
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
}
|
||
```
|
||
|
||
2. **URL参数传递** (备选方案)
|
||
```typescript
|
||
// 如果后端不支持连接头部,可以通过URL传递
|
||
brokerURL: `${envConfig.wsBaseUrl}/ws?token=${token}`
|
||
```
|
||
|
||
**为什么移除SockJS?**
|
||
- ❌ SockJS不支持在WebSocket握手时传递自定义请求头
|
||
- ❌ 无法在连接建立时进行Token认证
|
||
- ❌ 只能通过URL参数传递Token,安全性较低
|
||
- ✅ 原生WebSocket完全支持Token认证
|
||
- ✅ 现代浏览器WebSocket支持度已经很好
|
||
|
||
#### WebSocket方案对比
|
||
|
||
| 特性 | 原生WebSocket + STOMP | SockJS + STOMP | Socket.io |
|
||
|------|---------------------|----------------|-----------|
|
||
| **Token认证** | ✅ 支持请求头传递 | ❌ 不支持请求头 | ✅ 支持 |
|
||
| **浏览器兼容** | ✅ 现代浏览器完全支持 | ✅ 兼容性最好 | ✅ 兼容性好 |
|
||
| **包体积** | ✅ 最小 | ⚠️ 中等 | ❌ 最大 |
|
||
| **Spring Boot集成** | ✅ 原生支持 | ✅ 原生支持 | ❌ 需要额外配置 |
|
||
| **标准化** | ✅ 标准协议 | ✅ 标准协议 | ❌ 私有协议 |
|
||
| **安全性** | ✅ 握手时认证 | ⚠️ URL参数认证 | ✅ 握手时认证 |
|
||
|
||
**最终选择:原生WebSocket + STOMP**
|
||
- 🎯 **安全性优先**: 支持在握手时进行Token认证
|
||
- 🎯 **性能最优**: 无额外协议层,直接使用WebSocket
|
||
- 🎯 **标准化**: 基于标准STOMP协议
|
||
- 🎯 **轻量级**: 最小的包体积
|
||
- 🎯 **兼容性**: 现代浏览器支持度已经足够
|
||
|
||
#### Token刷新处理
|
||
```typescript
|
||
// stores/auth.ts - 认证状态管理
|
||
import { defineStore } from 'pinia'
|
||
import { ref, watch } from 'vue'
|
||
import { WebSocketService } from '@/utils/websocket'
|
||
|
||
export const useAuthStore = defineStore('auth', () => {
|
||
const token = ref<string>('')
|
||
const user = ref<any>(null)
|
||
const wsService = ref<WebSocketService | null>(null)
|
||
|
||
// 监听Token变化,自动更新WebSocket连接
|
||
watch(token, (newToken, oldToken) => {
|
||
if (newToken && newToken !== oldToken && wsService.value) {
|
||
wsService.value.updateToken(newToken)
|
||
}
|
||
})
|
||
|
||
// 登录
|
||
const login = async (credentials: LoginRequest) => {
|
||
try {
|
||
const response = await authApi.login(credentials)
|
||
token.value = response.token
|
||
user.value = response.user
|
||
|
||
// 登录成功后建立WebSocket连接
|
||
if (!wsService.value) {
|
||
wsService.value = new WebSocketService()
|
||
}
|
||
wsService.value.connect(token.value)
|
||
|
||
return response
|
||
} catch (error) {
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 登出
|
||
const logout = () => {
|
||
// 断开WebSocket连接
|
||
if (wsService.value) {
|
||
wsService.value.disconnect()
|
||
wsService.value = null
|
||
}
|
||
|
||
token.value = ''
|
||
user.value = null
|
||
}
|
||
|
||
// Token刷新
|
||
const refreshToken = async () => {
|
||
try {
|
||
const response = await authApi.refreshToken()
|
||
token.value = response.token
|
||
// watch会自动处理WebSocket重连
|
||
return response
|
||
} catch (error) {
|
||
// Token刷新失败,执行登出
|
||
logout()
|
||
throw error
|
||
}
|
||
}
|
||
|
||
return {
|
||
token,
|
||
user,
|
||
wsService,
|
||
login,
|
||
logout,
|
||
refreshToken
|
||
}
|
||
})
|
||
```
|
||
|
||
#### 后端WebSocket安全配置
|
||
对应的Spring Boot后端配置示例:
|
||
|
||
```java
|
||
// WebSocketConfig.java
|
||
@Configuration
|
||
@EnableWebSocketMessageBroker
|
||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||
|
||
@Override
|
||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||
config.enableSimpleBroker("/topic", "/queue");
|
||
config.setApplicationDestinationPrefixes("/app");
|
||
config.setUserDestinationPrefix("/user");
|
||
}
|
||
|
||
@Override
|
||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||
registry.addEndpoint("/ws")
|
||
.setAllowedOriginPatterns("*");
|
||
// 移除.withSockJS(),使用原生WebSocket支持Token认证
|
||
}
|
||
|
||
@Override
|
||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||
registration.interceptors(new AuthChannelInterceptor());
|
||
}
|
||
}
|
||
|
||
// AuthChannelInterceptor.java - Token验证拦截器
|
||
@Component
|
||
public class AuthChannelInterceptor implements ChannelInterceptor {
|
||
|
||
@Override
|
||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
|
||
|
||
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||
String token = accessor.getFirstNativeHeader("Authorization");
|
||
if (token != null && token.startsWith("Bearer ")) {
|
||
String jwt = token.substring(7);
|
||
// 验证JWT Token
|
||
if (jwtTokenProvider.validateToken(jwt)) {
|
||
String userId = jwtTokenProvider.getUserIdFromToken(jwt);
|
||
accessor.setUser(new StompPrincipal(userId));
|
||
} else {
|
||
throw new IllegalArgumentException("Invalid token");
|
||
}
|
||
} else {
|
||
throw new IllegalArgumentException("Missing token");
|
||
}
|
||
}
|
||
|
||
return message;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 错误处理和重连机制
|
||
```typescript
|
||
// 增强的WebSocket错误处理
|
||
export class WebSocketService {
|
||
private tokenExpiredCallback?: () => void
|
||
|
||
constructor(onTokenExpired?: () => void) {
|
||
this.tokenExpiredCallback = onTokenExpired
|
||
// ... 其他初始化代码
|
||
}
|
||
|
||
private handleStompError(frame: any) {
|
||
console.error('STOMP错误:', frame)
|
||
|
||
// 检查是否是Token相关错误
|
||
if (frame.headers && frame.headers.message) {
|
||
const errorMessage = frame.headers.message.toLowerCase()
|
||
|
||
if (errorMessage.includes('unauthorized') ||
|
||
errorMessage.includes('invalid token') ||
|
||
errorMessage.includes('token expired')) {
|
||
|
||
console.warn('Token认证失败,触发重新登录')
|
||
this.tokenExpiredCallback?.()
|
||
return
|
||
}
|
||
}
|
||
|
||
// 其他错误进行重连
|
||
this.handleReconnect()
|
||
}
|
||
}
|
||
|
||
// 在应用中使用
|
||
const authStore = useAuthStore()
|
||
const wsService = new WebSocketService(() => {
|
||
// Token过期回调
|
||
authStore.logout()
|
||
router.push('/login')
|
||
})
|
||
```
|
||
|
||
#### 安全最佳实践
|
||
|
||
1. **Token验证**: 每次WebSocket连接都验证Token有效性
|
||
2. **权限控制**: 基于用户角色限制订阅和发送权限
|
||
3. **连接限制**: 限制单用户的并发连接数
|
||
4. **消息加密**: 敏感消息内容加密传输
|
||
5. **审计日志**: 记录WebSocket连接和消息日志
|
||
|
||
这个方案完全支持Token认证,并且提供了完整的Token生命周期管理,包括刷新、过期处理和安全重连机制。
|
||
|
||
## 11. 测试策略
|
||
|
||
### 11.1 单元测试
|
||
```typescript
|
||
// tests/components/UserProfile.test.ts
|
||
import { mount } from '@vue/test-utils'
|
||
import { describe, it, expect } from 'vitest'
|
||
import UserProfile from '@/components/UserProfile.vue'
|
||
|
||
describe('UserProfile', () => {
|
||
it('renders user information correctly', () => {
|
||
const user = {
|
||
id: '1',
|
||
username: 'testuser',
|
||
nickname: 'Test User',
|
||
avatar: 'https://example.com/avatar.jpg'
|
||
}
|
||
|
||
const wrapper = mount(UserProfile, {
|
||
props: { user }
|
||
})
|
||
|
||
expect(wrapper.text()).toContain('Test User')
|
||
expect(wrapper.find('img').attributes('src')).toBe(user.avatar)
|
||
})
|
||
})
|
||
```
|
||
|
||
### 11.2 E2E测试
|
||
```typescript
|
||
// cypress/e2e/auth.cy.ts
|
||
describe('Authentication', () => {
|
||
it('should login successfully', () => {
|
||
cy.visit('/login')
|
||
cy.get('[data-cy=username]').type('testuser')
|
||
cy.get('[data-cy=password]').type('password123')
|
||
cy.get('[data-cy=login-btn]').click()
|
||
|
||
cy.url().should('include', '/dashboard')
|
||
cy.get('[data-cy=user-menu]').should('be.visible')
|
||
})
|
||
})
|
||
```
|
||
|
||
### 11.3 测试配置
|
||
```typescript
|
||
// vitest.config.ts
|
||
import { defineConfig } from 'vitest/config'
|
||
import vue from '@vitejs/plugin-vue'
|
||
|
||
export default defineConfig({
|
||
plugins: [vue()],
|
||
test: {
|
||
environment: 'jsdom',
|
||
globals: true,
|
||
setupFiles: ['./tests/setup.ts']
|
||
},
|
||
resolve: {
|
||
alias: {
|
||
'@': '/src'
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
## 12. 国际化支持
|
||
|
||
### 12.1 i18n配置
|
||
- **vue-i18n**: `9.10.2` (国际化支持)
|
||
- **@intlify/unplugin-vue-i18n**: `4.0.0` (构建时优化)
|
||
|
||
```typescript
|
||
// i18n/index.ts
|
||
import { createI18n } from 'vue-i18n'
|
||
import zh from './locales/zh.json'
|
||
import en from './locales/en.json'
|
||
|
||
export const i18n = createI18n({
|
||
legacy: false,
|
||
locale: 'zh',
|
||
fallbackLocale: 'en',
|
||
messages: {
|
||
zh,
|
||
en
|
||
}
|
||
})
|
||
```
|
||
|
||
### 12.2 语言文件结构
|
||
```json
|
||
// i18n/locales/zh.json
|
||
{
|
||
"common": {
|
||
"confirm": "确认",
|
||
"cancel": "取消",
|
||
"save": "保存",
|
||
"delete": "删除"
|
||
},
|
||
"auth": {
|
||
"login": "登录",
|
||
"register": "注册",
|
||
"logout": "退出登录"
|
||
},
|
||
"chat": {
|
||
"sendMessage": "发送消息",
|
||
"typing": "正在输入...",
|
||
"offline": "离线"
|
||
}
|
||
}
|
||
```
|
||
|
||
## 13. 移动端适配
|
||
|
||
### 13.1 响应式断点
|
||
```css
|
||
/* tailwind.config.js */
|
||
module.exports = {
|
||
theme: {
|
||
screens: {
|
||
'xs': '475px',
|
||
'sm': '640px',
|
||
'md': '768px',
|
||
'lg': '1024px',
|
||
'xl': '1280px',
|
||
'2xl': '1536px',
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 13.2 移动端优化
|
||
- **触摸优化**: 44px最小触摸目标
|
||
- **手势支持**: 滑动、缩放、长按
|
||
- **性能优化**: 虚拟滚动、图片懒加载
|
||
- **离线支持**: Service Worker缓存
|
||
|
||
### 13.3 PWA配置
|
||
```typescript
|
||
// vite.config.ts PWA配置
|
||
VitePWA({
|
||
registerType: 'autoUpdate',
|
||
includeAssets: ['favicon.ico', 'apple-touch-icon.png'],
|
||
manifest: {
|
||
name: '情绪博物馆',
|
||
short_name: '情绪博物馆',
|
||
description: '记录情绪,分享心情的温暖空间',
|
||
theme_color: '#4A90E2',
|
||
background_color: '#ffffff',
|
||
display: 'standalone',
|
||
icons: [
|
||
{
|
||
src: 'pwa-192x192.png',
|
||
sizes: '192x192',
|
||
type: 'image/png'
|
||
},
|
||
{
|
||
src: 'pwa-512x512.png',
|
||
sizes: '512x512',
|
||
type: 'image/png'
|
||
}
|
||
]
|
||
}
|
||
})
|
||
```
|
||
|
||
## 14. 总结
|
||
|
||
本技术方案基于现代化的Vue3生态系统,采用TypeScript提供类型安全,使用Vite构建工具提升开发体验。方案涵盖了:
|
||
|
||
### 14.1 技术优势
|
||
- **现代化技术栈**: Vue3 + TypeScript + Vite
|
||
- **完整的工具链**: 从开发到部署的全流程支持
|
||
- **性能优化**: 代码分割、懒加载、缓存策略
|
||
- **开发体验**: 热更新、自动导入、类型检查
|
||
|
||
### 14.2 可扩展性
|
||
- **模块化设计**: 清晰的项目结构和职责分离
|
||
- **组件化开发**: 可复用的UI组件库
|
||
- **插件系统**: 支持功能扩展和第三方集成
|
||
- **国际化支持**: 多语言适配能力
|
||
|
||
### 14.3 维护性
|
||
- **代码规范**: ESLint + Prettier统一代码风格
|
||
- **测试覆盖**: 单元测试 + E2E测试
|
||
- **文档完善**: 组件文档和API文档
|
||
- **版本管理**: 语义化版本控制
|
||
|
||
### 14.4 部署方案
|
||
- **多环境支持**: local/dev/test/prod环境配置
|
||
- **容器化部署**: Docker + Nginx部署方案
|
||
- **CI/CD流程**: 自动化构建和部署
|
||
- **监控告警**: 错误监控和性能分析
|
||
|
||
这个技术方案能够很好地支持情绪博物馆Web端的所有功能需求,同时具备良好的可维护性和扩展性,为项目的长期发展奠定坚实基础。
|