这篇内容是在有道云旧笔记基础上重新整理的 Vue/Vite 工程实践 笔记。原始记录偏碎片化,这里补充了使用场景、阅读顺序和落地时需要注意的边界,方便以后在项目里快速复用。

适用场景 ​

  • Vue/Vite 项目中遇到配置、组件封装、构建或运行时问题时,作为排查入口。
  • 需要把一次性调试经验沉淀成团队内可复用的前端工程实践。
  • 迁移旧项目或升级依赖时,用来对照检查环境变量、插件和组件行为差异。

核心要点 ​

  • 正文已经按 「🧠 调度层」、「⚙️ 执行层」、「🛡 稳定性」、「🧩 控制层」 等小节组织,阅读时可以先定位到当前问题对应的小节。
  • 保留了 1 段可直接参考的代码或命令,主要涉及 javascript,复制前需要按当前项目路径、版本和变量名做调整。

✅ 📌 你现在这版具备的能力 ​

🧠 调度层 ​

  • ✔ Web Worker 定时调度
  • ✔ 多任务支持

⚙️ 执行层 ​

  • ✔ 单链路 retry(不会并发)
  • ✔ 指数退避
  • ✔ AbortController 取消请求

🛡 稳定性 ​

  • ✔ 4xx 不重试(避免 404 风暴)
  • ✔ 5xx 自动重试
  • ✔ 防重复请求(锁)

🧩 控制层 ​

  • ✔ pause / resume
  • ✔ 页面隐藏自动暂停

🌐 浏览器级 ​

  • ✔ 多 tab 选主(防止重复轮询)

🧬 可扩展 ​

  • ✔ 默认配置
  • ✔ 动态 interval
  • ✔ runOnce
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
// pollingManager.js

import axios from 'axios';

// ================== 默认配置 ==================
const DEFAULT_OPTIONS = {
interval: 1000,
retry: 3,
retryDelay: 2000,
immediate: true,
};

class PollingManager {
constructor() {
this.worker = new Worker(
new URL('../workers/polling.worker.js', import.meta.url),
{ type: 'module' },
);

this.tasks = new Map();
this.controllers = new Map();
this.runningTasks = new Set();

this.paused = false;

// 多 tab 控制
this.channel = new BroadcastChannel('polling_channel');
this.isLeader = true;

this.worker.onmessage = this.handleMessage.bind(this);

this.initVisibilityControl();
this.initBroadcastChannel();
}

// ================== Worker 回调 ==================
handleMessage(e) {
if (this.paused || !this.isLeader) return;

const { type, id } = e.data;

if (type === 'EXECUTE') {
const task = this.tasks.get(id);
if (!task) return;

this.executeTask(task);
}
}

// ================== 核心执行(无并发 + 单链路 retry) ==================
async executeTask(task) {
if (this.runningTasks.has(task.id)) return;

this.runningTasks.add(task.id);

const controller = new AbortController();
this.controllers.set(task.id, controller);

try {
const data = await this.requestWithRetry(
task,
task.retry,
controller,
);

task.onSuccess && task.onSuccess(data);
}
catch (err) {
if (!axios.isCancel || !axios.isCancel(err)) {
task.onError && task.onError(err);
this.reportError(err, task);
}
}
finally {
this.runningTasks.delete(task.id);
}
}

// ================== 请求 + 重试(核心逻辑) ==================
async requestWithRetry(task, retryCount, controller) {
try {
const res = await axios({
...task.request,
signal: controller.signal,
});

return res.data;
}
catch (err) {
if (axios.isCancel && axios.isCancel(err)) {
throw err;
}

const status = err?.response?.status;

// 只重试:网络错误 + 5xx
const shouldRetry
= retryCount > 0
&& (!status || (status >= 500 && status < 600));

if (!shouldRetry) {
throw err;
}

// 指数退避(更企业级)
const delay
= (task.retryDelay || 1000)
* 2 ** (task.retry - retryCount);

await this.sleep(delay);

return this.requestWithRetry(task, retryCount - 1, controller);
}
}

sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

// ================== API ==================
start(options) {
if (!options || !options.id) return;

const finalOptions = {
...DEFAULT_OPTIONS,
...options,
};

const id = finalOptions.id;

if (this.tasks.has(id)) return;

this.tasks.set(id, finalOptions);

if (finalOptions.immediate) {
this.executeTask(finalOptions);
}

this.worker.postMessage({
type: 'START',
payload: {
id,
interval: finalOptions.interval,
},
});
}

stop(id) {
this.worker.postMessage({ type: 'STOP', id });

const controller = this.controllers.get(id);
controller && controller.abort();

this.tasks.delete(id);
this.controllers.delete(id);
this.runningTasks.delete(id);
}

stopAll() {
this.worker.postMessage({ type: 'STOP_ALL' });

this.controllers.forEach(c => c.abort());

this.tasks.clear();
this.controllers.clear();
this.runningTasks.clear();
}

pauseAll() {
this.paused = true;
}

resumeAll() {
this.paused = false;
}

runOnce(id) {
const task = this.tasks.get(id);
if (task) this.executeTask(task);
}

updateInterval(id, interval) {
const task = this.tasks.get(id);
if (!task) return;

this.stop(id);

task.interval = interval;

this.start(task);
}

setDefaultOptions(options) {
Object.assign(DEFAULT_OPTIONS, options);
}

// ================== 页面可见性 ==================
initVisibilityControl() {
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.pauseAll();
}
else {
this.resumeAll();
}
});
}

