2526 lines
60 KiB
Markdown
2526 lines
60 KiB
Markdown
|
|
# 解班客项目性能优化文档
|
|||
|
|
|
|||
|
|
## 📋 文档概述
|
|||
|
|
|
|||
|
|
本文档详细说明解班客项目的性能优化策略、监控方案和优化实践,涵盖前端、后端、数据库和基础设施的全方位性能优化。
|
|||
|
|
|
|||
|
|
### 文档目标
|
|||
|
|
- 建立完整的性能监控体系
|
|||
|
|
- 提供系统性的性能优化方案
|
|||
|
|
- 制定性能基准和优化目标
|
|||
|
|
- 建立性能问题诊断和解决流程
|
|||
|
|
|
|||
|
|
## 🎯 性能目标和指标
|
|||
|
|
|
|||
|
|
### 核心性能指标
|
|||
|
|
|
|||
|
|
#### 前端性能指标
|
|||
|
|
```javascript
|
|||
|
|
// 前端性能目标
|
|||
|
|
const FRONTEND_PERFORMANCE_TARGETS = {
|
|||
|
|
// 页面加载性能
|
|||
|
|
pageLoad: {
|
|||
|
|
FCP: 1.5, // 首次内容绘制 < 1.5s
|
|||
|
|
LCP: 2.5, // 最大内容绘制 < 2.5s
|
|||
|
|
FID: 100, // 首次输入延迟 < 100ms
|
|||
|
|
CLS: 0.1, // 累积布局偏移 < 0.1
|
|||
|
|
TTI: 3.5 // 可交互时间 < 3.5s
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 资源加载
|
|||
|
|
resources: {
|
|||
|
|
jsBundle: 250, // JS包大小 < 250KB
|
|||
|
|
cssBundle: 50, // CSS包大小 < 50KB
|
|||
|
|
images: 100, // 图片大小 < 100KB
|
|||
|
|
fonts: 30 // 字体大小 < 30KB
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 运行时性能
|
|||
|
|
runtime: {
|
|||
|
|
fps: 60, // 帧率 >= 60fps
|
|||
|
|
memoryUsage: 50, // 内存使用 < 50MB
|
|||
|
|
cpuUsage: 30 // CPU使用率 < 30%
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 后端性能指标
|
|||
|
|
const BACKEND_PERFORMANCE_TARGETS = {
|
|||
|
|
// API响应时间
|
|||
|
|
api: {
|
|||
|
|
p50: 200, // 50%请求 < 200ms
|
|||
|
|
p95: 500, // 95%请求 < 500ms
|
|||
|
|
p99: 1000 // 99%请求 < 1000ms
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 吞吐量
|
|||
|
|
throughput: {
|
|||
|
|
rps: 1000, // 每秒请求数 >= 1000
|
|||
|
|
concurrent: 500 // 并发用户数 >= 500
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 资源使用
|
|||
|
|
resources: {
|
|||
|
|
cpu: 70, // CPU使用率 < 70%
|
|||
|
|
memory: 80, // 内存使用率 < 80%
|
|||
|
|
disk: 85 // 磁盘使用率 < 85%
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 数据库性能指标
|
|||
|
|
const DATABASE_PERFORMANCE_TARGETS = {
|
|||
|
|
// 查询性能
|
|||
|
|
query: {
|
|||
|
|
select: 50, // SELECT查询 < 50ms
|
|||
|
|
insert: 100, // INSERT操作 < 100ms
|
|||
|
|
update: 150, // UPDATE操作 < 150ms
|
|||
|
|
delete: 100 // DELETE操作 < 100ms
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 连接池
|
|||
|
|
connection: {
|
|||
|
|
poolSize: 20, // 连接池大小
|
|||
|
|
maxWait: 5000, // 最大等待时间 < 5s
|
|||
|
|
activeRatio: 0.8 // 活跃连接比例 < 80%
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 性能监控仪表板
|
|||
|
|
```javascript
|
|||
|
|
// 性能监控配置
|
|||
|
|
class PerformanceMonitor {
|
|||
|
|
constructor() {
|
|||
|
|
this.metrics = new Map()
|
|||
|
|
this.alerts = new Map()
|
|||
|
|
this.collectors = []
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化监控
|
|||
|
|
initialize() {
|
|||
|
|
// 前端性能监控
|
|||
|
|
this.initWebVitalsMonitoring()
|
|||
|
|
|
|||
|
|
// 后端性能监控
|
|||
|
|
this.initServerMonitoring()
|
|||
|
|
|
|||
|
|
// 数据库性能监控
|
|||
|
|
this.initDatabaseMonitoring()
|
|||
|
|
|
|||
|
|
// 基础设施监控
|
|||
|
|
this.initInfrastructureMonitoring()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Web Vitals监控
|
|||
|
|
initWebVitalsMonitoring() {
|
|||
|
|
// 使用Web Vitals库
|
|||
|
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|||
|
|
getCLS(this.onCLS.bind(this))
|
|||
|
|
getFID(this.onFID.bind(this))
|
|||
|
|
getFCP(this.onFCP.bind(this))
|
|||
|
|
getLCP(this.onLCP.bind(this))
|
|||
|
|
getTTFB(this.onTTFB.bind(this))
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 自定义性能指标
|
|||
|
|
this.monitorCustomMetrics()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理CLS指标
|
|||
|
|
onCLS(metric) {
|
|||
|
|
this.recordMetric('CLS', metric.value)
|
|||
|
|
|
|||
|
|
if (metric.value > FRONTEND_PERFORMANCE_TARGETS.pageLoad.CLS) {
|
|||
|
|
this.triggerAlert('CLS_HIGH', {
|
|||
|
|
value: metric.value,
|
|||
|
|
threshold: FRONTEND_PERFORMANCE_TARGETS.pageLoad.CLS,
|
|||
|
|
url: window.location.href
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理FID指标
|
|||
|
|
onFID(metric) {
|
|||
|
|
this.recordMetric('FID', metric.value)
|
|||
|
|
|
|||
|
|
if (metric.value > FRONTEND_PERFORMANCE_TARGETS.pageLoad.FID) {
|
|||
|
|
this.triggerAlert('FID_HIGH', {
|
|||
|
|
value: metric.value,
|
|||
|
|
threshold: FRONTEND_PERFORMANCE_TARGETS.pageLoad.FID,
|
|||
|
|
url: window.location.href
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 自定义性能监控
|
|||
|
|
monitorCustomMetrics() {
|
|||
|
|
// 监控资源加载时间
|
|||
|
|
this.monitorResourceTiming()
|
|||
|
|
|
|||
|
|
// 监控API请求性能
|
|||
|
|
this.monitorAPIPerformance()
|
|||
|
|
|
|||
|
|
// 监控内存使用
|
|||
|
|
this.monitorMemoryUsage()
|
|||
|
|
|
|||
|
|
// 监控帧率
|
|||
|
|
this.monitorFrameRate()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 资源加载时间监控
|
|||
|
|
monitorResourceTiming() {
|
|||
|
|
const observer = new PerformanceObserver((list) => {
|
|||
|
|
for (const entry of list.getEntries()) {
|
|||
|
|
if (entry.entryType === 'resource') {
|
|||
|
|
const loadTime = entry.responseEnd - entry.startTime
|
|||
|
|
|
|||
|
|
this.recordMetric('resource_load_time', {
|
|||
|
|
name: entry.name,
|
|||
|
|
type: this.getResourceType(entry.name),
|
|||
|
|
loadTime: loadTime,
|
|||
|
|
size: entry.transferSize
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 检查是否超过阈值
|
|||
|
|
if (this.isResourceLoadTimeSlow(entry.name, loadTime)) {
|
|||
|
|
this.triggerAlert('SLOW_RESOURCE', {
|
|||
|
|
resource: entry.name,
|
|||
|
|
loadTime: loadTime
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
observer.observe({ entryTypes: ['resource'] })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// API性能监控
|
|||
|
|
monitorAPIPerformance() {
|
|||
|
|
const originalFetch = window.fetch
|
|||
|
|
|
|||
|
|
window.fetch = async (...args) => {
|
|||
|
|
const startTime = performance.now()
|
|||
|
|
const url = args[0]
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await originalFetch(...args)
|
|||
|
|
const endTime = performance.now()
|
|||
|
|
const duration = endTime - startTime
|
|||
|
|
|
|||
|
|
this.recordMetric('api_request', {
|
|||
|
|
url: url,
|
|||
|
|
method: args[1]?.method || 'GET',
|
|||
|
|
status: response.status,
|
|||
|
|
duration: duration,
|
|||
|
|
success: response.ok
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 检查API响应时间
|
|||
|
|
if (duration > 1000) { // 超过1秒
|
|||
|
|
this.triggerAlert('SLOW_API', {
|
|||
|
|
url: url,
|
|||
|
|
duration: duration,
|
|||
|
|
status: response.status
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return response
|
|||
|
|
} catch (error) {
|
|||
|
|
const endTime = performance.now()
|
|||
|
|
const duration = endTime - startTime
|
|||
|
|
|
|||
|
|
this.recordMetric('api_request', {
|
|||
|
|
url: url,
|
|||
|
|
method: args[1]?.method || 'GET',
|
|||
|
|
duration: duration,
|
|||
|
|
success: false,
|
|||
|
|
error: error.message
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
this.triggerAlert('API_ERROR', {
|
|||
|
|
url: url,
|
|||
|
|
error: error.message,
|
|||
|
|
duration: duration
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
throw error
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 内存使用监控
|
|||
|
|
monitorMemoryUsage() {
|
|||
|
|
if ('memory' in performance) {
|
|||
|
|
setInterval(() => {
|
|||
|
|
const memory = performance.memory
|
|||
|
|
|
|||
|
|
this.recordMetric('memory_usage', {
|
|||
|
|
used: memory.usedJSHeapSize,
|
|||
|
|
total: memory.totalJSHeapSize,
|
|||
|
|
limit: memory.jsHeapSizeLimit,
|
|||
|
|
usage_ratio: memory.usedJSHeapSize / memory.jsHeapSizeLimit
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 检查内存使用率
|
|||
|
|
const usageRatio = memory.usedJSHeapSize / memory.jsHeapSizeLimit
|
|||
|
|
if (usageRatio > 0.8) { // 超过80%
|
|||
|
|
this.triggerAlert('HIGH_MEMORY_USAGE', {
|
|||
|
|
usage: memory.usedJSHeapSize,
|
|||
|
|
limit: memory.jsHeapSizeLimit,
|
|||
|
|
ratio: usageRatio
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}, 30000) // 每30秒检查一次
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 帧率监控
|
|||
|
|
monitorFrameRate() {
|
|||
|
|
let lastTime = performance.now()
|
|||
|
|
let frameCount = 0
|
|||
|
|
|
|||
|
|
const measureFPS = () => {
|
|||
|
|
frameCount++
|
|||
|
|
const currentTime = performance.now()
|
|||
|
|
|
|||
|
|
if (currentTime - lastTime >= 1000) { // 每秒计算一次
|
|||
|
|
const fps = Math.round((frameCount * 1000) / (currentTime - lastTime))
|
|||
|
|
|
|||
|
|
this.recordMetric('fps', fps)
|
|||
|
|
|
|||
|
|
if (fps < 30) { // 低于30fps
|
|||
|
|
this.triggerAlert('LOW_FPS', { fps: fps })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
frameCount = 0
|
|||
|
|
lastTime = currentTime
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
requestAnimationFrame(measureFPS)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
requestAnimationFrame(measureFPS)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 记录指标
|
|||
|
|
recordMetric(name, value) {
|
|||
|
|
const timestamp = Date.now()
|
|||
|
|
|
|||
|
|
if (!this.metrics.has(name)) {
|
|||
|
|
this.metrics.set(name, [])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.metrics.get(name).push({
|
|||
|
|
timestamp: timestamp,
|
|||
|
|
value: value
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 保持最近1000条记录
|
|||
|
|
const records = this.metrics.get(name)
|
|||
|
|
if (records.length > 1000) {
|
|||
|
|
records.splice(0, records.length - 1000)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送到监控服务
|
|||
|
|
this.sendToMonitoringService(name, value, timestamp)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 触发告警
|
|||
|
|
triggerAlert(type, data) {
|
|||
|
|
const alert = {
|
|||
|
|
type: type,
|
|||
|
|
data: data,
|
|||
|
|
timestamp: Date.now(),
|
|||
|
|
url: window.location.href,
|
|||
|
|
userAgent: navigator.userAgent
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.warn('Performance Alert:', alert)
|
|||
|
|
|
|||
|
|
// 发送告警到监控服务
|
|||
|
|
this.sendAlertToService(alert)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送数据到监控服务
|
|||
|
|
async sendToMonitoringService(metric, value, timestamp) {
|
|||
|
|
try {
|
|||
|
|
await fetch('/api/monitoring/metrics', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json'
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
metric: metric,
|
|||
|
|
value: value,
|
|||
|
|
timestamp: timestamp,
|
|||
|
|
url: window.location.href,
|
|||
|
|
session_id: this.getSessionId()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to send metric to monitoring service:', error)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送告警到服务
|
|||
|
|
async sendAlertToService(alert) {
|
|||
|
|
try {
|
|||
|
|
await fetch('/api/monitoring/alerts', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json'
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify(alert)
|
|||
|
|
})
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to send alert to monitoring service:', error)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取会话ID
|
|||
|
|
getSessionId() {
|
|||
|
|
let sessionId = sessionStorage.getItem('performance_session_id')
|
|||
|
|
if (!sessionId) {
|
|||
|
|
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
|
|||
|
|
sessionStorage.setItem('performance_session_id', sessionId)
|
|||
|
|
}
|
|||
|
|
return sessionId
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化性能监控
|
|||
|
|
const performanceMonitor = new PerformanceMonitor()
|
|||
|
|
performanceMonitor.initialize()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 🚀 前端性能优化
|
|||
|
|
|
|||
|
|
### 代码分割和懒加载
|
|||
|
|
|
|||
|
|
#### Vue路由懒加载
|
|||
|
|
```javascript
|
|||
|
|
// router/index.js - 路由懒加载配置
|
|||
|
|
import { createRouter, createWebHistory } from 'vue-router'
|
|||
|
|
|
|||
|
|
const routes = [
|
|||
|
|
{
|
|||
|
|
path: '/',
|
|||
|
|
name: 'Home',
|
|||
|
|
component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue')
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
path: '/animals',
|
|||
|
|
name: 'Animals',
|
|||
|
|
component: () => import(/* webpackChunkName: "animals" */ '@/views/Animals.vue')
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
path: '/adopt',
|
|||
|
|
name: 'Adopt',
|
|||
|
|
component: () => import(/* webpackChunkName: "adopt" */ '@/views/Adopt.vue')
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
path: '/profile',
|
|||
|
|
name: 'Profile',
|
|||
|
|
component: () => import(/* webpackChunkName: "profile" */ '@/views/Profile.vue'),
|
|||
|
|
meta: { requiresAuth: true }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
path: '/admin',
|
|||
|
|
name: 'Admin',
|
|||
|
|
component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/AdminLayout.vue'),
|
|||
|
|
children: [
|
|||
|
|
{
|
|||
|
|
path: 'dashboard',
|
|||
|
|
component: () => import(/* webpackChunkName: "admin-dashboard" */ '@/views/admin/Dashboard.vue')
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
path: 'users',
|
|||
|
|
component: () => import(/* webpackChunkName: "admin-users" */ '@/views/admin/Users.vue')
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
path: 'animals',
|
|||
|
|
component: () => import(/* webpackChunkName: "admin-animals" */ '@/views/admin/Animals.vue')
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const router = createRouter({
|
|||
|
|
history: createWebHistory(),
|
|||
|
|
routes
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export default router
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 组件懒加载
|
|||
|
|
```vue
|
|||
|
|
<!-- AnimalList.vue - 组件懒加载示例 -->
|
|||
|
|
<template>
|
|||
|
|
<div class="animal-list">
|
|||
|
|
<div class="filters">
|
|||
|
|
<!-- 过滤器组件 -->
|
|||
|
|
<AnimalFilters @filter="handleFilter" />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="list-container">
|
|||
|
|
<!-- 虚拟滚动列表 -->
|
|||
|
|
<VirtualList
|
|||
|
|
:items="filteredAnimals"
|
|||
|
|
:item-height="200"
|
|||
|
|
:container-height="600"
|
|||
|
|
@scroll-end="loadMore"
|
|||
|
|
>
|
|||
|
|
<template #item="{ item }">
|
|||
|
|
<!-- 懒加载动物卡片组件 -->
|
|||
|
|
<Suspense>
|
|||
|
|
<template #default>
|
|||
|
|
<AnimalCard :animal="item" />
|
|||
|
|
</template>
|
|||
|
|
<template #fallback>
|
|||
|
|
<AnimalCardSkeleton />
|
|||
|
|
</template>
|
|||
|
|
</Suspense>
|
|||
|
|
</template>
|
|||
|
|
</VirtualList>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, computed, defineAsyncComponent } from 'vue'
|
|||
|
|
import { useAnimalStore } from '@/stores/animal'
|
|||
|
|
|
|||
|
|
// 异步组件
|
|||
|
|
const AnimalFilters = defineAsyncComponent(() => import('@/components/AnimalFilters.vue'))
|
|||
|
|
const AnimalCard = defineAsyncComponent(() => import('@/components/AnimalCard.vue'))
|
|||
|
|
const AnimalCardSkeleton = defineAsyncComponent(() => import('@/components/AnimalCardSkeleton.vue'))
|
|||
|
|
const VirtualList = defineAsyncComponent(() => import('@/components/VirtualList.vue'))
|
|||
|
|
|
|||
|
|
const animalStore = useAnimalStore()
|
|||
|
|
|
|||
|
|
const filteredAnimals = computed(() => animalStore.filteredAnimals)
|
|||
|
|
|
|||
|
|
// 处理过滤
|
|||
|
|
const handleFilter = (filters) => {
|
|||
|
|
animalStore.applyFilters(filters)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载更多
|
|||
|
|
const loadMore = () => {
|
|||
|
|
animalStore.loadMoreAnimals()
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 虚拟滚动组件
|
|||
|
|
```vue
|
|||
|
|
<!-- VirtualList.vue - 虚拟滚动实现 -->
|
|||
|
|
<template>
|
|||
|
|
<div
|
|||
|
|
class="virtual-list"
|
|||
|
|
:style="{ height: containerHeight + 'px' }"
|
|||
|
|
@scroll="handleScroll"
|
|||
|
|
ref="containerRef"
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
class="virtual-list-phantom"
|
|||
|
|
:style="{ height: totalHeight + 'px' }"
|
|||
|
|
></div>
|
|||
|
|
|
|||
|
|
<div
|
|||
|
|
class="virtual-list-content"
|
|||
|
|
:style="{ transform: `translateY(${offsetY}px)` }"
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
v-for="item in visibleItems"
|
|||
|
|
:key="item.id"
|
|||
|
|
class="virtual-list-item"
|
|||
|
|
:style="{ height: itemHeight + 'px' }"
|
|||
|
|
>
|
|||
|
|
<slot name="item" :item="item" :index="item.index"></slot>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
items: {
|
|||
|
|
type: Array,
|
|||
|
|
required: true
|
|||
|
|
},
|
|||
|
|
itemHeight: {
|
|||
|
|
type: Number,
|
|||
|
|
required: true
|
|||
|
|
},
|
|||
|
|
containerHeight: {
|
|||
|
|
type: Number,
|
|||
|
|
required: true
|
|||
|
|
},
|
|||
|
|
buffer: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 5
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const emit = defineEmits(['scroll-end'])
|
|||
|
|
|
|||
|
|
const containerRef = ref(null)
|
|||
|
|
const scrollTop = ref(0)
|
|||
|
|
|
|||
|
|
// 计算总高度
|
|||
|
|
const totalHeight = computed(() => props.items.length * props.itemHeight)
|
|||
|
|
|
|||
|
|
// 计算可见区域的起始和结束索引
|
|||
|
|
const visibleRange = computed(() => {
|
|||
|
|
const start = Math.floor(scrollTop.value / props.itemHeight)
|
|||
|
|
const end = Math.min(
|
|||
|
|
start + Math.ceil(props.containerHeight / props.itemHeight) + props.buffer,
|
|||
|
|
props.items.length
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
start: Math.max(0, start - props.buffer),
|
|||
|
|
end
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 计算可见项目
|
|||
|
|
const visibleItems = computed(() => {
|
|||
|
|
const { start, end } = visibleRange.value
|
|||
|
|
return props.items.slice(start, end).map((item, index) => ({
|
|||
|
|
...item,
|
|||
|
|
index: start + index
|
|||
|
|
}))
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 计算偏移量
|
|||
|
|
const offsetY = computed(() => {
|
|||
|
|
return visibleRange.value.start * props.itemHeight
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 处理滚动
|
|||
|
|
const handleScroll = (event) => {
|
|||
|
|
scrollTop.value = event.target.scrollTop
|
|||
|
|
|
|||
|
|
// 检查是否滚动到底部
|
|||
|
|
const { scrollTop: currentScrollTop, scrollHeight, clientHeight } = event.target
|
|||
|
|
|
|||
|
|
if (currentScrollTop + clientHeight >= scrollHeight - 100) {
|
|||
|
|
emit('scroll-end')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 节流函数
|
|||
|
|
const throttle = (func, delay) => {
|
|||
|
|
let timeoutId
|
|||
|
|
let lastExecTime = 0
|
|||
|
|
|
|||
|
|
return function (...args) {
|
|||
|
|
const currentTime = Date.now()
|
|||
|
|
|
|||
|
|
if (currentTime - lastExecTime > delay) {
|
|||
|
|
func.apply(this, args)
|
|||
|
|
lastExecTime = currentTime
|
|||
|
|
} else {
|
|||
|
|
clearTimeout(timeoutId)
|
|||
|
|
timeoutId = setTimeout(() => {
|
|||
|
|
func.apply(this, args)
|
|||
|
|
lastExecTime = Date.now()
|
|||
|
|
}, delay - (currentTime - lastExecTime))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用节流的滚动处理
|
|||
|
|
const throttledHandleScroll = throttle(handleScroll, 16) // 60fps
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
if (containerRef.value) {
|
|||
|
|
containerRef.value.addEventListener('scroll', throttledHandleScroll, { passive: true })
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onUnmounted(() => {
|
|||
|
|
if (containerRef.value) {
|
|||
|
|
containerRef.value.removeEventListener('scroll', throttledHandleScroll)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.virtual-list {
|
|||
|
|
position: relative;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.virtual-list-phantom {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
z-index: -1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.virtual-list-content {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.virtual-list-item {
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 图片优化和懒加载
|
|||
|
|
|
|||
|
|
#### 图片懒加载指令
|
|||
|
|
```javascript
|
|||
|
|
// directives/lazyload.js - 图片懒加载指令
|
|||
|
|
export const lazyload = {
|
|||
|
|
mounted(el, binding) {
|
|||
|
|
const { value: src, modifiers } = binding
|
|||
|
|
|
|||
|
|
// 创建占位符
|
|||
|
|
const placeholder = modifiers.placeholder ?
|
|||
|
|
'/images/placeholder.svg' :
|
|||
|
|
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZGRkIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPkxvYWRpbmcuLi48L3RleHQ+PC9zdmc+'
|
|||
|
|
|
|||
|
|
el.src = placeholder
|
|||
|
|
|
|||
|
|
// 创建Intersection Observer
|
|||
|
|
const observer = new IntersectionObserver((entries) => {
|
|||
|
|
entries.forEach(entry => {
|
|||
|
|
if (entry.isIntersecting) {
|
|||
|
|
const img = entry.target
|
|||
|
|
|
|||
|
|
// 预加载图片
|
|||
|
|
const imageLoader = new Image()
|
|||
|
|
|
|||
|
|
imageLoader.onload = () => {
|
|||
|
|
// 添加淡入效果
|
|||
|
|
img.style.transition = 'opacity 0.3s'
|
|||
|
|
img.style.opacity = '0'
|
|||
|
|
|
|||
|
|
img.src = src
|
|||
|
|
|
|||
|
|
// 图片加载完成后淡入
|
|||
|
|
img.onload = () => {
|
|||
|
|
img.style.opacity = '1'
|
|||
|
|
img.classList.add('loaded')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
imageLoader.onerror = () => {
|
|||
|
|
// 加载失败时显示错误图片
|
|||
|
|
img.src = '/images/error.svg'
|
|||
|
|
img.classList.add('error')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
imageLoader.src = src
|
|||
|
|
|
|||
|
|
// 停止观察
|
|||
|
|
observer.unobserve(img)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}, {
|
|||
|
|
rootMargin: '50px' // 提前50px开始加载
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
observer.observe(el)
|
|||
|
|
|
|||
|
|
// 保存observer引用以便清理
|
|||
|
|
el._lazyloadObserver = observer
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
unmounted(el) {
|
|||
|
|
if (el._lazyloadObserver) {
|
|||
|
|
el._lazyloadObserver.disconnect()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 图片优化组件
|
|||
|
|
// components/OptimizedImage.vue
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<div class="optimized-image" :class="{ loading: isLoading, error: hasError }">
|
|||
|
|
<img
|
|||
|
|
ref="imageRef"
|
|||
|
|
:src="currentSrc"
|
|||
|
|
:alt="alt"
|
|||
|
|
:loading="lazy ? 'lazy' : 'eager'"
|
|||
|
|
@load="handleLoad"
|
|||
|
|
@error="handleError"
|
|||
|
|
:style="imageStyle"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<div v-if="isLoading" class="loading-placeholder">
|
|||
|
|
<div class="loading-spinner"></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-if="hasError" class="error-placeholder">
|
|||
|
|
<svg viewBox="0 0 24 24" class="error-icon">
|
|||
|
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
|||
|
|
</svg>
|
|||
|
|
<span>图片加载失败</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, computed, onMounted } from 'vue'
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
src: {
|
|||
|
|
type: String,
|
|||
|
|
required: true
|
|||
|
|
},
|
|||
|
|
alt: {
|
|||
|
|
type: String,
|
|||
|
|
default: ''
|
|||
|
|
},
|
|||
|
|
width: {
|
|||
|
|
type: [Number, String],
|
|||
|
|
default: 'auto'
|
|||
|
|
},
|
|||
|
|
height: {
|
|||
|
|
type: [Number, String],
|
|||
|
|
default: 'auto'
|
|||
|
|
},
|
|||
|
|
lazy: {
|
|||
|
|
type: Boolean,
|
|||
|
|
default: true
|
|||
|
|
},
|
|||
|
|
quality: {
|
|||
|
|
type: Number,
|
|||
|
|
default: 80,
|
|||
|
|
validator: (value) => value >= 1 && value <= 100
|
|||
|
|
},
|
|||
|
|
format: {
|
|||
|
|
type: String,
|
|||
|
|
default: 'webp',
|
|||
|
|
validator: (value) => ['webp', 'jpeg', 'png'].includes(value)
|
|||
|
|
},
|
|||
|
|
sizes: {
|
|||
|
|
type: String,
|
|||
|
|
default: '100vw'
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const imageRef = ref(null)
|
|||
|
|
const isLoading = ref(true)
|
|||
|
|
const hasError = ref(false)
|
|||
|
|
|
|||
|
|
// 生成优化后的图片URL
|
|||
|
|
const optimizedSrc = computed(() => {
|
|||
|
|
if (!props.src) return ''
|
|||
|
|
|
|||
|
|
// 如果是外部链接,直接返回
|
|||
|
|
if (props.src.startsWith('http')) {
|
|||
|
|
return props.src
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 构建优化参数
|
|||
|
|
const params = new URLSearchParams({
|
|||
|
|
q: props.quality,
|
|||
|
|
f: props.format
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if (props.width !== 'auto') {
|
|||
|
|
params.append('w', props.width)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (props.height !== 'auto') {
|
|||
|
|
params.append('h', props.height)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return `/api/images/optimize?src=${encodeURIComponent(props.src)}&${params.toString()}`
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 当前显示的图片源
|
|||
|
|
const currentSrc = computed(() => {
|
|||
|
|
if (hasError.value) {
|
|||
|
|
return '/images/error.svg'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (isLoading.value && props.lazy) {
|
|||
|
|
return '/images/placeholder.svg'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return optimizedSrc.value
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 图片样式
|
|||
|
|
const imageStyle = computed(() => ({
|
|||
|
|
width: typeof props.width === 'number' ? `${props.width}px` : props.width,
|
|||
|
|
height: typeof props.height === 'number' ? `${props.height}px` : props.height,
|
|||
|
|
objectFit: 'cover'
|
|||
|
|
}))
|
|||
|
|
|
|||
|
|
// 处理图片加载完成
|
|||
|
|
const handleLoad = () => {
|
|||
|
|
isLoading.value = false
|
|||
|
|
hasError.value = false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理图片加载错误
|
|||
|
|
const handleError = () => {
|
|||
|
|
isLoading.value = false
|
|||
|
|
hasError.value = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 支持WebP检测
|
|||
|
|
const supportsWebP = () => {
|
|||
|
|
const canvas = document.createElement('canvas')
|
|||
|
|
canvas.width = 1
|
|||
|
|
canvas.height = 1
|
|||
|
|
return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
// 如果浏览器不支持WebP,回退到JPEG
|
|||
|
|
if (props.format === 'webp' && !supportsWebP()) {
|
|||
|
|
// 这里可以修改format,但由于是props,需要通过其他方式处理
|
|||
|
|
console.warn('Browser does not support WebP, falling back to JPEG')
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.optimized-image {
|
|||
|
|
position: relative;
|
|||
|
|
display: inline-block;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.optimized-image img {
|
|||
|
|
display: block;
|
|||
|
|
transition: opacity 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading-placeholder,
|
|||
|
|
.error-placeholder {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
bottom: 0;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
background-color: #f5f5f5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading-spinner {
|
|||
|
|
width: 24px;
|
|||
|
|
height: 24px;
|
|||
|
|
border: 2px solid #e0e0e0;
|
|||
|
|
border-top: 2px solid #1976d2;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
animation: spin 1s linear infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes spin {
|
|||
|
|
0% { transform: rotate(0deg); }
|
|||
|
|
100% { transform: rotate(360deg); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.error-placeholder {
|
|||
|
|
flex-direction: column;
|
|||
|
|
color: #666;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.error-icon {
|
|||
|
|
width: 24px;
|
|||
|
|
height: 24px;
|
|||
|
|
fill: #f44336;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.optimized-image.loading img {
|
|||
|
|
opacity: 0.5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.optimized-image.error img {
|
|||
|
|
opacity: 0.3;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 缓存策略优化
|
|||
|
|
|
|||
|
|
#### Service Worker缓存
|
|||
|
|
```javascript
|
|||
|
|
// public/sw.js - Service Worker缓存策略
|
|||
|
|
const CACHE_NAME = 'jiebanke-v1.0.0'
|
|||
|
|
const STATIC_CACHE = 'jiebanke-static-v1.0.0'
|
|||
|
|
const DYNAMIC_CACHE = 'jiebanke-dynamic-v1.0.0'
|
|||
|
|
const IMAGE_CACHE = 'jiebanke-images-v1.0.0'
|
|||
|
|
|
|||
|
|
// 需要缓存的静态资源
|
|||
|
|
const STATIC_ASSETS = [
|
|||
|
|
'/',
|
|||
|
|
'/manifest.json',
|
|||
|
|
'/css/app.css',
|
|||
|
|
'/js/app.js',
|
|||
|
|
'/images/logo.svg',
|
|||
|
|
'/images/placeholder.svg',
|
|||
|
|
'/images/error.svg',
|
|||
|
|
'/fonts/roboto-regular.woff2',
|
|||
|
|
'/fonts/roboto-medium.woff2'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
// 缓存策略配置
|
|||
|
|
const CACHE_STRATEGIES = {
|
|||
|
|
// 静态资源:缓存优先
|
|||
|
|
static: {
|
|||
|
|
pattern: /\.(css|js|woff2?|svg|png|jpg|jpeg|gif|ico)$/,
|
|||
|
|
strategy: 'cacheFirst',
|
|||
|
|
maxAge: 30 * 24 * 60 * 60 * 1000, // 30天
|
|||
|
|
maxEntries: 100
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// API请求:网络优先
|
|||
|
|
api: {
|
|||
|
|
pattern: /^https?:\/\/.*\/api\//,
|
|||
|
|
strategy: 'networkFirst',
|
|||
|
|
maxAge: 5 * 60 * 1000, // 5分钟
|
|||
|
|
maxEntries: 50
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 图片:缓存优先
|
|||
|
|
images: {
|
|||
|
|
pattern: /\.(png|jpg|jpeg|gif|webp|svg)$/,
|
|||
|
|
strategy: 'cacheFirst',
|
|||
|
|
maxAge: 7 * 24 * 60 * 60 * 1000, // 7天
|
|||
|
|
maxEntries: 200
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// HTML页面:网络优先
|
|||
|
|
pages: {
|
|||
|
|
pattern: /\.html$|\/$/,
|
|||
|
|
strategy: 'networkFirst',
|
|||
|
|
maxAge: 24 * 60 * 60 * 1000, // 1天
|
|||
|
|
maxEntries: 20
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 安装事件
|
|||
|
|
self.addEventListener('install', (event) => {
|
|||
|
|
console.log('Service Worker installing...')
|
|||
|
|
|
|||
|
|
event.waitUntil(
|
|||
|
|
caches.open(STATIC_CACHE)
|
|||
|
|
.then((cache) => {
|
|||
|
|
console.log('Caching static assets')
|
|||
|
|
return cache.addAll(STATIC_ASSETS)
|
|||
|
|
})
|
|||
|
|
.then(() => {
|
|||
|
|
console.log('Static assets cached')
|
|||
|
|
return self.skipWaiting()
|
|||
|
|
})
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 激活事件
|
|||
|
|
self.addEventListener('activate', (event) => {
|
|||
|
|
console.log('Service Worker activating...')
|
|||
|
|
|
|||
|
|
event.waitUntil(
|
|||
|
|
caches.keys()
|
|||
|
|
.then((cacheNames) => {
|
|||
|
|
return Promise.all(
|
|||
|
|
cacheNames.map((cacheName) => {
|
|||
|
|
// 删除旧版本缓存
|
|||
|
|
if (cacheName !== STATIC_CACHE &&
|
|||
|
|
cacheName !== DYNAMIC_CACHE &&
|
|||
|
|
cacheName !== IMAGE_CACHE) {
|
|||
|
|
console.log('Deleting old cache:', cacheName)
|
|||
|
|
return caches.delete(cacheName)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
.then(() => {
|
|||
|
|
console.log('Service Worker activated')
|
|||
|
|
return self.clients.claim()
|
|||
|
|
})
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 拦截请求
|
|||
|
|
self.addEventListener('fetch', (event) => {
|
|||
|
|
const { request } = event
|
|||
|
|
const url = new URL(request.url)
|
|||
|
|
|
|||
|
|
// 只处理同源请求
|
|||
|
|
if (url.origin !== location.origin) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据请求类型选择缓存策略
|
|||
|
|
const strategy = getStrategy(request)
|
|||
|
|
|
|||
|
|
if (strategy) {
|
|||
|
|
event.respondWith(handleRequest(request, strategy))
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 获取缓存策略
|
|||
|
|
function getStrategy(request) {
|
|||
|
|
const url = request.url
|
|||
|
|
|
|||
|
|
for (const [name, config] of Object.entries(CACHE_STRATEGIES)) {
|
|||
|
|
if (config.pattern.test(url)) {
|
|||
|
|
return { name, ...config }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理请求
|
|||
|
|
async function handleRequest(request, strategy) {
|
|||
|
|
switch (strategy.strategy) {
|
|||
|
|
case 'cacheFirst':
|
|||
|
|
return cacheFirst(request, strategy)
|
|||
|
|
case 'networkFirst':
|
|||
|
|
return networkFirst(request, strategy)
|
|||
|
|
case 'staleWhileRevalidate':
|
|||
|
|
return staleWhileRevalidate(request, strategy)
|
|||
|
|
default:
|
|||
|
|
return fetch(request)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 缓存优先策略
|
|||
|
|
async function cacheFirst(request, strategy) {
|
|||
|
|
const cacheName = getCacheName(strategy.name)
|
|||
|
|
const cache = await caches.open(cacheName)
|
|||
|
|
const cachedResponse = await cache.match(request)
|
|||
|
|
|
|||
|
|
if (cachedResponse) {
|
|||
|
|
// 检查缓存是否过期
|
|||
|
|
const cacheTime = await getCacheTime(cache, request)
|
|||
|
|
if (cacheTime && Date.now() - cacheTime < strategy.maxAge) {
|
|||
|
|
return cachedResponse
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const networkResponse = await fetch(request)
|
|||
|
|
|
|||
|
|
if (networkResponse.ok) {
|
|||
|
|
// 缓存新响应
|
|||
|
|
await cache.put(request, networkResponse.clone())
|
|||
|
|
await setCacheTime(cache, request, Date.now())
|
|||
|
|
|
|||
|
|
// 清理过期缓存
|
|||
|
|
await cleanupCache(cache, strategy.maxEntries)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return networkResponse
|
|||
|
|
} catch (error) {
|
|||
|
|
// 网络失败时返回缓存
|
|||
|
|
if (cachedResponse) {
|
|||
|
|
return cachedResponse
|
|||
|
|
}
|
|||
|
|
throw error
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 网络优先策略
|
|||
|
|
async function networkFirst(request, strategy) {
|
|||
|
|
const cacheName = getCacheName(strategy.name)
|
|||
|
|
const cache = await caches.open(cacheName)
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const networkResponse = await fetch(request)
|
|||
|
|
|
|||
|
|
if (networkResponse.ok) {
|
|||
|
|
// 缓存响应
|
|||
|
|
await cache.put(request, networkResponse.clone())
|
|||
|
|
await setCacheTime(cache, request, Date.now())
|
|||
|
|
|
|||
|
|
// 清理过期缓存
|
|||
|
|
await cleanupCache(cache, strategy.maxEntries)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return networkResponse
|
|||
|
|
} catch (error) {
|
|||
|
|
// 网络失败时尝试从缓存获取
|
|||
|
|
const cachedResponse = await cache.match(request)
|
|||
|
|
|
|||
|
|
if (cachedResponse) {
|
|||
|
|
const cacheTime = await getCacheTime(cache, request)
|
|||
|
|
if (!cacheTime || Date.now() - cacheTime < strategy.maxAge) {
|
|||
|
|
return cachedResponse
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
throw error
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 过期重新验证策略
|
|||
|
|
async function staleWhileRevalidate(request, strategy) {
|
|||
|
|
const cacheName = getCacheName(strategy.name)
|
|||
|
|
const cache = await caches.open(cacheName)
|
|||
|
|
const cachedResponse = await cache.match(request)
|
|||
|
|
|
|||
|
|
// 后台更新缓存
|
|||
|
|
const fetchPromise = fetch(request).then(async (networkResponse) => {
|
|||
|
|
if (networkResponse.ok) {
|
|||
|
|
await cache.put(request, networkResponse.clone())
|
|||
|
|
await setCacheTime(cache, request, Date.now())
|
|||
|
|
await cleanupCache(cache, strategy.maxEntries)
|
|||
|
|
}
|
|||
|
|
return networkResponse
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 如果有缓存,立即返回;否则等待网络响应
|
|||
|
|
return cachedResponse || fetchPromise
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取缓存名称
|
|||
|
|
function getCacheName(strategyName) {
|
|||
|
|
switch (strategyName) {
|
|||
|
|
case 'static':
|
|||
|
|
return STATIC_CACHE
|
|||
|
|
case 'images':
|
|||
|
|
return IMAGE_CACHE
|
|||
|
|
default:
|
|||
|
|
return DYNAMIC_CACHE
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置缓存时间
|
|||
|
|
async function setCacheTime(cache, request, time) {
|
|||
|
|
const timeKey = `${request.url}:timestamp`
|
|||
|
|
await cache.put(timeKey, new Response(time.toString()))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取缓存时间
|
|||
|
|
async function getCacheTime(cache, request) {
|
|||
|
|
const timeKey = `${request.url}:timestamp`
|
|||
|
|
const timeResponse = await cache.match(timeKey)
|
|||
|
|
|
|||
|
|
if (timeResponse) {
|
|||
|
|
const timeText = await timeResponse.text()
|
|||
|
|
return parseInt(timeText, 10)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清理过期缓存
|
|||
|
|
async function cleanupCache(cache, maxEntries) {
|
|||
|
|
const keys = await cache.keys()
|
|||
|
|
|
|||
|
|
// 过滤出非时间戳的键
|
|||
|
|
const contentKeys = keys.filter(key => !key.url.includes(':timestamp'))
|
|||
|
|
|
|||
|
|
if (contentKeys.length > maxEntries) {
|
|||
|
|
// 删除最旧的条目
|
|||
|
|
const keysToDelete = contentKeys.slice(0, contentKeys.length - maxEntries)
|
|||
|
|
|
|||
|
|
for (const key of keysToDelete) {
|
|||
|
|
await cache.delete(key)
|
|||
|
|
// 同时删除对应的时间戳
|
|||
|
|
await cache.delete(`${key.url}:timestamp`)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 消息处理
|
|||
|
|
self.addEventListener('message', (event) => {
|
|||
|
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
|||
|
|
self.skipWaiting()
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 🔧 后端性能优化
|
|||
|
|
|
|||
|
|
### 数据库查询优化
|
|||
|
|
|
|||
|
|
#### 查询优化策略
|
|||
|
|
```javascript
|
|||
|
|
// models/Animal.js - 动物模型优化
|
|||
|
|
const { Model, DataTypes, Op } = require('sequelize')
|
|||
|
|
const sequelize = require('../config/database')
|
|||
|
|
|
|||
|
|
class Animal extends Model {
|
|||
|
|
// 优化的查询方法
|
|||
|
|
static async findWithPagination(options = {}) {
|
|||
|
|
const {
|
|||
|
|
page = 1,
|
|||
|
|
limit = 20,
|
|||
|
|
filters = {},
|
|||
|
|
sort = 'created_at',
|
|||
|
|
order = 'DESC',
|
|||
|
|
include = []
|
|||
|
|
} = options
|
|||
|
|
|
|||
|
|
const offset = (page - 1) * limit
|
|||
|
|
|
|||
|
|
// 构建查询条件
|
|||
|
|
const where = this.buildWhereClause(filters)
|
|||
|
|
|
|||
|
|
// 优化的查询配置
|
|||
|
|
const queryOptions = {
|
|||
|
|
where,
|
|||
|
|
limit: parseInt(limit),
|
|||
|
|
offset: parseInt(offset),
|
|||
|
|
order: [[sort, order]],
|
|||
|
|
include: this.buildIncludeClause(include),
|
|||
|
|
// 使用索引提示
|
|||
|
|
attributes: {
|
|||
|
|
include: [
|
|||
|
|
// 计算字段
|
|||
|
|
[
|
|||
|
|
sequelize.literal(`(
|
|||
|
|
SELECT COUNT(*)
|
|||
|
|
FROM adoptions
|
|||
|
|
WHERE adoptions.animal_id = Animal.id
|
|||
|
|
AND adoptions.status = 'completed'
|
|||
|
|
)`),
|
|||
|
|
'adoption_count'
|
|||
|
|
]
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
// 子查询优化
|
|||
|
|
subQuery: false,
|
|||
|
|
// 启用查询缓存
|
|||
|
|
benchmark: true,
|
|||
|
|
logging: process.env.NODE_ENV === 'development'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 执行查询
|
|||
|
|
const result = await this.findAndCountAll(queryOptions)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
data: result.rows,
|
|||
|
|
pagination: {
|
|||
|
|
page: parseInt(page),
|
|||
|
|
limit: parseInt(limit),
|
|||
|
|
total: result.count,
|
|||
|
|
pages: Math.ceil(result.count / limit)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 构建WHERE子句
|
|||
|
|
static buildWhereClause(filters) {
|
|||
|
|
const where = {}
|
|||
|
|
|
|||
|
|
// 状态过滤
|
|||
|
|
if (filters.status) {
|
|||
|
|
where.status = filters.status
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 类型过滤
|
|||
|
|
if (filters.type) {
|
|||
|
|
where.type = filters.type
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 年龄范围过滤
|
|||
|
|
if (filters.minAge || filters.maxAge) {
|
|||
|
|
where.age = {}
|
|||
|
|
if (filters.minAge) {
|
|||
|
|
where.age[Op.gte] = filters.minAge
|
|||
|
|
}
|
|||
|
|
if (filters.maxAge) {
|
|||
|
|
where.age[Op.lte] = filters.maxAge
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 地区过滤
|
|||
|
|
if (filters.location) {
|
|||
|
|
where.location = {
|
|||
|
|
[Op.like]: `%${filters.location}%`
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 关键词搜索
|
|||
|
|
if (filters.keyword) {
|
|||
|
|
where[Op.or] = [
|
|||
|
|
{ name: { [Op.like]: `%${filters.keyword}%` } },
|
|||
|
|
{ description: { [Op.like]: `%${filters.keyword}%` } },
|
|||
|
|
{ breed: { [Op.like]: `%${filters.keyword}%` } }
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 日期范围过滤
|
|||
|
|
if (filters.dateFrom || filters.dateTo) {
|
|||
|
|
where.created_at = {}
|
|||
|
|
if (filters.dateFrom) {
|
|||
|
|
where.created_at[Op.gte] = new Date(filters.dateFrom)
|
|||
|
|
}
|
|||
|
|
if (filters.dateTo) {
|
|||
|
|
where.created_at[Op.lte] = new Date(filters.dateTo)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return where
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 构建INCLUDE子句
|
|||
|
|
static buildIncludeClause(include) {
|
|||
|
|
const includes = []
|
|||
|
|
|
|||
|
|
if (include.includes('images')) {
|
|||
|
|
includes.push({
|
|||
|
|
model: require('./AnimalImage'),
|
|||
|
|
as: 'images',
|
|||
|
|
attributes: ['id', 'url', 'is_primary'],
|
|||
|
|
where: { is_deleted: false },
|
|||
|
|
required: false,
|
|||
|
|
// 只获取主图片以提高性能
|
|||
|
|
limit: 1,
|
|||
|
|
order: [['is_primary', 'DESC'], ['created_at', 'ASC']]
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (include.includes('shelter')) {
|
|||
|
|
includes.push({
|
|||
|
|
model: require('./Shelter'),
|
|||
|
|
as: 'shelter',
|
|||
|
|
attributes: ['id', 'name', 'location', 'contact_phone']
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (include.includes('adoptions')) {
|
|||
|
|
includes.push({
|
|||
|
|
model: require('./Adoption'),
|
|||
|
|
as: 'adoptions',
|
|||
|
|
attributes: ['id', 'status', 'created_at'],
|
|||
|
|
where: { status: { [Op.ne]: 'cancelled' } },
|
|||
|
|
required: false
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return includes
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 批量更新优化
|
|||
|
|
static async batchUpdate(updates) {
|
|||
|
|
const transaction = await sequelize.transaction()
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const results = []
|
|||
|
|
|
|||
|
|
// 分批处理,避免单次更新过多记录
|
|||
|
|
const batchSize = 100
|
|||
|
|
|
|||
|
|
for (let i = 0; i < updates.length; i += batchSize) {
|
|||
|
|
const batch = updates.slice(i, i + batchSize)
|
|||
|
|
|
|||
|
|
const batchPromises = batch.map(update => {
|
|||
|
|
return this.update(
|
|||
|
|
update.data,
|
|||
|
|
{
|
|||
|
|
where: { id: update.id },
|
|||
|
|
transaction,
|
|||
|
|
// 只返回受影响的行数,不返回完整记录
|
|||
|
|
returning: false
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const batchResults = await Promise.all(batchPromises)
|
|||
|
|
results.push(...batchResults)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await transaction.commit()
|
|||
|
|
return results
|
|||
|
|
} catch (error) {
|
|||
|
|
await transaction.rollback()
|
|||
|
|
throw error
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 统计查询优化
|
|||
|
|
static async getStatistics(filters = {}) {
|
|||
|
|
const where = this.buildWhereClause(filters)
|
|||
|
|
|
|||
|
|
// 使用原生SQL进行复杂统计查询
|
|||
|
|
const [results] = await sequelize.query(`
|
|||
|
|
SELECT
|
|||
|
|
COUNT(*) as total_count,
|
|||
|
|
COUNT(CASE WHEN status = 'available' THEN 1 END) as available_count,
|
|||
|
|
COUNT(CASE WHEN status = 'adopted' THEN 1 END) as adopted_count,
|
|||
|
|
COUNT(CASE WHEN status = 'pending' THEN 1 END) as pending_count,
|
|||
|
|
AVG(age) as average_age,
|
|||
|
|
COUNT(CASE WHEN type = 'dog' THEN 1 END) as dog_count,
|
|||
|
|
COUNT(CASE WHEN type = 'cat' THEN 1 END) as cat_count,
|
|||
|
|
COUNT(CASE WHEN created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as recent_count
|
|||
|
|
FROM animals
|
|||
|
|
WHERE ${this.buildSQLWhereClause(where)}
|
|||
|
|
`)
|
|||
|
|
|
|||
|
|
return results[0]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 构建SQL WHERE子句
|
|||
|
|
static buildSQLWhereClause(where) {
|
|||
|
|
// 这里需要根据实际的where对象构建SQL条件
|
|||
|
|
// 简化示例
|
|||
|
|
return '1=1' // 实际实现需要更复杂的逻辑
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 定义模型
|
|||
|
|
Animal.init({
|
|||
|
|
id: {
|
|||
|
|
type: DataTypes.INTEGER,
|
|||
|
|
primaryKey: true,
|
|||
|
|
autoIncrement: true
|
|||
|
|
},
|
|||
|
|
name: {
|
|||
|
|
type: DataTypes.STRING(100),
|
|||
|
|
allowNull: false,
|
|||
|
|
validate: {
|
|||
|
|
notEmpty: true,
|
|||
|
|
len: [1, 100]
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
type: {
|
|||
|
|
type: DataTypes.ENUM('dog', 'cat', 'other'),
|
|||
|
|
allowNull: false,
|
|||
|
|
// 添加索引
|
|||
|
|
index: true
|
|||
|
|
},
|
|||
|
|
breed: {
|
|||
|
|
type: DataTypes.STRING(100),
|
|||
|
|
allowNull: true
|
|||
|
|
},
|
|||
|
|
age: {
|
|||
|
|
type: DataTypes.INTEGER,
|
|||
|
|
allowNull: true,
|
|||
|
|
validate: {
|
|||
|
|
min: 0,
|
|||
|
|
max: 30
|
|||
|
|
},
|
|||
|
|
// 添加索引用于范围查询
|
|||
|
|
index: true
|
|||
|
|
},
|
|||
|
|
gender: {
|
|||
|
|
type: DataTypes.ENUM('male', 'female', 'unknown'),
|
|||
|
|
allowNull: false
|
|||
|
|
},
|
|||
|
|
size: {
|
|||
|
|
type: DataTypes.ENUM('small', 'medium', 'large'),
|
|||
|
|
allowNull: false
|
|||
|
|
},
|
|||
|
|
color: {
|
|||
|
|
type: DataTypes.STRING(50),
|
|||
|
|
allowNull: true
|
|||
|
|
},
|
|||
|
|
description: {
|
|||
|
|
type: DataTypes.TEXT,
|
|||
|
|
allowNull: true
|
|||
|
|
},
|
|||
|
|
health_status: {
|
|||
|
|
type: DataTypes.STRING(200),
|
|||
|
|
allowNull: true
|
|||
|
|
},
|
|||
|
|
vaccination_status: {
|
|||
|
|
type: DataTypes.BOOLEAN,
|
|||
|
|
defaultValue: false
|
|||
|
|
},
|
|||
|
|
sterilization_status: {
|
|||
|
|
type: DataTypes.BOOLEAN,
|
|||
|
|
defaultValue: false
|
|||
|
|
},
|
|||
|
|
status: {
|
|||
|
|
type: DataTypes.ENUM('available', 'adopted', 'pending', 'unavailable'),
|
|||
|
|
allowNull: false,
|
|||
|
|
defaultValue: 'available',
|
|||
|
|
// 添加索引
|
|||
|
|
index: true
|
|||
|
|
},
|
|||
|
|
location: {
|
|||
|
|
type: DataTypes.STRING(200),
|
|||
|
|
allowNull: true,
|
|||
|
|
// 添加索引用于地区查询
|
|||
|
|
index: true
|
|||
|
|
},
|
|||
|
|
shelter_id: {
|
|||
|
|
type: DataTypes.INTEGER,
|
|||
|
|
allowNull: true,
|
|||
|
|
references: {
|
|||
|
|
model: 'shelters',
|
|||
|
|
key: 'id'
|
|||
|
|
},
|
|||
|
|
// 外键索引
|
|||
|
|
index: true
|
|||
|
|
},
|
|||
|
|
rescue_date: {
|
|||
|
|
type: DataTypes.DATE,
|
|||
|
|
allowNull: true
|
|||
|
|
},
|
|||
|
|
is_deleted: {
|
|||
|
|
type: DataTypes.BOOLEAN,
|
|||
|
|
defaultValue: false,
|
|||
|
|
// 软删除索引
|
|||
|
|
index: true
|
|||
|
|
},
|
|||
|
|
created_at: {
|
|||
|
|
type: DataTypes.DATE,
|
|||
|
|
allowNull: false,
|
|||
|
|
defaultValue: DataTypes.NOW,
|
|||
|
|
// 时间索引
|
|||
|
|
index: true
|
|||
|
|
},
|
|||
|
|
updated_at: {
|
|||
|
|
type: DataTypes.DATE,
|
|||
|
|
allowNull: false,
|
|||
|
|
defaultValue: DataTypes.NOW
|
|||
|
|
}
|
|||
|
|
}, {
|
|||
|
|
sequelize,
|
|||
|
|
modelName: 'Animal',
|
|||
|
|
tableName: 'animals',
|
|||
|
|
timestamps: true,
|
|||
|
|
createdAt: 'created_at',
|
|||
|
|
updatedAt: 'updated_at',
|
|||
|
|
// 复合索引
|
|||
|
|
indexes: [
|
|||
|
|
{
|
|||
|
|
name: 'idx_animals_status_type',
|
|||
|
|
fields: ['status', 'type']
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: 'idx_animals_location_status',
|
|||
|
|
fields: ['location', 'status']
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: 'idx_animals_created_status',
|
|||
|
|
fields: ['created_at', 'status']
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: 'idx_animals_age_type',
|
|||
|
|
fields: ['age', 'type']
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
// 默认作用域
|
|||
|
|
defaultScope: {
|
|||
|
|
where: {
|
|||
|
|
is_deleted: false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
// 命名作用域
|
|||
|
|
scopes: {
|
|||
|
|
available: {
|
|||
|
|
where: {
|
|||
|
|
status: 'available',
|
|||
|
|
is_deleted: false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
withImages: {
|
|||
|
|
include: [{
|
|||
|
|
model: require('./AnimalImage'),
|
|||
|
|
as: 'images',
|
|||
|
|
where: { is_deleted: false },
|
|||
|
|
required: false
|
|||
|
|
}]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
module.exports = Animal
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 缓存策略实现
|
|||
|
|
|
|||
|
|
#### Redis缓存服务
|
|||
|
|
```javascript
|
|||
|
|
// services/CacheService.js - 缓存服务
|
|||
|
|
const Redis = require('ioredis')
|
|||
|
|
const logger = require('../utils/logger')
|
|||
|
|
|
|||
|
|
class CacheService {
|
|||
|
|
constructor() {
|
|||
|
|
this.redis = new Redis({
|
|||
|
|
host: process.env.REDIS_HOST || 'localhost',
|
|||
|
|
port: process.env.REDIS_PORT || 6379,
|
|||
|
|
password: process.env.REDIS_PASSWORD,
|
|||
|
|
db: process.env.REDIS_DB || 0,
|
|||
|
|
retryDelayOnFailover: 100,
|
|||
|
|
maxRetriesPerRequest: 3,
|
|||
|
|
lazyConnect: true,
|
|||
|
|
// 连接池配置
|
|||
|
|
family: 4,
|
|||
|
|
keepAlive: true,
|
|||
|
|
// 集群配置(如果使用Redis集群)
|
|||
|
|
enableOfflineQueue: false
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 缓存配置
|
|||
|
|
this.config = {
|
|||
|
|
// 默认过期时间(秒)
|
|||
|
|
defaultTTL: 3600, // 1小时
|
|||
|
|
|
|||
|
|
// 不同类型数据的TTL
|
|||
|
|
ttl: {
|
|||
|
|
user: 1800, // 用户信息 30分钟
|
|||
|
|
animal: 3600, // 动物信息 1小时
|
|||
|
|
statistics: 300, // 统计数据 5分钟
|
|||
|
|
search: 600, // 搜索结果 10分钟
|
|||
|
|
session: 86400, // 会话 24小时
|
|||
|
|
config: 7200 // 配置信息 2小时
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 缓存键前缀
|
|||
|
|
prefix: {
|
|||
|
|
user: 'user:',
|
|||
|
|
animal: 'animal:',
|
|||
|
|
list: 'list:',
|
|||
|
|
search: 'search:',
|
|||
|
|
statistics: 'stats:',
|
|||
|
|
session: 'session:',
|
|||
|
|
lock: 'lock:'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.setupEventHandlers()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置事件处理器
|
|||
|
|
setupEventHandlers() {
|
|||
|
|
this.redis.on('connect', () => {
|
|||
|
|
logger.info('Redis connected')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
this.redis.on('error', (error) => {
|
|||
|
|
logger.error('Redis error:', error)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
this.redis.on('close', () => {
|
|||
|
|
logger.warn('Redis connection closed')
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成缓存键
|
|||
|
|
generateKey(type, identifier, suffix = '') {
|
|||
|
|
const prefix = this.config.prefix[type] || ''
|
|||
|
|
return `${prefix}${identifier}${suffix ? ':' + suffix : ''}`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置缓存
|
|||
|
|
async set(key, value, ttl = null) {
|
|||
|
|
try {
|
|||
|
|
const serializedValue = JSON.stringify(value)
|
|||
|
|
const expireTime = ttl || this.config.defaultTTL
|
|||
|
|
|
|||
|
|
await this.redis.setex(key, expireTime, serializedValue)
|
|||
|
|
|
|||
|
|
logger.debug(`Cache set: ${key} (TTL: ${expireTime}s)`)
|
|||
|
|
return true
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('Cache set error:', error)
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取缓存
|
|||
|
|
async get(key) {
|
|||
|
|
try {
|
|||
|
|
const value = await this.redis.get(key)
|
|||
|
|
|
|||
|
|
if (value === null) {
|
|||
|
|
logger.debug(`Cache miss: ${key}`)
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logger.debug(`Cache hit: ${key}`)
|
|||
|
|
return JSON.parse(value)
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('Cache get error:', error)
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 删除缓存
|
|||
|
|
async del(key) {
|
|||
|
|
try {
|
|||
|
|
const result = await this.redis.del(key)
|
|||
|
|
logger.debug(`Cache deleted: ${key}`)
|
|||
|
|
return result > 0
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('Cache delete error:', error)
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 批量删除缓存
|
|||
|
|
async delPattern(pattern) {
|
|||
|
|
try {
|
|||
|
|
const keys = await this.redis.keys(pattern)
|
|||
|
|
|
|||
|
|
if (keys.length > 0) {
|
|||
|
|
const result = await this.redis.del(...keys)
|
|||
|
|
logger.debug(`Cache pattern deleted: ${pattern} (${keys.length} keys)`)
|
|||
|
|
return result
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return 0
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('Cache pattern delete error:', error)
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查缓存是否存在
|
|||
|
|
async exists(key) {
|
|||
|
|
try {
|
|||
|
|
const result = await this.redis.exists(key)
|
|||
|
|
return result === 1
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('Cache exists check error:', error)
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置缓存过期时间
|
|||
|
|
async expire(key, ttl) {
|
|||
|
|
try {
|
|||
|
|
const result = await this.redis.expire(key, ttl)
|
|||
|
|
return result === 1
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('Cache expire error:', error)
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取或设置缓存(缓存穿透保护)
|
|||
|
|
async getOrSet(key, fetchFunction, ttl = null) {
|
|||
|
|
try {
|
|||
|
|
// 先尝试从缓存获取
|
|||
|
|
let value = await this.get(key)
|
|||
|
|
|
|||
|
|
if (value !== null) {
|
|||
|
|
return value
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用分布式锁防止缓存击穿
|
|||
|
|
const lockKey = this.generateKey('lock', key)
|
|||
|
|
const lockAcquired = await this.acquireLock(lockKey, 10) // 10秒锁
|
|||
|
|
|
|||
|
|
if (!lockAcquired) {
|
|||
|
|
// 如果获取锁失败,等待一段时间后重试
|
|||
|
|
await this.sleep(100)
|
|||
|
|
value = await this.get(key)
|
|||
|
|
if (value !== null) {
|
|||
|
|
return value
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 执行数据获取函数
|
|||
|
|
value = await fetchFunction()
|
|||
|
|
|
|||
|
|
// 缓存结果(即使是null也要缓存,防止缓存穿透)
|
|||
|
|
const cacheValue = value !== null ? value : { __null: true }
|
|||
|
|
const expireTime = ttl || this.getTTL(key)
|
|||
|
|
|
|||
|
|
await this.set(key, cacheValue, expireTime)
|
|||
|
|
|
|||
|
|
return value
|
|||
|
|
} finally {
|
|||
|
|
// 释放锁
|
|||
|
|
if (lockAcquired) {
|
|||
|
|
await this.releaseLock(lockKey)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('Cache getOrSet error:', error)
|
|||
|
|
// 如果缓存操作失败,直接执行函数
|
|||
|
|
return await fetchFunction()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取TTL
|
|||
|
|
getTTL(key) {
|
|||
|
|
// 根据键名确定TTL
|
|||
|
|
for (const [type, prefix] of Object.entries(this.config.prefix)) {
|
|||
|
|
if (key.startsWith(prefix)) {
|
|||
|
|
return this.config.ttl[type] || this.config.defaultTTL
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return this.config.defaultTTL
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取分布式锁
|
|||
|
|
async acquireLock(lockKey, expireTime = 10) {
|
|||
|
|
try {
|
|||
|
|
const result = await this.redis.set(lockKey, '1', 'EX', expireTime, 'NX')
|
|||
|
|
return result === 'OK'
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('Lock acquire error:', error)
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 释放分布式锁
|
|||
|
|
async releaseLock(lockKey) {
|
|||
|
|
try {
|
|||
|
|
await this.redis.del(lockKey)
|
|||
|
|
return true
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('Lock release error:', error)
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 睡眠函数
|
|||
|
|
sleep(ms) {
|
|||
|
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 缓存用户信息
|
|||
|
|
async cacheUser(userId, userData) {
|
|||
|
|
const key = this.generateKey('user', userId)
|
|||
|
|
return await this.set(key, userData, this.config.ttl.user)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取用户缓存
|
|||
|
|
async getUser(userId) {
|
|||
|
|
const key = this.generateKey('user', userId)
|
|||
|
|
return await this.get(key)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 删除用户缓存
|
|||
|
|
async deleteUser(userId) {
|
|||
|
|
const key = this.generateKey('user', userId)
|
|||
|
|
return await this.del(key)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 缓存动物列表
|
|||
|
|
async cacheAnimalList(filters, data) {
|
|||
|
|
const key = this.generateKey('list', 'animals', this.hashFilters(filters))
|
|||
|
|
return await this.set(key, data, this.config.ttl.animal)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取动物列表缓存
|
|||
|
|
async getAnimalList(filters) {
|
|||
|
|
const key = this.generateKey('list', 'animals', this.hashFilters(filters))
|
|||
|
|
return await this.get(key)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 删除动物相关缓存
|
|||
|
|
async deleteAnimalCache(animalId = null) {
|
|||
|
|
if (animalId) {
|
|||
|
|
// 删除特定动物缓存
|
|||
|
|
const key = this.generateKey('animal', animalId)
|
|||
|
|
await this.del(key)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 删除所有动物列表缓存
|
|||
|
|
await this.delPattern(this.generateKey('list', 'animals', '*'))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 哈希过滤器参数
|
|||
|
|
hashFilters(filters) {
|
|||
|
|
const crypto = require('crypto')
|
|||
|
|
const filterString = JSON.stringify(filters, Object.keys(filters).sort())
|
|||
|
|
return crypto.createHash('md5').update(filterString).digest('hex')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 缓存统计数据
|
|||
|
|
async cacheStatistics(type, data) {
|
|||
|
|
const key = this.generateKey('statistics', type)
|
|||
|
|
return await this.set(key, data, this.config.ttl.statistics)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取统计缓存
|
|||
|
|
async getStatistics(type) {
|
|||
|
|
const key = this.generateKey('statistics', type)
|
|||
|
|
return await this.get(key)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 批量操作
|
|||
|
|
async mget(keys) {
|
|||
|
|
try {
|
|||
|
|
const values = await this.redis.mget(...keys)
|
|||
|
|
return values.map(value => value ? JSON.parse(value) : null)
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('Cache mget error:', error)
|
|||
|
|
return new Array(keys.length).fill(null)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async mset(keyValuePairs, ttl = null) {
|
|||
|
|
try {
|
|||
|
|
const pipeline = this.redis.pipeline()
|
|||
|
|
const expireTime = ttl || this.config.defaultTTL
|
|||
|
|
|
|||
|
|
for (const [key, value] of keyValuePairs) {
|
|||
|
|
pipeline.setex(key, expireTime, JSON.stringify(value))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await pipeline.exec()
|
|||
|
|
return true
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('Cache mset error:', error)
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 关闭连接
|
|||
|
|
async close() {
|
|||
|
|
await this.redis.quit()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = new CacheService()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### API响应优化
|
|||
|
|
|
|||
|
|
#### 响应压缩和优化中间件
|
|||
|
|
```javascript
|
|||
|
|
// middleware/optimization.js - 性能优化中间件
|
|||
|
|
const compression = require('compression')
|
|||
|
|
const helmet = require('helmet')
|
|||
|
|
const rateLimit = require('express-rate-limit')
|
|||
|
|
const slowDown = require('express-slow-down')
|
|||
|
|
const responseTime = require('response-time')
|
|||
|
|
const cacheService = require('../services/CacheService')
|
|||
|
|
const logger = require('../utils/logger')
|
|||
|
|
|
|||
|
|
// 响应压缩中间件
|
|||
|
|
const compressionMiddleware = compression({
|
|||
|
|
// 压缩级别 (1-9, 9最高)
|
|||
|
|
level: 6,
|
|||
|
|
// 压缩阈值,小于1KB的响应不压缩
|
|||
|
|
threshold: 1024,
|
|||
|
|
// 过滤器函数
|
|||
|
|
filter: (req, res) => {
|
|||
|
|
// 不压缩已经压缩的内容
|
|||
|
|
if (req.headers['x-no-compression']) {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 只压缩文本内容
|
|||
|
|
const contentType = res.getHeader('content-type')
|
|||
|
|
if (contentType) {
|
|||
|
|
return /text|json|javascript|css|xml|svg/.test(contentType)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return compression.filter(req, res)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 安全头中间件
|
|||
|
|
const securityMiddleware = helmet({
|
|||
|
|
contentSecurityPolicy: {
|
|||
|
|
directives: {
|
|||
|
|
defaultSrc: ["'self'"],
|
|||
|
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
|||
|
|
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
|||
|
|
imgSrc: ["'self'", "data:", "https:"],
|
|||
|
|
scriptSrc: ["'self'"],
|
|||
|
|
connectSrc: ["'self'", "https://api.jiebanke.com"]
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
hsts: {
|
|||
|
|
maxAge: 31536000,
|
|||
|
|
includeSubDomains: true,
|
|||
|
|
preload: true
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 速率限制中间件
|
|||
|
|
const rateLimitMiddleware = rateLimit({
|
|||
|
|
windowMs: 15 * 60 * 1000, // 15分钟
|
|||
|
|
max: 1000, // 每个IP最多1000次请求
|
|||
|
|
message: {
|
|||
|
|
error: 'Too many requests',
|
|||
|
|
message: '请求过于频繁,请稍后再试'
|
|||
|
|
},
|
|||
|
|
standardHeaders: true,
|
|||
|
|
legacyHeaders: false,
|
|||
|
|
// 自定义键生成器
|
|||
|
|
keyGenerator: (req) => {
|
|||
|
|
// 优先使用用户ID,其次使用IP
|
|||
|
|
return req.user?.id || req.ip
|
|||
|
|
},
|
|||
|
|
// 跳过成功的请求
|
|||
|
|
skipSuccessfulRequests: false,
|
|||
|
|
// 跳过失败的请求
|
|||
|
|
skipFailedRequests: true
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// API速率限制(更严格)
|
|||
|
|
const apiRateLimitMiddleware = rateLimit({
|
|||
|
|
windowMs: 15 * 60 * 1000, // 15分钟
|
|||
|
|
max: 500, // API请求限制更严格
|
|||
|
|
message: {
|
|||
|
|
error: 'API rate limit exceeded',
|
|||
|
|
message: 'API请求频率超限,请稍后再试'
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 慢速响应中间件
|
|||
|
|
const slowDownMiddleware = slowDown({
|
|||
|
|
windowMs: 15 * 60 * 1000, // 15分钟
|
|||
|
|
delayAfter: 100, // 100次请求后开始延迟
|
|||
|
|
delayMs: 500, // 每次增加500ms延迟
|
|||
|
|
maxDelayMs: 20000 // 最大延迟20秒
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 响应时间中间件
|
|||
|
|
const responseTimeMiddleware = responseTime((req, res, time) => {
|
|||
|
|
// 记录慢查询
|
|||
|
|
if (time > 1000) { // 超过1秒
|
|||
|
|
logger.warn('Slow request detected', {
|
|||
|
|
method: req.method,
|
|||
|
|
url: req.url,
|
|||
|
|
responseTime: time,
|
|||
|
|
userAgent: req.get('User-Agent'),
|
|||
|
|
ip: req.ip
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加响应时间头
|
|||
|
|
res.set('X-Response-Time', `${time}ms`)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 缓存中间件
|
|||
|
|
const cacheMiddleware = (options = {}) => {
|
|||
|
|
const {
|
|||
|
|
ttl = 300, // 默认5分钟
|
|||
|
|
keyGenerator = (req) => `${req.method}:${req.originalUrl}`,
|
|||
|
|
condition = () => true,
|
|||
|
|
vary = ['Accept-Encoding']
|
|||
|
|
} = options
|
|||
|
|
|
|||
|
|
return async (req, res, next) => {
|
|||
|
|
// 只缓存GET请求
|
|||
|
|
if (req.method !== 'GET') {
|
|||
|
|
return next()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查缓存条件
|
|||
|
|
if (!condition(req)) {
|
|||
|
|
return next()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const cacheKey = keyGenerator(req)
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 尝试从缓存获取
|
|||
|
|
const cachedResponse = await cacheService.get(cacheKey)
|
|||
|
|
|
|||
|
|
if (cachedResponse) {
|
|||
|
|
// 设置缓存头
|
|||
|
|
res.set('X-Cache', 'HIT')
|
|||
|
|
res.set('Cache-Control', `public, max-age=${ttl}`)
|
|||
|
|
|
|||
|
|
// 设置Vary头
|
|||
|
|
if (vary.length > 0) {
|
|||
|
|
res.vary(vary)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 返回缓存的响应
|
|||
|
|
return res.status(cachedResponse.status).json(cachedResponse.data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 缓存未命中,继续处理请求
|
|||
|
|
res.set('X-Cache', 'MISS')
|
|||
|
|
|
|||
|
|
// 拦截响应
|
|||
|
|
const originalJson = res.json
|
|||
|
|
res.json = function(data) {
|
|||
|
|
// 只缓存成功的响应
|
|||
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|||
|
|
const responseData = {
|
|||
|
|
status: res.statusCode,
|
|||
|
|
data: data
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 异步缓存,不阻塞响应
|
|||
|
|
cacheService.set(cacheKey, responseData, ttl).catch(error => {
|
|||
|
|
logger.error('Cache set error:', error)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调用原始json方法
|
|||
|
|
return originalJson.call(this, data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
next()
|
|||
|
|
} catch (error) {
|
|||
|
|
logger.error('Cache middleware error:', error)
|
|||
|
|
next()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ETag中间件
|
|||
|
|
const etagMiddleware = (req, res, next) => {
|
|||
|
|
const originalJson = res.json
|
|||
|
|
|
|||
|
|
res.json = function(data) {
|
|||
|
|
if (req.method === 'GET' && res.statusCode === 200) {
|
|||
|
|
const crypto = require('crypto')
|
|||
|
|
const etag = crypto.createHash('md5').update(JSON.stringify(data)).digest('hex')
|
|||
|
|
|
|||
|
|
res.set('ETag', `"${etag}"`)
|
|||
|
|
|
|||
|
|
// 检查If-None-Match头
|
|||
|
|
const clientEtag = req.get('If-None-Match')
|
|||
|
|
if (clientEtag === `"${etag}"`) {
|
|||
|
|
return res.status(304).end()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return originalJson.call(this, data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
next()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 请求日志中间件
|
|||
|
|
const requestLogMiddleware = (req, res, next) => {
|
|||
|
|
const startTime = Date.now()
|
|||
|
|
|
|||
|
|
// 记录请求开始
|
|||
|
|
logger.info('Request started', {
|
|||
|
|
method: req.method,
|
|||
|
|
url: req.url,
|
|||
|
|
ip: req.ip,
|
|||
|
|
userAgent: req.get('User-Agent'),
|
|||
|
|
contentLength: req.get('Content-Length')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 监听响应结束
|
|||
|
|
res.on('finish', () => {
|
|||
|
|
const duration = Date.now() - startTime
|
|||
|
|
|
|||
|
|
logger.info('Request completed', {
|
|||
|
|
method: req.method,
|
|||
|
|
url: req.url,
|
|||
|
|
statusCode: res.statusCode,
|
|||
|
|
duration: duration,
|
|||
|
|
contentLength: res.get('Content-Length')
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
next()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 健康检查中间件
|
|||
|
|
const healthCheckMiddleware = (req, res, next) => {
|
|||
|
|
if (req.path === '/health' || req.path === '/ping') {
|
|||
|
|
return res.json({
|
|||
|
|
status: 'ok',
|
|||
|
|
timestamp: new Date().toISOString(),
|
|||
|
|
uptime: process.uptime(),
|
|||
|
|
memory: process.memoryUsage(),
|
|||
|
|
version: process.env.npm_package_version || '1.0.0'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
next()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 错误处理中间件
|
|||
|
|
const errorHandlingMiddleware = (error, req, res, next) => {
|
|||
|
|
logger.error('Request error', {
|
|||
|
|
error: error.message,
|
|||
|
|
stack: error.stack,
|
|||
|
|
method: req.method,
|
|||
|
|
url: req.url,
|
|||
|
|
ip: req.ip
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 根据错误类型返回不同响应
|
|||
|
|
if (error.name === 'ValidationError') {
|
|||
|
|
return res.status(400).json({
|
|||
|
|
error: 'Validation Error',
|
|||
|
|
message: error.message,
|
|||
|
|
details: error.details
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (error.name === 'UnauthorizedError') {
|
|||
|
|
return res.status(401).json({
|
|||
|
|
error: 'Unauthorized',
|
|||
|
|
message: '认证失败'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (error.name === 'ForbiddenError') {
|
|||
|
|
return res.status(403).json({
|
|||
|
|
error: 'Forbidden',
|
|||
|
|
message: '权限不足'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (error.name === 'NotFoundError') {
|
|||
|
|
return res.status(404).json({
|
|||
|
|
error: 'Not Found',
|
|||
|
|
message: '资源不存在'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 默认服务器错误
|
|||
|
|
res.status(500).json({
|
|||
|
|
error: 'Internal Server Error',
|
|||
|
|
message: process.env.NODE_ENV === 'production' ? '服务器内部错误' : error.message
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = {
|
|||
|
|
compressionMiddleware,
|
|||
|
|
securityMiddleware,
|
|||
|
|
rateLimitMiddleware,
|
|||
|
|
apiRateLimitMiddleware,
|
|||
|
|
slowDownMiddleware,
|
|||
|
|
responseTimeMiddleware,
|
|||
|
|
cacheMiddleware,
|
|||
|
|
etagMiddleware,
|
|||
|
|
requestLogMiddleware,
|
|||
|
|
healthCheckMiddleware,
|
|||
|
|
errorHandlingMiddleware
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 📊 监控和分析
|
|||
|
|
|
|||
|
|
### 性能监控仪表板
|
|||
|
|
```javascript
|
|||
|
|
// utils/PerformanceAnalyzer.js - 性能分析工具
|
|||
|
|
class PerformanceAnalyzer {
|
|||
|
|
constructor() {
|
|||
|
|
this.metrics = new Map()
|
|||
|
|
this.alerts = []
|
|||
|
|
this.thresholds = {
|
|||
|
|
responseTime: {
|
|||
|
|
warning: 500, // 500ms
|
|||
|
|
critical: 1000 // 1s
|
|||
|
|
},
|
|||
|
|
errorRate: {
|
|||
|
|
warning: 0.01, // 1%
|
|||
|
|
critical: 0.05 // 5%
|
|||
|
|
},
|
|||
|
|
throughput: {
|
|||
|
|
warning: 100, // 100 RPS
|
|||
|
|
critical: 50 // 50 RPS
|
|||
|
|
},
|
|||
|
|
memoryUsage: {
|
|||
|
|
warning: 0.8, // 80%
|
|||
|
|
critical: 0.9 // 90%
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 记录性能指标
|
|||
|
|
recordMetric(name, value, tags = {}) {
|
|||
|
|
const timestamp = Date.now()
|
|||
|
|
const metric = {
|
|||
|
|
name,
|
|||
|
|
value,
|
|||
|
|
timestamp,
|
|||
|
|
tags
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!this.metrics.has(name)) {
|
|||
|
|
this.metrics.set(name, [])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.metrics.get(name).push(metric)
|
|||
|
|
|
|||
|
|
// 保持最近1000条记录
|
|||
|
|
const records = this.metrics.get(name)
|
|||
|
|
if (records.length > 1000) {
|
|||
|
|
records.splice(0, records.length - 1000)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查阈值
|
|||
|
|
this.checkThresholds(name, value, tags)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查阈值
|
|||
|
|
checkThresholds(metricName, value, tags) {
|
|||
|
|
const threshold = this.thresholds[metricName]
|
|||
|
|
if (!threshold) return
|
|||
|
|
|
|||
|
|
let level = null
|
|||
|
|
if (value >= threshold.critical) {
|
|||
|
|
level = 'critical'
|
|||
|
|
} else if (value >= threshold.warning) {
|
|||
|
|
level = 'warning'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (level) {
|
|||
|
|
this.triggerAlert({
|
|||
|
|
metric: metricName,
|
|||
|
|
value,
|
|||
|
|
level,
|
|||
|
|
threshold: threshold[level],
|
|||
|
|
tags,
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 触发告警
|
|||
|
|
triggerAlert(alert) {
|
|||
|
|
this.alerts.push(alert)
|
|||
|
|
|
|||
|
|
// 保持最近100条告警
|
|||
|
|
if (this.alerts.length > 100) {
|
|||
|
|
this.alerts.splice(0, this.alerts.length - 100)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送告警通知
|
|||
|
|
this.sendAlert(alert)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送告警
|
|||
|
|
async sendAlert(alert) {
|
|||
|
|
const message = `性能告警: ${alert.metric} = ${alert.value} (阈值: ${alert.threshold})`
|
|||
|
|
|
|||
|
|
console.warn(message, alert)
|
|||
|
|
|
|||
|
|
// 这里可以集成邮件、短信、钉钉等告警通道
|
|||
|
|
try {
|
|||
|
|
// 发送到监控系统
|
|||
|
|
await fetch('/api/monitoring/alerts', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify(alert)
|
|||
|
|
})
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to send alert:', error)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取性能报告
|
|||
|
|
getPerformanceReport(timeRange = 3600000) { // 默认1小时
|
|||
|
|
const now = Date.now()
|
|||
|
|
const startTime = now - timeRange
|
|||
|
|
|
|||
|
|
const report = {
|
|||
|
|
timeRange: {
|
|||
|
|
start: new Date(startTime).toISOString(),
|
|||
|
|
end: new Date(now).toISOString()
|
|||
|
|
},
|
|||
|
|
metrics: {},
|
|||
|
|
alerts: this.alerts.filter(alert => alert.timestamp >= startTime),
|
|||
|
|
summary: {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 分析各项指标
|
|||
|
|
for (const [name, records] of this.metrics.entries()) {
|
|||
|
|
const filteredRecords = records.filter(record => record.timestamp >= startTime)
|
|||
|
|
|
|||
|
|
if (filteredRecords.length === 0) continue
|
|||
|
|
|
|||
|
|
const values = filteredRecords.map(record => record.value)
|
|||
|
|
|
|||
|
|
report.metrics[name] = {
|
|||
|
|
count: filteredRecords.length,
|
|||
|
|
min: Math.min(...values),
|
|||
|
|
max: Math.max(...values),
|
|||
|
|
avg: values.reduce((sum, val) => sum + val, 0) / values.length,
|
|||
|
|
p50: this.percentile(values, 0.5),
|
|||
|
|
p95: this.percentile(values, 0.95),
|
|||
|
|
p99: this.percentile(values, 0.99)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成摘要
|
|||
|
|
report.summary = this.generateSummary(report)
|
|||
|
|
|
|||
|
|
return report
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算百分位数
|
|||
|
|
percentile(values, p) {
|
|||
|
|
const sorted = values.slice().sort((a, b) => a - b)
|
|||
|
|
const index = Math.ceil(sorted.length * p) - 1
|
|||
|
|
return sorted[index] || 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成性能摘要
|
|||
|
|
generateSummary(report) {
|
|||
|
|
const summary = {
|
|||
|
|
status: 'healthy',
|
|||
|
|
issues: [],
|
|||
|
|
recommendations: []
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查响应时间
|
|||
|
|
const responseTime = report.metrics.responseTime
|
|||
|
|
if (responseTime) {
|
|||
|
|
if (responseTime.p95 > this.thresholds.responseTime.critical) {
|
|||
|
|
summary.status = 'critical'
|
|||
|
|
summary.issues.push('95%的请求响应时间超过1秒')
|
|||
|
|
summary.recommendations.push('优化数据库查询和缓存策略')
|
|||
|
|
} else if (responseTime.p95 > this.thresholds.responseTime.warning) {
|
|||
|
|
summary.status = 'warning'
|
|||
|
|
summary.issues.push('95%的请求响应时间超过500ms')
|
|||
|
|
summary.recommendations.push('检查慢查询和网络延迟')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查错误率
|
|||
|
|
const errorRate = report.metrics.errorRate
|
|||
|
|
if (errorRate && errorRate.avg > this.thresholds.errorRate.warning) {
|
|||
|
|
summary.status = summary.status === 'critical' ? 'critical' : 'warning'
|
|||
|
|
summary.issues.push(`错误率过高: ${(errorRate.avg * 100).toFixed(2)}%`)
|
|||
|
|
summary.recommendations.push('检查应用日志和错误处理')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查内存使用
|
|||
|
|
const memoryUsage = report.metrics.memoryUsage
|
|||
|
|
if (memoryUsage && memoryUsage.max > this.thresholds.memoryUsage.critical) {
|
|||
|
|
summary.status = 'critical'
|
|||
|
|
summary.issues.push('内存使用率超过90%')
|
|||
|
|
summary.recommendations.push('检查内存泄漏和优化内存使用')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return summary
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = PerformanceAnalyzer
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 🎯 总结
|
|||
|
|
|
|||
|
|
本性能优化文档提供了解班客项目的全方位性能优化方案,包括:
|
|||
|
|
|
|||
|
|
### 核心优化策略
|
|||
|
|
1. **前端优化**:代码分割、懒加载、虚拟滚动、图片优化
|
|||
|
|
2. **后端优化**:数据库查询优化、缓存策略、API响应优化
|
|||
|
|
3. **监控体系**:性能指标监控、告警机制、性能分析
|
|||
|
|
|
|||
|
|
### 性能目标
|
|||
|
|
- 页面加载时间 < 2.5秒
|
|||
|
|
- API响应时间 < 500ms (95%)
|
|||
|
|
- 系统可用性 > 99.9%
|
|||
|
|
- 并发用户数 > 500
|
|||
|
|
|
|||
|
|
### 下一步计划
|
|||
|
|
1. 实施性能监控系统
|
|||
|
|
2. 优化关键路径性能
|
|||
|
|
3. 建立性能基准测试
|
|||
|
|
4. 持续性能优化迭代
|
|||
|
|
|
|||
|
|
通过系统性的性能优化,确保解班客项目能够为用户提供快速、稳定、高质量的服务体验。
|