常见问题诊断
水合错误(Hydration Mismatch)
问题表现
// 控制台错误信息
[Vue warn]: Hydration node mismatch:
- Client vnode: div
- Server rendered DOM: span
// 或者
[Vue warn]: Hydration text content mismatch in <div>:
- Client: "Hello World"
- Server: "你好世界"
解决方案
<!-- 错误示例:客户端和服务端渲染不一致 -->
<template>
<div>
<!-- 时间戳会导致水合错误 -->
<span>{{ new Date().toISOString() }}</span>
<!-- 随机数会导致水合错误 -->
<div>{{ Math.random() }}</div>
<!-- 浏览器特定API会导致错误 -->
<p v-if="window.innerWidth > 768">桌面版本</p>
</div>
</template>
<!-- 正确示例:确保一致性 -->
<template>
<div>
<!-- 使用客户端渲染包装器 -->
<ClientOnly>
<span>{{ currentTime }}</span>
<template #fallback>
<span>加载中...</span>
</template>
</ClientOnly>
<!-- 使用响应式数据 -->
<div v-if="isMounted && isDesktop">桌面版本</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const currentTime = ref('')
const isMounted = ref(false)
const isDesktop = ref(false)
onMounted(() => {
isMounted.value = true
isDesktop.value = window.innerWidth > 768
currentTime.value = new Date().toISOString()
})
</script>
ClientOnly组件实现
<!-- components/ClientOnly.vue -->
<template>
<div v-if="isMounted">
<slot />
</div>
<div v-else>
<slot name="fallback" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const isMounted = ref(false)
onMounted(() => {
isMounted.value = true
})
</script>
内存泄漏问题
检测工具
// server/monitoring/memory.js
import v8 from 'v8'
import { performance } from 'perf_hooks'
class MemoryMonitor {
constructor() {
this.snapshots = []
this.interval = null
}
start(intervalMs = 30000) {
this.interval = setInterval(() => {
this.takeSnapshot()
}, intervalMs)
}
stop() {
if (this.interval) {
clearInterval(this.interval)
this.interval = null
}
}
takeSnapshot() {
const memUsage = process.memoryUsage()
const heapStats = v8.getHeapStatistics()
const snapshot = {
timestamp: Date.now(),
rss: memUsage.rss,
heapUsed: memUsage.heapUsed,
heapTotal: memUsage.heapTotal,
external: memUsage.external,
heapSizeLimit: heapStats.heap_size_limit,
totalHeapSize: heapStats.total_heap_size,
usedHeapSize: heapStats.used_heap_size
}
this.snapshots.push(snapshot)
// 保留最近100个快照
if (this.snapshots.length > 100) {
this.snapshots.shift()
}
// 检查内存泄漏
this.checkMemoryLeak(snapshot)
}
checkMemoryLeak(current) {
if (this.snapshots.length < 10) return
const recent = this.snapshots.slice(-10)
const trend = this.calculateTrend(recent.map(s => s.heapUsed))
// 如果内存使用持续增长
if (trend > 1024 * 1024) { // 1MB增长趋势
console.warn('检测到可能的内存泄漏:', {
trend: `${(trend / 1024 / 1024).toFixed(2)}MB`,
currentHeap: `${(current.heapUsed / 1024 / 1024).toFixed(2)}MB`,
heapLimit: `${(current.heapSizeLimit / 1024 / 1024).toFixed(2)}MB`
})
// 可以在这里触发告警或自动重启
this.handleMemoryLeak(current)
}
}
calculateTrend(values) {
if (values.length < 2) return 0
const n = values.length
const sumX = (n * (n - 1)) / 2
const sumY = values.reduce((a, b) => a + b, 0)
const sumXY = values.reduce((sum, y, x) => sum + x * y, 0)
const sumX2 = values.reduce((sum, _, x) => sum + x * x, 0)
return (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX)
}
handleMemoryLeak(snapshot) {
// 生成堆快照
if (process.env.NODE_ENV === 'development') {
const heapSnapshot = v8.writeHeapSnapshot()
console.log('堆快照已保存:', heapSnapshot)
}
// 强制垃圾回收
if (global.gc) {
global.gc()
}
}
getStats() {
if (this.snapshots.length === 0) return null
const latest = this.snapshots[this.snapshots.length - 1]
const oldest = this.snapshots[0]
return {
current: {
rss: `${(latest.rss / 1024 / 1024).toFixed(2)}MB`,
heapUsed: `${(latest.heapUsed / 1024 / 1024).toFixed(2)}MB`,
heapTotal: `${(latest.heapTotal / 1024 / 1024).toFixed(2)}MB`
},
trend: {
rss: latest.rss - oldest.rss,
heapUsed: latest.heapUsed - oldest.heapUsed,
heapTotal: latest.heapTotal - oldest.heapTotal
},
snapshots: this.snapshots.length
}
}
}
export const memoryMonitor = new MemoryMonitor()
// 在应用启动时开始监控
if (process.env.NODE_ENV === 'production') {
memoryMonitor.start()
}
性能问题诊断
渲染性能分析
// utils/performance.js
import { performance } from 'perf_hooks'
class PerformanceProfiler {
constructor() {
this.marks = new Map()
this.measures = []
}
mark(name) {
const markName = `${name}-${Date.now()}`
performance.mark(markName)
this.marks.set(name, markName)
return markName
}
measure(name, startMark) {
const endMark = this.mark(`${name}-end`)
const startMarkName = this.marks.get(startMark) || startMark
try {
performance.measure(name, startMarkName, endMark)
const measure = performance.getEntriesByName(name, 'measure')[0]
this.measures.push({
name,
duration: measure.duration,
timestamp: Date.now()
})
return measure.duration
} catch (error) {
console.warn('性能测量失败:', error.message)
return 0
}
}
getReport() {
const report = {
totalMeasures: this.measures.length,
averages: {},
slowest: {},
recent: this.measures.slice(-10)
}
// 按名称分组计算平均值
const grouped = this.measures.reduce((acc, measure) => {
if (!acc[measure.name]) {
acc[measure.name] = []
}
acc[measure.name].push(measure.duration)
return acc
}, {})
Object.entries(grouped).forEach(([name, durations]) => {
const avg = durations.reduce((a, b) => a + b, 0) / durations.length
const max = Math.max(...durations)
report.averages[name] = `${avg.toFixed(2)}ms`
report.slowest[name] = `${max.toFixed(2)}ms`
})
return report
}
clear() {
this.marks.clear()
this.measures = []
performance.clearMarks()
performance.clearMeasures()
}
}
export const profiler = new PerformanceProfiler()
// SSR性能中间件
export function ssrPerformanceMiddleware(req, res, next) {
const requestId = `request-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
profiler.mark(`${requestId}-start`)
// 记录各个阶段
const originalRender = res.render
res.render = function(view, locals, callback) {
profiler.mark(`${requestId}-render-start`)
const renderCallback = (err, html) => {
profiler.measure(`${requestId}-render`, `${requestId}-render-start`)
if (callback) {
callback(err, html)
} else if (err) {
throw err
} else {
res.send(html)
}
}
originalRender.call(this, view, locals, renderCallback)
}
res.on('finish', () => {
const totalDuration = profiler.measure(`${requestId}-total`, `${requestId}-start`)
// 记录慢请求
if (totalDuration > 1000) { // 超过1秒
console.warn('慢请求检测:', {
url: req.url,
method: req.method,
duration: `${totalDuration.toFixed(2)}ms`,
userAgent: req.get('User-Agent')
})
}
})
next()
}
调试工具配置
Vue DevTools配置
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 开发环境启用Vue DevTools
if (process.env.NODE_ENV === 'development') {
app.config.devtools = true
// 启用性能追踪
app.config.performance = true
// 自定义DevTools配置
if (typeof window !== 'undefined') {
window.__VUE_DEVTOOLS_GLOBAL_HOOK__ = window.__VUE_DEVTOOLS_GLOBAL_HOOK__ || {}
window.__VUE_DEVTOOLS_GLOBAL_HOOK__.Vue = app
}
}
export { app }
服务端调试配置
// server/debug.js
import debug from 'debug'
import util from 'util'
// 创建不同模块的调试器
export const debugSSR = debug('app:ssr')
export const debugRoute = debug('app:route')
export const debugCache = debug('app:cache')
export const debugDB = debug('app:db')
export const debugAuth = debug('app:auth')
// 格式化调试输出
function formatDebugOutput(namespace, data) {
const timestamp = new Date().toISOString()
const formatted = typeof data === 'object'
? util.inspect(data, { colors: true, depth: 3 })
: data
return `[${timestamp}] ${namespace}: ${formatted}`
}
// 增强的调试函数
export function createDebugger(namespace) {
const debugFn = debug(namespace)
return {
log: (data) => debugFn(formatDebugOutput(namespace, data)),
error: (error) => {
debugFn(formatDebugOutput(namespace, {
message: error.message,
stack: error.stack,
timestamp: Date.now()
}))
},
time: (label) => {
const start = Date.now()
return () => {
const duration = Date.now() - start
debugFn(formatDebugOutput(namespace, `${label}: ${duration}ms`))
}
}
}
}
// 使用示例
const debug = createDebugger('app:example')
// 计时调试
const endTimer = debug.time('数据库查询')
// ... 执行数据库查询
endTimer() // 输出: 数据库查询: 150ms
错误追踪配置
// server/error-tracking.js
import * as Sentry from '@sentry/node'
import { createDebugger } from './debug.js'
const debug = createDebugger('app:error')
class ErrorTracker {
constructor() {
this.errors = []
this.maxErrors = 100
}
track(error, context = {}) {
const errorInfo = {
id: this.generateErrorId(),
message: error.message,
stack: error.stack,
timestamp: Date.now(),
context,
severity: this.determineSeverity(error)
}
// 添加到本地错误列表
this.errors.unshift(errorInfo)
if (this.errors.length > this.maxErrors) {
this.errors.pop()
}
// 发送到Sentry
if (Sentry.getCurrentHub().getClient()) {
Sentry.withScope((scope) => {
scope.setContext('error_context', context)
scope.setLevel(errorInfo.severity)
Sentry.captureException(error)
})
}
// 本地调试输出
debug.error(errorInfo)
return errorInfo.id
}
generateErrorId() {
return `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
determineSeverity(error) {
if (error.name === 'ValidationError') return 'warning'
if (error.status >= 400 && error.status < 500) return 'warning'
if (error.status >= 500) return 'error'
if (error.name === 'TypeError' || error.name === 'ReferenceError') return 'error'
return 'info'
}
getRecentErrors(limit = 10) {
return this.errors.slice(0, limit)
}
getErrorById(id) {
return this.errors.find(error => error.id === id)
}
getErrorStats() {
const now = Date.now()
const oneHour = 60 * 60 * 1000
const oneDay = 24 * oneHour
const recentErrors = this.errors.filter(error =>
now - error.timestamp < oneHour
)
const dailyErrors = this.errors.filter(error =>
now - error.timestamp < oneDay
)
const severityCount = this.errors.reduce((acc, error) => {
acc[error.severity] = (acc[error.severity] || 0) + 1
return acc
}, {})
return {
total: this.errors.length,
lastHour: recentErrors.length,
lastDay: dailyErrors.length,
bySeverity: severityCount
}
}
}
export const errorTracker = new ErrorTracker()
// 全局错误处理中间件
export function errorHandlingMiddleware(err, req, res, next) {
const errorId = errorTracker.track(err, {
url: req.url,
method: req.method,
userAgent: req.get('User-Agent'),
ip: req.ip,
userId: req.user?.id
})
// 根据环境返回不同的错误信息
if (process.env.NODE_ENV === 'production') {
res.status(err.status || 500).json({
error: '服务器内部错误',
errorId,
timestamp: Date.now()
})
} else {
res.status(err.status || 500).json({
error: err.message,
stack: err.stack,
errorId,
timestamp: Date.now()
})
}
}
// 未捕获异常处理
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error)
errorTracker.track(error, { type: 'uncaughtException' })
// 优雅关闭
process.exit(1)
})
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的Promise拒绝:', reason)
errorTracker.track(new Error(reason), {
type: 'unhandledRejection',
promise: promise.toString()
})
})
日志系统
结构化日志配置
// server/logging/index.js
import winston from 'winston'
import DailyRotateFile from 'winston-daily-rotate-file'
import path from 'path'
import config from '../config/index.js'
// 自定义日志格式
const logFormat = winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss.SSS'
}),
winston.format.errors({ stack: true }),
winston.format.json(),
winston.format.printf(({ timestamp, level, message, ...meta }) => {
return JSON.stringify({
timestamp,
level,
message,
...meta
})
})
)
// 创建日志传输器
const transports = [
// 控制台输出
new winston.transports.Console({
format: config.isDevelopment
? winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
: logFormat
}),
// 错误日志文件
new DailyRotateFile({
filename: path.join('logs', 'error-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
level: 'error',
format: logFormat,
maxSize: '20m',
maxFiles: '14d',
zippedArchive: true
}),
// 组合日志文件
new DailyRotateFile({
filename: path.join('logs', 'combined-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
format: logFormat,
maxSize: '20m',
maxFiles: '7d',
zippedArchive: true
})
]
// 创建logger实例
const logger = winston.createLogger({
level: config.logging.level,
format: logFormat,
transports,
exitOnError: false
})
// 请求日志中间件
export function requestLoggingMiddleware(req, res, next) {
const start = Date.now()
const requestId = req.headers['x-request-id'] ||
`req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
// 添加请求ID到请求对象
req.requestId = requestId
// 记录请求开始
logger.info('请求开始', {
requestId,
method: req.method,
url: req.url,
userAgent: req.get('User-Agent'),
ip: req.ip,
referer: req.get('Referer')
})
// 监听响应结束
res.on('finish', () => {
const duration = Date.now() - start
logger.info('请求完成', {
requestId,
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${duration}ms`,
contentLength: res.get('Content-Length')
})
})
next()
}
// 增强的logger方法
class EnhancedLogger {
constructor(baseLogger) {
this.logger = baseLogger
}
info(message, meta = {}) {
this.logger.info(message, this.enrichMeta(meta))
}
warn(message, meta = {}) {
this.logger.warn(message, this.enrichMeta(meta))
}
error(message, error = null, meta = {}) {
const errorMeta = error ? {
error: {
name: error.name,
message: error.message,
stack: error.stack
}
} : {}
this.logger.error(message, this.enrichMeta({ ...meta, ...errorMeta }))
}
debug(message, meta = {}) {
this.logger.debug(message, this.enrichMeta(meta))
}
enrichMeta(meta) {
return {
...meta,
pid: process.pid,
memory: process.memoryUsage(),
uptime: process.uptime()
}
}
// SSR特定的日志方法
ssrRender(component, duration, meta = {}) {
this.info('SSR渲染完成', {
component,
duration: `${duration}ms`,
...meta
})
}
cacheHit(key, meta = {}) {
this.debug('缓存命中', {
cacheKey: key,
...meta
})
}
cacheMiss(key, meta = {}) {
this.debug('缓存未命中', {
cacheKey: key,
...meta
})
}
apiCall(url, method, duration, statusCode, meta = {}) {
this.info('API调用', {
url,
method,
duration: `${duration}ms`,
statusCode,
...meta
})
}
}
export const enhancedLogger = new EnhancedLogger(logger)
export { logger }
export default enhancedLogger
测试和验证
SSR测试工具
// tests/ssr-test-utils.js
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
import { createMemoryHistory, createRouter } from 'vue-router'
import { createPinia } from 'pinia'
import routes from '../src/router/routes.js'
export class SSRTestHelper {
constructor() {
this.app = null
this.router = null
this.pinia = null
}
async createApp(initialRoute = '/') {
// 创建应用实例
this.app = createSSRApp({})
// 创建路由
this.router = createRouter({
history: createMemoryHistory(),
routes
})
// 创建状态管理
this.pinia = createPinia()
// 安装插件
this.app.use(this.router)
this.app.use(this.pinia)
// 导航到初始路由
await this.router.push(initialRoute)
await this.router.isReady()
return this.app
}
async renderToString(initialRoute = '/') {
const app = await this.createApp(initialRoute)
return await renderToString(app)
}
async testRoute(route, expectedContent) {
const html = await this.renderToString(route)
const tests = {
hasContent: html.includes(expectedContent),
hasValidHTML: this.validateHTML(html),
hasNoErrors: !html.includes('error'),
hasMetaTags: html.includes('<meta'),
hasTitle: html.includes('<title>')
}
return {
html,
tests,
passed: Object.values(tests).every(Boolean)
}
}
validateHTML(html) {
// 基础HTML验证
const hasDoctype = html.startsWith('<!DOCTYPE html>')
const hasHtmlTag = html.includes('<html')
const hasHeadTag = html.includes('<head>')
const hasBodyTag = html.includes('<body>')
const isWellFormed = this.checkWellFormed(html)
return hasDoctype && hasHtmlTag && hasHeadTag && hasBodyTag && isWellFormed
}
checkWellFormed(html) {
// 简单的标签匹配检查
const openTags = html.match(/<[^/][^>]*>/g) || []
const closeTags = html.match(/<\/[^>]*>/g) || []
// 自闭合标签
const selfClosing = ['img', 'br', 'hr', 'input', 'meta', 'link']
const filteredOpenTags = openTags.filter(tag => {
const tagName = tag.match(/<([^\s>]+)/)?.[1]?.toLowerCase()
return !selfClosing.includes(tagName) && !tag.endsWith('/>')
})
return Math.abs(filteredOpenTags.length - closeTags.length) <= 2 // 允许小误差
}
async testHydration(route) {
const serverHTML = await this.renderToString(route)
// 模拟客户端渲染
const clientApp = await this.createApp(route)
const clientHTML = await renderToString(clientApp)
return {
serverHTML,
clientHTML,
matches: serverHTML === clientHTML,
diff: this.findDifferences(serverHTML, clientHTML)
}
}
findDifferences(str1, str2) {
const differences = []
const maxLength = Math.max(str1.length, str2.length)
for (let i = 0; i < maxLength; i++) {
if (str1[i] !== str2[i]) {
const context = {
position: i,
server: str1.substring(Math.max(0, i - 20), i + 20),
client: str2.substring(Math.max(0, i - 20), i + 20)
}
differences.push(context)
// 只记录前5个差异
if (differences.length >= 5) break
}
}
return differences
}
}
// 测试用例示例
export async function runSSRTests() {
const helper = new SSRTestHelper()
const results = []
const testCases = [
{ route: '/', expectedContent: '首页' },
{ route: '/about', expectedContent: '关于我们' },
{ route: '/posts', expectedContent: '文章列表' },
{ route: '/posts/1', expectedContent: '文章详情' }
]
for (const testCase of testCases) {
try {
const result = await helper.testRoute(testCase.route, testCase.expectedContent)
results.push({
route: testCase.route,
...result
})
} catch (error) {
results.push({
route: testCase.route,
error: error.message,
passed: false
})
}
}
return results
}
性能基准测试
负载测试脚本
// scripts/load-test.js
import autocannon from 'autocannon'
import { performance } from 'perf_hooks'
class LoadTester {
constructor(baseUrl = 'http://localhost:3000') {
this.baseUrl = baseUrl
}
async runTest(options = {}) {
const defaultOptions = {
url: this.baseUrl,
connections: 10,
duration: 30,
pipelining: 1,
headers: {
'User-Agent': 'LoadTest/1.0'
}
}
const testOptions = { ...defaultOptions, ...options }
console.log('开始负载测试:', testOptions)
const start = performance.now()
const result = await autocannon(testOptions)
const duration = performance.now() - start
return {
...result,
testDuration: duration,
summary: this.generateSummary(result)
}
}
generateSummary(result) {
return {
totalRequests: result.requests.total,
requestsPerSecond: result.requests.average,
averageLatency: `${result.latency.average}ms`,
maxLatency: `${result.latency.max}ms`,
throughput: `${(result.throughput.average / 1024 / 1024).toFixed(2)}MB/s`,
errors: result.errors,
timeouts: result.timeouts,
successRate: `${((result.requests.total - result.errors) / result.requests.total * 100).toFixed(2)}%`
}
}
async testMultipleRoutes(routes, options = {}) {
const results = []
for (const route of routes) {
console.log(`测试路由: ${route}`)
const routeOptions = {
...options,
url: `${this.baseUrl}${route}`
}
const result = await this.runTest(routeOptions)
results.push({
route,
...result
})
// 测试间隔
await new Promise(resolve => setTimeout(resolve, 2000))
}
return results
}
async comparePerformance(routes, scenarios) {
const comparison = []
for (const scenario of scenarios) {
console.log(`\n测试场景: ${scenario.name}`)
const results = await this.testMultipleRoutes(routes, scenario.options)
comparison.push({
scenario: scenario.name,
results,
averageRPS: results.reduce((sum, r) => sum + r.requests.average, 0) / results.length,
averageLatency: results.reduce((sum, r) => sum + r.latency.average, 0) / results.length
})
}
return comparison
}
}
// 运行测试
async function main() {
const tester = new LoadTester()
const routes = ['/', '/about', '/posts', '/posts/1']
const scenarios = [
{
name: '低负载',
options: { connections: 5, duration: 30 }
},
{
name: '中等负载',
options: { connections: 20, duration: 30 }
},
{
name: '高负载',
options: { connections: 50, duration: 30 }
}
]
try {
const comparison = await tester.comparePerformance(routes, scenarios)
console.log('\n=== 性能测试报告 ===')
comparison.forEach(result => {
console.log(`\n${result.scenario}:`)
console.log(` 平均RPS: ${result.averageRPS.toFixed(2)}`)
console.log(` 平均延迟: ${result.averageLatency.toFixed(2)}ms`)
})
} catch (error) {
console.error('测试失败:', error)
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
main()
}
export { LoadTester }
下一步
在下一章节中,我们将学习Vue SSR的最佳实践和进阶技巧,包括代码组织、架构设计和团队协作等内容。