@quicktvui/web-cli 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
left: 0;
|
|
122
122
|
right: 0;
|
|
123
123
|
z-index: 99998;
|
|
124
|
-
background: rgba(0, 0, 0, 0.
|
|
124
|
+
background: rgba(0, 0, 0, 0.2);
|
|
125
125
|
color: #aaa;
|
|
126
126
|
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
|
|
127
127
|
font-size: 12px;
|
|
@@ -289,10 +289,159 @@
|
|
|
289
289
|
|
|
290
290
|
// ========== SSE 热更新 ==========
|
|
291
291
|
var sseConnected = false;
|
|
292
|
+
var sse = null;
|
|
293
|
+
var sseLeaderHeartbeat = null;
|
|
294
|
+
var sseLeaderPoller = null;
|
|
295
|
+
var sseTabId = 'qt-dev-tab-' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
|
296
|
+
var sseLeaderKey = 'quicktvui-dev-sse-leader';
|
|
297
|
+
var sseLeaderTtl = 10000;
|
|
292
298
|
// 页面加载后的冷却期:这段时间内忽略 bundle-update 事件
|
|
293
299
|
// 防止 SSE 重连后收到缓存的旧 bundle-update 导致无限刷新
|
|
294
300
|
var sseCooldownUntil = Date.now() + 3000; // 3 秒冷却期
|
|
295
301
|
|
|
302
|
+
function readSseLeader() {
|
|
303
|
+
try {
|
|
304
|
+
var raw = localStorage.getItem(sseLeaderKey);
|
|
305
|
+
return raw ? JSON.parse(raw) : null;
|
|
306
|
+
} catch (e) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function isSseLeaderAlive(leader) {
|
|
312
|
+
return !!(leader && leader.tabId && (Date.now() - leader.ts) < sseLeaderTtl);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function writeSseLeader() {
|
|
316
|
+
try {
|
|
317
|
+
localStorage.setItem(sseLeaderKey, JSON.stringify({
|
|
318
|
+
tabId: sseTabId,
|
|
319
|
+
ts: Date.now(),
|
|
320
|
+
href: window.location.href
|
|
321
|
+
}));
|
|
322
|
+
} catch (e) {
|
|
323
|
+
console.warn('[Dev] Failed to write SSE leader lock:', e);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function clearSseLeader() {
|
|
328
|
+
var leader = readSseLeader();
|
|
329
|
+
if (leader && leader.tabId === sseTabId) {
|
|
330
|
+
try {
|
|
331
|
+
localStorage.removeItem(sseLeaderKey);
|
|
332
|
+
} catch (e) {
|
|
333
|
+
console.warn('[Dev] Failed to clear SSE leader lock:', e);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function startSseHeartbeat() {
|
|
339
|
+
if (sseLeaderHeartbeat) return;
|
|
340
|
+
writeSseLeader();
|
|
341
|
+
sseLeaderHeartbeat = setInterval(function() {
|
|
342
|
+
writeSseLeader();
|
|
343
|
+
}, Math.max(2000, Math.floor(sseLeaderTtl / 2)));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function stopSseHeartbeat() {
|
|
347
|
+
if (sseLeaderHeartbeat) {
|
|
348
|
+
clearInterval(sseLeaderHeartbeat);
|
|
349
|
+
sseLeaderHeartbeat = null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function closeSse(reason) {
|
|
354
|
+
stopSseHeartbeat();
|
|
355
|
+
if (sse) {
|
|
356
|
+
sse.close();
|
|
357
|
+
sse = null;
|
|
358
|
+
}
|
|
359
|
+
if (sseConnected) {
|
|
360
|
+
console.log('[Dev] SSE closed:', reason || 'manual');
|
|
361
|
+
}
|
|
362
|
+
sseConnected = false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function shouldOwnSse() {
|
|
366
|
+
return document.visibilityState !== 'hidden' &&
|
|
367
|
+
(typeof document.hasFocus !== 'function' || document.hasFocus());
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function becomeSseLeader() {
|
|
371
|
+
writeSseLeader();
|
|
372
|
+
startSseHeartbeat();
|
|
373
|
+
if (!sse) {
|
|
374
|
+
console.log('[Dev] SSE leader acquired by current tab');
|
|
375
|
+
connectSse();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function connectSse() {
|
|
380
|
+
if (sse || !window.__BUNDLE_CONFIG__ || !window.__BUNDLE_CONFIG__.watch) return;
|
|
381
|
+
try {
|
|
382
|
+
sse = new EventSource(window.__BUNDLE_CONFIG__.sseEndpoint);
|
|
383
|
+
sse.onopen = function() {
|
|
384
|
+
sseConnected = true;
|
|
385
|
+
console.log('[Dev] SSE connected');
|
|
386
|
+
updateStatus('ready', '已连接 - 等待变化');
|
|
387
|
+
};
|
|
388
|
+
sse.onmessage = function(e) {
|
|
389
|
+
try {
|
|
390
|
+
var data = JSON.parse(e.data);
|
|
391
|
+
console.log('[Dev] SSE event:', data.type, data);
|
|
392
|
+
if (data.type === 'connected') {
|
|
393
|
+
updateStatus('ready', '开发服务器已连接');
|
|
394
|
+
} else if (data.type === 'bundle-update') {
|
|
395
|
+
if (Date.now() < sseCooldownUntil) {
|
|
396
|
+
console.log('[Dev] Ignoring bundle-update during cooldown');
|
|
397
|
+
updateStatus('ready', '构建完成');
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
updateStatus('building', 'Bundle 更新中...');
|
|
401
|
+
setTimeout(function() { location.reload(); }, 300);
|
|
402
|
+
} else if (data.type === 'full-reload') {
|
|
403
|
+
if (Date.now() < sseCooldownUntil) {
|
|
404
|
+
console.log('[Dev] Ignoring full-reload during cooldown');
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
updateStatus('building', '正在刷新...');
|
|
408
|
+
setTimeout(function() { location.reload(); }, 300);
|
|
409
|
+
} else if (data.type === 'build-status') {
|
|
410
|
+
if (data.status === 'building') {
|
|
411
|
+
updateStatus('building', data.message || '构建中...');
|
|
412
|
+
} else if (data.status === 'ready') {
|
|
413
|
+
updateStatus('ready', data.message || '构建完成');
|
|
414
|
+
} else if (data.status === 'error') {
|
|
415
|
+
updateStatus('error', data.message || '构建失败');
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} catch (ex) {
|
|
419
|
+
console.error('[Dev] SSE parse error:', ex);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
sse.onerror = function() {
|
|
423
|
+
sseConnected = false;
|
|
424
|
+
console.log('[Dev] SSE disconnected, retrying...');
|
|
425
|
+
updateStatus('building', 'SSE 断开,重连中...');
|
|
426
|
+
};
|
|
427
|
+
} catch (e) {
|
|
428
|
+
console.error('[Dev] SSE init error:', e);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function updateSseLeadership() {
|
|
433
|
+
if (!window.__BUNDLE_CONFIG__ || !window.__BUNDLE_CONFIG__.watch) return;
|
|
434
|
+
|
|
435
|
+
// 前台页优先:当前可见且有焦点的标签页主动接管 SSE。
|
|
436
|
+
if (!shouldOwnSse()) {
|
|
437
|
+
clearSseLeader();
|
|
438
|
+
closeSse('tab inactive');
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
becomeSseLeader();
|
|
443
|
+
}
|
|
444
|
+
|
|
296
445
|
// ========== CORS 代理 ==========
|
|
297
446
|
// 在 web-cli dev 模式下,拦截所有跨域 fetch/XHR/Image 请求,
|
|
298
447
|
// 转换为 /proxy/ 路径由 DevServer 代理转发,解决浏览器 CORS 限制
|
|
@@ -411,56 +560,29 @@
|
|
|
411
560
|
})();
|
|
412
561
|
|
|
413
562
|
if (window.__BUNDLE_CONFIG__ && window.__BUNDLE_CONFIG__.watch) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
var data = JSON.parse(e.data);
|
|
424
|
-
console.log('[Dev] SSE event:', data.type, data);
|
|
425
|
-
if (data.type === 'connected') {
|
|
426
|
-
updateStatus('ready', '开发服务器已连接');
|
|
427
|
-
} else if (data.type === 'bundle-update') {
|
|
428
|
-
// 冷却期内忽略 bundle-update(可能是 SSE 重连后缓存的旧事件)
|
|
429
|
-
if (Date.now() < sseCooldownUntil) {
|
|
430
|
-
console.log('[Dev] Ignoring bundle-update during cooldown');
|
|
431
|
-
updateStatus('ready', '构建完成');
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
updateStatus('building', 'Bundle 更新中...');
|
|
435
|
-
setTimeout(function() { location.reload(); }, 300);
|
|
436
|
-
} else if (data.type === 'full-reload') {
|
|
437
|
-
if (Date.now() < sseCooldownUntil) {
|
|
438
|
-
console.log('[Dev] Ignoring full-reload during cooldown');
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
updateStatus('building', '正在刷新...');
|
|
442
|
-
setTimeout(function() { location.reload(); }, 300);
|
|
443
|
-
} else if (data.type === 'build-status') {
|
|
444
|
-
if (data.status === 'building') {
|
|
445
|
-
updateStatus('building', data.message || '构建中...');
|
|
446
|
-
} else if (data.status === 'ready') {
|
|
447
|
-
updateStatus('ready', data.message || '构建完成');
|
|
448
|
-
} else if (data.status === 'error') {
|
|
449
|
-
updateStatus('error', data.message || '构建失败');
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
} catch (ex) {
|
|
453
|
-
console.error('[Dev] SSE parse error:', ex);
|
|
563
|
+
updateSseLeadership();
|
|
564
|
+
sseLeaderPoller = setInterval(updateSseLeadership, 3000);
|
|
565
|
+
window.addEventListener('storage', function(event) {
|
|
566
|
+
if (event.key === sseLeaderKey) {
|
|
567
|
+
if (shouldOwnSse()) {
|
|
568
|
+
setTimeout(updateSseLeadership, 0);
|
|
569
|
+
} else {
|
|
570
|
+
closeSse('another tab owns SSE');
|
|
571
|
+
updateStatus('ready', '热更新由其他标签页持有');
|
|
454
572
|
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
window.addEventListener('focus', updateSseLeadership);
|
|
576
|
+
window.addEventListener('blur', updateSseLeadership);
|
|
577
|
+
document.addEventListener('visibilitychange', updateSseLeadership);
|
|
578
|
+
window.addEventListener('beforeunload', function() {
|
|
579
|
+
clearSseLeader();
|
|
580
|
+
closeSse('beforeunload');
|
|
581
|
+
});
|
|
582
|
+
window.addEventListener('pagehide', function() {
|
|
583
|
+
clearSseLeader();
|
|
584
|
+
closeSse('pagehide');
|
|
585
|
+
});
|
|
464
586
|
}
|
|
465
587
|
|
|
466
588
|
// ========== 自动添加 bundle 参数(如果 URL 中没有) ==========
|