// ================== 多 tab 选主 ==================
initBroadcastChannel() {
this.channel.onmessage = (e) => {
if (e.data === 'CHECK_LEADER') {
this.channel.postMessage('I_AM_LEADER');
}

if (e.data === 'I_AM_LEADER') {
this.isLeader = false;
}
};

this.channel.postMessage('CHECK_LEADER');

setTimeout(() => {
this.isLeader = true;
}, 100);
}

// ================== 错误上报 ==================
reportError(err, task) {
console.error('[Polling Error]', task.id, err);

// 可接入 Sentry / 日志系统
}
}

export const pollingManager = new PollingManager();


// hooks/usePolling.js
import { pollingManager } from '@/utils/pollingManager';

export function usePolling() {
const taskIds = [];

function startPolling(options) {
if (!options || !options.id) return;

pollingManager.start(options);
taskIds.push(options.id);
}

function stopPolling(id) {
pollingManager.stop(id);
}

function updateInterval(id, interval) {
pollingManager.updateInterval(id, interval);
}

function runOnce(id) {
pollingManager.runOnce(id);
}

onUnmounted(() => {
taskIds.forEach(id => pollingManager.stop(id));
});

return {
startPolling,
stopPolling,
updateInterval,
runOnce,
};
}


// ./workers/polling.worker.js
const tasks = new Map();

globalThis.onmessage = (e) => {
const { type, payload, id } = e.data;

switch (type) {
case 'START':
startTask(payload);
break;
case 'STOP':
stopTask(id);
break;
case 'STOP_ALL':
stopAll();
break;
}
};

function startTask(task) {
if (!task || !task.id || tasks.has(task.id)) return;

const timer = setInterval(() => {
postMessage({
type: 'EXECUTE',
id: task.id,
});
}, task.interval);

tasks.set(task.id, { ...task, timer });
}

function stopTask(id) {
const task = tasks.get(id);
if (task) {
clearInterval(task.timer);
tasks.delete(id);
}
}

function stopAll() {
tasks.forEach(task => clearInterval(task.timer));
tasks.clear();
}


// vite

worker: {
format: 'es', // 使用ES模块
},

<script setup>
import { usePolling } from '@/hooks/usePolling';

const { startPolling } = usePolling();

startPolling({
id: 'device-status',
request: {
url: '/api/device/status',
method: 'GET',
},
onSuccess: (data) => {
console.log('设备状态:', data);
},
onError: (err) => {
console.error('失败:', err);
},
});
</script>


实践检查清单 &#8203;

  • 先确认 Vue、Vite、Node、包管理器版本,再判断示例是否需要按当前工程结构调整。
  • 涉及构建或插件配置时,建议同时验证开发环境和生产构建结果。
  • 涉及 UI 或浏览器行为时,至少在桌面端和移动端各验证一次关键路径。
  • 涉及接口、服务端或权限逻辑时,补充失败分支和权限边界测试。

复盘小结 &#8203;

这篇笔记更适合作为问题排查或方案落地前的速查材料。后续如果在真实项目中再次遇到同类问题,可以继续把具体版本、报错信息、最终取舍和验证结果补到对应小节里,让它从代码片段逐步沉淀成完整方案。