清晰的解决方案步骤
第一部分:不使用Web Worker的方案(简单直接)
步骤1:创建时间管理Store
文件路径: src/stores/timeStore.js
javascript
import { defineStore } from 'pinia'
import { ref, computed, onUnmounted } from 'vue'
export const useTimeStore = defineStore('time', () => {
// 初始持续时间(从路由参数传入,单位:秒)
const initialDurationMs = ref(0)
// 当前持续时间(毫秒)
const currentDurationMs = ref(0)
// 开始时间点(performance.now())
const startTime = ref(null)
// 定时器相关
let rafId = null
let lastVisibleTime = null
// 初始化持续时间
const initDuration = (initialSeconds) => {
initialDurationMs.value = initialSeconds * 1000
currentDurationMs.value = initialDurationMs.value
startTime.value = performance.now()
lastVisibleTime = startTime.value
// 启动高性能计时器
startCounting()
// 监听页面可见性变化
setupVisibilityListener()
}
// 使用requestAnimationFrame进行精确计时
const startCounting = () => {
if (rafId) cancelAnimationFrame(rafId)
const update = () => {
const now = performance.now()
const elapsed = now - startTime.value
currentDurationMs.value = initialDurationMs.value + elapsed
lastVisibleTime = now
rafId = requestAnimationFrame(update)
}
update()
}
// 处理页面可见性变化
const setupVisibilityListener = () => {
const handleVisibilityChange = () => {
if (document.hidden) {
// 页面进入后台,暂停计时器
if (rafId) {
cancelAnimationFrame(rafId)
rafId = null
}
} else {
// 页面回到前台,补偿后台时间并重启计时器
if (lastVisibleTime && !rafId) {
const now = performance.now()
const timeInBackground = now - lastVisibleTime
currentDurationMs.value += timeInBackground
startTime.value += timeInBackground
startCounting()
}
}
}
document.addEventListener('visibilitychange', handleVisibilityChange)
// 返回清理函数
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}
// 格式化持续时间
const formattedDuration = computed(() => {
const totalSeconds = Math.floor(currentDurationMs.value / 1000)
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
return {
hours: hours.toString().padStart(2, '0'),
minutes: minutes.toString().padStart(2, '0'),
seconds: seconds.toString().padStart(2, '0'),
totalMs: currentDurationMs.value
}
})
// 清理资源
const cleanup = () => {
if (rafId) {
cancelAnimationFrame(rafId)
rafId = null
}
}
onUnmounted(() => {
cleanup()
})
return {
formattedDuration,
initDuration,
cleanup,
currentDurationMs
}
})
步骤2:创建右上角持续时间组件
文件路径: src/components/DurationDisplay/index.vue
vue
<template>
<div class="duration-display">
<el-icon><Clock /></el-icon>
<span class="label">持续时间:</span>
<span class="time">{{ formattedDuration.hours }}:{{ formattedDuration.minutes }}:{{ formattedDuration.seconds }}</span>
</div>
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { useTimeStore } from '@/stores/timeStore'
import { Clock } from '@element-plus/icons-vue'
// 获取路由参数中的初始时间
const route = useRoute()
const timeStore = useTimeStore()
// 从路由参数获取初始持续时间(单位:秒)
// 假设路由参数名为 initialDuration
const initialDuration = route.params.initialDuration ||
route.query.initialDuration ||
0
onMounted(() => {
// 初始化持续时间
timeStore.initDuration(Number(initialDuration))
})
onUnmounted(() => {
// 清理资源
timeStore.cleanup()
})
const formattedDuration = timeStore.formattedDuration
</script>
<style scoped>
.duration-display {
display: flex;
align-items: center;
gap: 8px;
font-family: 'Courier New', monospace;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 8px 16px;
border-radius: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.duration-display .el-icon {
font-size: 18px;
}
.duration-display .label {
font-weight: 500;
opacity: 0.9;
}
.duration-display .time {
font-weight: bold;
font-size: 18px;
letter-spacing: 1px;
}
</style>
步骤3:在页面中使用持续时间组件
文件路径: src/views/YourPage.vue
vue
<template>
<div class="page-container">
<!-- 右上角固定位置 -->
<div class="duration-container">
<DurationDisplay />
</div>
<!-- 页面内容 -->
<div class="page-content">
<!-- 表格组件 -->
<DataTable />
</div>
</div>
</template>
<script setup>
import DurationDisplay from '@/components/DurationDisplay/index.vue'
import DataTable from '@/components/DataTable/index.vue'
</script>
<style scoped>
.page-container {
position: relative;
min-height: 100vh;
padding: 20px;
}
.duration-container {
position: absolute;
top: 20px;
right: 20px;
z-index: 1000;
}
</style>
步骤4:创建表格组件
文件路径: src/components/DataTable/index.vue
vue
<template>
<div class="data-table-container">
<div class="table-header">
<el-button
type="primary"
@click="loadData"
:loading="loading"
icon="Refresh"
>
刷新数据
</el-button>
<!-- 调试信息(仅开发环境显示) -->
<div v-if="showDebugInfo" class="debug-info">
<el-tag type="info">
最后同步时间: {{ lastSyncTime || '未同步' }}
</el-tag>
</div>
</div>
<el-table
:data="tableData"
v-loading="loading"
style="width: 100%"
stripe
>
<el-table-column
prop="id"
label="ID"
width="80"
align="center"
/>
<el-table-column
prop="name"
label="名称"
min-width="120"
/>
<el-table-column
prop="value"
label="数值"
width="100"
align="right"
/>
<el-table-column
prop="timestamp"
label="创建时间"
width="180"
>
<template #default="{ row }">
{{ formatTime(row.timestamp) }}
</template>
</el-table-column>
<el-table-column
label="操作"
width="120"
align="center"
>
<template #default>
<el-button size="small">查看</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
const tableData = ref([])
const loading = ref(false)
const lastSyncTime = ref(null)
// 是否显示调试信息
const showDebugInfo = computed(() => {
return import.meta.env.DEV
})
// 格式化时间(直接显示后端返回的时间)
const formatTime = (timestamp) => {
if (!timestamp) return '-'
const date = new Date(timestamp)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
}
// 加载数据
const loadData = async () => {
loading.value = true
try {
// 模拟API调用
const response = await fetch('/api/table-data', {
method: 'GET',
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache'
}
})
const data = await response.json()
tableData.value = data
// 记录最后同步时间
lastSyncTime.value = new Date().toLocaleTimeString()
ElMessage.success('数据加载成功')
// 在生产环境,可以记录时间同步日志
if (!import.meta.env.DEV) {
console.log('表格数据加载时间:', {
clientTime: new Date().toISOString(),
sampleDataTime: data[0]?.timestamp
})
}
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
}
// 初始化加载数据
loadData()
</script>
<style scoped>
.data-table-container {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.debug-info {
display: flex;
gap: 10px;
}
</style>
第二部分:使用Web Worker的方案(高级,适合对精度要求极高的场景)
Web Worker文件结构
src/
├── workers/
│ ├── duration.worker.js # Worker主文件
│ └── duration.worker.helper.js # Worker工具函数
├── stores/
│ └── timeStore.worker.js # 使用Worker的Store
└── components/
└── DurationDisplay.worker.vue # 使用Worker的组件
步骤1:创建Web Worker文件
文件路径: src/workers/duration.worker.js
javascript
// 高性能计时器,使用performance.now()确保精度
let startTime = null
let initialDuration = 0
let currentDuration = 0
let lastUpdateTime = null
let isRunning = true
// 消息处理
self.onmessage = function(e) {
const { type, data } = e.data
switch (type) {
case 'INIT':
startTime = performance.now()
initialDuration = data.initialDuration || 0
currentDuration = initialDuration
lastUpdateTime = startTime
startTimer()
break
case 'PAUSE':
isRunning = false
break
case 'RESUME':
isRunning = true
if (lastUpdateTime) {
// 补偿暂停期间的时间
const now = performance.now()
const elapsed = now - lastUpdateTime
currentDuration += elapsed
lastUpdateTime = now
}
break
case 'ADJUST':
// 调整时间(用于同步)
if (data.adjustment) {
currentDuration += data.adjustment
}
break
case 'GET_CURRENT':
// 返回当前持续时间
self.postMessage({
type: 'CURRENT_DURATION',
duration: currentDuration
})
break
case 'DESTROY':
self.close()
break
}
}
// 使用requestAnimationFrame计时
function startTimer() {
function update() {
if (!isRunning || startTime === null) return
const now = performance.now()
currentDuration = initialDuration + (now - startTime)
lastUpdateTime = now
// 每秒更新一次(减少消息传递频率)
self.postMessage({
type: 'DURATION_UPDATE',
duration: currentDuration,
timestamp: Date.now()
})
// 继续下一帧
self.requestAnimationFrame(update)
}
// 使用模拟的requestAnimationFrame
self.requestAnimationFrame = self.requestAnimationFrame ||
function(callback) {
return setTimeout(() => {
callback(performance.now())
}, 1000 / 60) // 60fps
}
update()
}
// 错误处理
self.onerror = function(error) {
console.error('Worker error:', error)
self.postMessage({
type: 'ERROR',
error: error.message
})
}
步骤2:创建使用Worker的Store
文件路径: src/stores/timeStore.worker.js
javascript
import { defineStore } from 'pinia'
import { ref, computed, onUnmounted } from 'vue'
export const useTimeStoreWithWorker = defineStore('timeWorker', () => {
const durationMs = ref(0)
let worker = null
let cleanupListeners = null
// 初始化Worker
const initWorker = () => {
if (worker) return
worker = new Worker(new URL('@/workers/duration.worker.js', import.meta.url), {
type: 'module'
})
// 监听Worker消息
worker.onmessage = (e) => {
const { type, duration } = e.data
if (type === 'DURATION_UPDATE') {
durationMs.value = duration
}
}
// 错误处理
worker.onerror = (error) => {
console.error('Worker error:', error)
// 降级为不使用Worker的计时
fallbackToMainThread()
}
// 设置清理监听器
cleanupListeners = setupVisibilityListener()
}
// 初始化持续时间
const initDuration = (initialSeconds) => {
initWorker()
worker.postMessage({
type: 'INIT',
data: {
initialDuration: initialSeconds * 1000
}
})
}
// 处理页面可见性变化
const setupVisibilityListener = () => {
const handleVisibilityChange = () => {
if (!worker) return
if (document.hidden) {
// 页面进入后台,暂停Worker计时
worker.postMessage({ type: 'PAUSE' })
} else {
// 页面回到前台,恢复Worker计时
worker.postMessage({ type: 'RESUME' })
}
}
document.addEventListener('visibilitychange', handleVisibilityChange)
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}
// 降级方案:如果Worker不可用,使用主线程计时
const fallbackToMainThread = () => {
if (worker) {
worker.terminate()
worker = null
}
console.warn('Worker不可用,降级为主线程计时')
// 这里可以调用不使用Worker的Store
// 在实际项目中,你可能需要导入并切换store
}
// 格式化持续时间
const formattedDuration = computed(() => {
const totalSeconds = Math.floor(durationMs.value / 1000)
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
return {
hours: hours.toString().padStart(2, '0'),
minutes: minutes.toString().padStart(2, '0'),
seconds: seconds.toString().padStart(2, '0'),
totalMs: durationMs.value
}
})
// 清理资源
const cleanup = () => {
if (worker) {
worker.postMessage({ type: 'DESTROY' })
worker.terminate()
worker = null
}
if (cleanupListeners) {
cleanupListeners()
cleanupListeners = null
}
}
onUnmounted(() => {
cleanup()
})
return {
formattedDuration,
initDuration,
cleanup,
durationMs
}
})
步骤3:创建使用Worker的持续时间组件
文件路径: src/components/DurationDisplay.worker.vue
vue
<template>
<div class="duration-display-worker">
<div class="indicator" :class="{ 'worker-active': workerActive }"></div>
<span class="time">
{{ formattedDuration.hours }}:{{ formattedDuration.minutes }}:{{ formattedDuration.seconds }}
</span>
<el-tag v-if="workerActive" size="small" type="success">Worker</el-tag>
</div>
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { useTimeStoreWithWorker } from '@/stores/timeStore.worker.js'
const route = useRoute()
const timeStore = useTimeStoreWithWorker()
// 从路由参数获取初始时间
const initialDuration = route.params.initialDuration ||
route.query.initialDuration ||
0
// 是否使用Worker(可以根据浏览器支持动态决定)
const workerActive = typeof Worker !== 'undefined'
onMounted(() => {
if (workerActive) {
timeStore.initDuration(Number(initialDuration))
} else {
console.warn('当前浏览器不支持Web Worker')
// 可以在这里切换到不使用Worker的版本
}
})
onUnmounted(() => {
timeStore.cleanup()
})
const formattedDuration = timeStore.formattedDuration
</script>
<style scoped>
.duration-display-worker {
display: flex;
align-items: center;
gap: 10px;
background: #2c3e50;
color: white;
padding: 10px 20px;
border-radius: 8px;
font-family: 'Roboto Mono', monospace;
}
.indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background: #e74c3c;
}
.indicator.worker-active {
background: #2ecc71;
animation: pulse 2s infinite;
}
.time {
font-size: 20px;
font-weight: bold;
letter-spacing: 1px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
第三部分:部署和优化建议
1. 路由配置(确保传递初始时间)
javascript
// src/router/index.js
{
path: '/your-page',
name: 'YourPage',
component: () => import('@/views/YourPage.vue'),
props: (route) => ({
// 传递初始持续时间参数
initialDuration: route.query.initialDuration || 0
})
}
2. 性能监控
javascript
// src/utils/performanceMonitor.js
export const setupPerformanceMonitor = () => {
// 监控内存使用
if (performance.memory) {
setInterval(() => {
const memory = performance.memory
const usedMB = Math.round(memory.usedJSHeapSize / 1024 / 1024)
const totalMB = Math.round(memory.totalJSHeapSize / 1024 / 1024)
if (usedMB > 100) { // 超过100MB
console.warn('内存使用过高:', { usedMB, totalMB })
}
}, 30000)
}
// 监控时间偏差
let lastCheckTime = Date.now()
setInterval(() => {
const now = Date.now()
const elapsed = now - lastCheckTime
lastCheckTime = now
// 如果计时偏差超过2%,记录警告
if (Math.abs(elapsed - 1000) > 20) {
console.warn('计时偏差过大:', Math.abs(elapsed - 1000), 'ms')
}
}, 1000)
}
3. 生产环境配置
javascript
// vite.config.js 或 webpack配置
export default defineConfig({
build: {
rollupOptions: {
output: {
// Worker文件单独处理
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
}
}
},
worker: {
format: 'es',
plugins: []
}
})
第四部分:选择建议
选择方案A(不使用Worker):
- 适用于大多数场景
- 实现简单,维护成本低
- 兼容性好
- 推荐使用此方案
选择方案B(使用Worker):
- 适用于对计时精度要求极高的场景
- 页面有大量计算任务时,避免阻塞UI
- 后台运行更稳定
- 但实现复杂,兼容性需要考虑
最终建议实施步骤:
- 立即实施 :采用方案A(不使用Worker),代码简洁,解决主要问题
- 测试验证:在生产环境测试,监控时间同步情况
- 逐步优化 :如果仍有问题,再考虑方案B(使用Worker)
- 监控告警:添加时间偏差监控,超过阈值时告警
这样的分步实施方案既保证了快速解决问题,又为后续优化留下了空间。