vue2server7 7.0.18 → 7.0.20
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.
- package/frontEnd/src/components/NumberRange.vue +95 -202
- package/frontEnd/src/pages/TablePage.vue +228 -162
- package/package.json +1 -1
- package/test/777 +24 -0
- package/test/1/main.ts +0 -52
- package/test/1/utils/charts.ts +0 -38
- package/test/1/utils/color.ts +0 -110
- package/test/1/utils/date.ts +0 -9
- package/test/1/utils/request/Axios.ts +0 -294
- package/test/1/utils/request/AxiosCancel.ts +0 -67
- package/test/1/utils/request/AxiosTransform.ts +0 -63
- package/test/1/utils/request/index.ts +0 -231
- package/test/1/utils/request/utils.ts +0 -53
- package/test/1/utils/route/constant.ts +0 -14
- package/test/1/utils/route/index.ts +0 -110
- package/test/1/utils/version-check.worker.ts +0 -58
- package/test/1/utils/version-update.ts +0 -150
- package/test/1/version-hot-update.md +0 -277
- package/test/1/vite-plugin-version.ts +0 -35
- package/test/1/vite.config.ts +0 -46
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export const LAYOUT = () => import('@/layouts/index.vue');
|
|
2
|
-
export const BLANK_LAYOUT = () => import('@/layouts/blank.vue');
|
|
3
|
-
export const IFRAME = () => import('@/layouts/components/FrameBlank.vue');
|
|
4
|
-
export const EXCEPTION_COMPONENT = () => import('@/pages/result/500/index.vue');
|
|
5
|
-
export const PARENT_LAYOUT = () =>
|
|
6
|
-
new Promise((resolve) => {
|
|
7
|
-
resolve({ name: 'ParentLayout' });
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
export const PAGE_NOT_FOUND_ROUTE = {
|
|
11
|
-
path: '/:w+',
|
|
12
|
-
name: '404Page',
|
|
13
|
-
redirect: '/result/404',
|
|
14
|
-
};
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import cloneDeep from 'lodash/cloneDeep';
|
|
2
|
-
|
|
3
|
-
import type { RouteItem } from '@/api/model/permissionModel';
|
|
4
|
-
import type { RouteMeta } from '@/types/interface';
|
|
5
|
-
import {
|
|
6
|
-
BLANK_LAYOUT,
|
|
7
|
-
EXCEPTION_COMPONENT,
|
|
8
|
-
IFRAME,
|
|
9
|
-
LAYOUT,
|
|
10
|
-
PAGE_NOT_FOUND_ROUTE,
|
|
11
|
-
PARENT_LAYOUT,
|
|
12
|
-
} from '@/utils/route/constant';
|
|
13
|
-
|
|
14
|
-
// 动态从包内引入单个Icon,如果没有网络环境可以使用这种方式 但是会导致产物存在多个chunk
|
|
15
|
-
// const iconsPath = import.meta.glob('../../../node_modules/tdesign-icons-vue-next/esm/components/*.js');
|
|
16
|
-
|
|
17
|
-
// async function getMenuIcon(iconName: string): Promise<string> {
|
|
18
|
-
// const RenderIcon = iconsPath[`../../../node_modules/tdesign-icons-vue-next/esm/components/${iconName}.js`];
|
|
19
|
-
|
|
20
|
-
// const Icon = await RenderIcon();
|
|
21
|
-
// return shallowRef(Icon.default);
|
|
22
|
-
// }
|
|
23
|
-
|
|
24
|
-
const LayoutMap = new Map<string, () => Promise<typeof import('*.vue')>>();
|
|
25
|
-
|
|
26
|
-
LayoutMap.set('LAYOUT', LAYOUT);
|
|
27
|
-
LayoutMap.set('BLANK', BLANK_LAYOUT);
|
|
28
|
-
LayoutMap.set('IFRAME', IFRAME);
|
|
29
|
-
|
|
30
|
-
let dynamicViewsModules: Record<string, () => Promise<Recordable>>;
|
|
31
|
-
|
|
32
|
-
// 动态引入路由组件
|
|
33
|
-
function asyncImportRoute(routes: RouteItem[] | undefined) {
|
|
34
|
-
dynamicViewsModules = dynamicViewsModules || import.meta.glob('../../pages/**/*.vue');
|
|
35
|
-
if (!routes) return;
|
|
36
|
-
|
|
37
|
-
routes.forEach(async (item) => {
|
|
38
|
-
const { component, name } = item;
|
|
39
|
-
const { children } = item;
|
|
40
|
-
|
|
41
|
-
if (component) {
|
|
42
|
-
const layoutFound = LayoutMap.get(component.toUpperCase());
|
|
43
|
-
if (layoutFound) {
|
|
44
|
-
item.component = layoutFound;
|
|
45
|
-
} else {
|
|
46
|
-
item.component = dynamicImport(dynamicViewsModules, component);
|
|
47
|
-
}
|
|
48
|
-
} else if (name) {
|
|
49
|
-
item.component = PARENT_LAYOUT();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// 动态从包内引入单个Icon,如果没有网络环境可以使用这种方式 但是会导致产物存在多个chunk
|
|
53
|
-
// if (item.meta.icon) item.meta.icon = await getMenuIcon(item.meta.icon);
|
|
54
|
-
|
|
55
|
-
children && asyncImportRoute(children);
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function dynamicImport(dynamicViewsModules: Record<string, () => Promise<Recordable>>, component: string) {
|
|
60
|
-
const keys = Object.keys(dynamicViewsModules);
|
|
61
|
-
const matchKeys = keys.filter((key) => {
|
|
62
|
-
const k = key.replace('../../pages', '');
|
|
63
|
-
const startFlag = component.startsWith('/');
|
|
64
|
-
const endFlag = component.endsWith('.vue') || component.endsWith('.tsx');
|
|
65
|
-
const startIndex = startFlag ? 0 : 1;
|
|
66
|
-
const lastIndex = endFlag ? k.length : k.lastIndexOf('.');
|
|
67
|
-
return k.substring(startIndex, lastIndex) === component;
|
|
68
|
-
});
|
|
69
|
-
if (matchKeys?.length === 1) {
|
|
70
|
-
const matchKey = matchKeys[0];
|
|
71
|
-
return dynamicViewsModules[matchKey];
|
|
72
|
-
}
|
|
73
|
-
if (matchKeys?.length > 1) {
|
|
74
|
-
throw new Error(
|
|
75
|
-
'Please do not create `.vue` and `.TSX` files with the same file name in the same hierarchical directory under the views folder. This will cause dynamic introduction failure',
|
|
76
|
-
);
|
|
77
|
-
} else {
|
|
78
|
-
console.warn(`Can't find ${component} in pages folder`);
|
|
79
|
-
}
|
|
80
|
-
return EXCEPTION_COMPONENT;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 将背景对象变成路由对象
|
|
84
|
-
export function transformObjectToRoute<T = RouteItem>(routeList: RouteItem[]): T[] {
|
|
85
|
-
routeList.forEach(async (route) => {
|
|
86
|
-
const component = route.component as string;
|
|
87
|
-
|
|
88
|
-
if (component) {
|
|
89
|
-
if (component.toUpperCase() === 'LAYOUT') {
|
|
90
|
-
route.component = LayoutMap.get(component.toUpperCase());
|
|
91
|
-
} else {
|
|
92
|
-
route.children = [cloneDeep(route)];
|
|
93
|
-
route.component = LAYOUT;
|
|
94
|
-
route.name = `${route.name}Parent`;
|
|
95
|
-
route.path = '';
|
|
96
|
-
route.meta = (route.meta || {}) as RouteMeta;
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
throw new Error('component is undefined');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
route.children && asyncImportRoute(route.children);
|
|
103
|
-
|
|
104
|
-
// 动态从包内引入单个Icon,如果没有网络环境可以使用这种方式 但是会导致产物存在多个chunk
|
|
105
|
-
// if (route.meta.icon)
|
|
106
|
-
// route.meta.icon = await getMenuIcon(route.meta.icon);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
return [PAGE_NOT_FOUND_ROUTE, ...routeList] as unknown as T[];
|
|
110
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
declare function postMessage(message: unknown): void;
|
|
2
|
-
|
|
3
|
-
let timer: ReturnType<typeof setInterval> | null = null;
|
|
4
|
-
let currentVersion = '';
|
|
5
|
-
let checkUrl = '';
|
|
6
|
-
let interval = 60_000;
|
|
7
|
-
|
|
8
|
-
async function check() {
|
|
9
|
-
try {
|
|
10
|
-
const res = await fetch(`${checkUrl}?t=${Date.now()}`);
|
|
11
|
-
if (!res.ok) return;
|
|
12
|
-
const data = await res.json();
|
|
13
|
-
if (data.version && data.version !== currentVersion) {
|
|
14
|
-
postMessage({ type: 'update-available', version: data.version });
|
|
15
|
-
stop();
|
|
16
|
-
}
|
|
17
|
-
} catch {
|
|
18
|
-
// Network error — silently ignore, next tick will retry
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function stop() {
|
|
23
|
-
if (timer) {
|
|
24
|
-
clearInterval(timer);
|
|
25
|
-
timer = null;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function start() {
|
|
30
|
-
stop();
|
|
31
|
-
check();
|
|
32
|
-
timer = setInterval(check, interval);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
onmessage = (e: MessageEvent) => {
|
|
36
|
-
const msg = e.data;
|
|
37
|
-
|
|
38
|
-
switch (msg.type) {
|
|
39
|
-
case 'start':
|
|
40
|
-
currentVersion = msg.version;
|
|
41
|
-
checkUrl = msg.url;
|
|
42
|
-
interval = msg.interval || 60_000;
|
|
43
|
-
start();
|
|
44
|
-
break;
|
|
45
|
-
|
|
46
|
-
case 'pause':
|
|
47
|
-
stop();
|
|
48
|
-
break;
|
|
49
|
-
|
|
50
|
-
case 'resume':
|
|
51
|
-
start();
|
|
52
|
-
break;
|
|
53
|
-
|
|
54
|
-
case 'stop':
|
|
55
|
-
stop();
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
};
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { ElNotification } from 'element-plus';
|
|
2
|
-
import { h } from 'vue';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 前端版本热更新检测模块(Element Plus 版)
|
|
6
|
-
*
|
|
7
|
-
* 工作原理:
|
|
8
|
-
* 1. 应用启动时,从 <meta name="app-version"> 读取当前构建版本号
|
|
9
|
-
* 2. 启动一个 Web Worker 按固定间隔轮询 version.json(构建时生成)
|
|
10
|
-
* 3. Worker 发现远端版本号与当前不一致时,通过 postMessage 通知主线程
|
|
11
|
-
* 4. 主线程弹出确认弹窗,用户可选择"立即刷新"或"稍后提醒"
|
|
12
|
-
* 5. 选择稍后提醒时,等待 SNOOZE_INTERVAL 后重新开始轮询
|
|
13
|
-
*
|
|
14
|
-
* 附加行为:
|
|
15
|
-
* - 页面切到后台时暂停轮询,回到前台时恢复(节省资源)
|
|
16
|
-
* - 开发环境(DEV)下不启用检测
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/** 正常轮询间隔:60 秒 */
|
|
20
|
-
const POLL_INTERVAL = 60 * 1000;
|
|
21
|
-
|
|
22
|
-
/** 用户点击"稍后提醒"后的静默期:5 分钟,期间不再弹窗 */
|
|
23
|
-
const SNOOZE_INTERVAL = 5 * 60 * 1000;
|
|
24
|
-
|
|
25
|
-
/** 负责后台轮询的 Web Worker 实例 */
|
|
26
|
-
let worker: Worker | null = null;
|
|
27
|
-
|
|
28
|
-
/** 防重复弹窗标记:同一轮检测周期内只弹一次 */
|
|
29
|
-
let hasNotified = false;
|
|
30
|
-
|
|
31
|
-
/** 当前前端构建版本号,来自 HTML meta 标签 */
|
|
32
|
-
let currentVersion = '';
|
|
33
|
-
|
|
34
|
-
/** version.json 的完整 URL(含 BASE_URL 前缀,适配子路径部署) */
|
|
35
|
-
let versionUrl = '';
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* 初始化版本检测,应在应用挂载后调用一次
|
|
39
|
-
*
|
|
40
|
-
* 流程:
|
|
41
|
-
* 1. 开发环境直接跳过
|
|
42
|
-
* 2. 读取 meta 标签中的版本号,读不到则放弃
|
|
43
|
-
* 3. 创建 Web Worker;浏览器不支持时静默降级
|
|
44
|
-
* 4. 监听 Worker 消息,收到 update-available 则弹窗
|
|
45
|
-
* 5. 监听 visibilitychange 实现前后台暂停/恢复
|
|
46
|
-
*/
|
|
47
|
-
export function initVersionCheck() {
|
|
48
|
-
if (import.meta.env.DEV) return;
|
|
49
|
-
|
|
50
|
-
currentVersion = document.querySelector('meta[name="app-version"]')?.getAttribute('content') || '';
|
|
51
|
-
if (!currentVersion) return;
|
|
52
|
-
|
|
53
|
-
versionUrl = `${import.meta.env.BASE_URL}version.json`;
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
worker = new Worker(new URL('./version-check.worker.ts', import.meta.url), { type: 'module' });
|
|
57
|
-
} catch {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
worker.onmessage = (e: MessageEvent) => {
|
|
62
|
-
if (e.data.type === 'update-available' && !hasNotified) {
|
|
63
|
-
hasNotified = true;
|
|
64
|
-
showUpdateDialog();
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
worker.postMessage({ type: 'start', version: currentVersion, url: versionUrl, interval: POLL_INTERVAL });
|
|
69
|
-
|
|
70
|
-
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 页面可见性变化回调
|
|
75
|
-
* - 回到前台:恢复轮询
|
|
76
|
-
* - 切到后台:暂停轮询(避免不必要的网络请求)
|
|
77
|
-
*/
|
|
78
|
-
function handleVisibilityChange() {
|
|
79
|
-
if (!worker || hasNotified) return;
|
|
80
|
-
if (document.visibilityState === 'visible') {
|
|
81
|
-
worker.postMessage({ type: 'resume', interval: POLL_INTERVAL });
|
|
82
|
-
} else {
|
|
83
|
-
worker.postMessage({ type: 'pause' });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* 弹出版本更新通知
|
|
89
|
-
* - 点击"立即刷新":刷新页面加载新版本
|
|
90
|
-
* - 关闭通知:进入静默期后重新开始检测
|
|
91
|
-
*/
|
|
92
|
-
function showUpdateDialog() {
|
|
93
|
-
const notification = ElNotification({
|
|
94
|
-
title: '发现新版本',
|
|
95
|
-
message: h('div', [
|
|
96
|
-
h('p', { style: 'margin: 0 0 12px' }, '系统已更新,请刷新页面以获取最新功能和修复。'),
|
|
97
|
-
h('div', { style: 'text-align: right' }, [
|
|
98
|
-
h(
|
|
99
|
-
'button',
|
|
100
|
-
{
|
|
101
|
-
style: 'margin-right: 8px; padding: 5px 15px; cursor: pointer; border: 1px solid #dcdfe6; border-radius: 4px; background: #fff; color: #606266; font-size: 14px;',
|
|
102
|
-
onClick: () => {
|
|
103
|
-
notification.close();
|
|
104
|
-
scheduleRecheck();
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
'稍后提醒',
|
|
108
|
-
),
|
|
109
|
-
h(
|
|
110
|
-
'button',
|
|
111
|
-
{
|
|
112
|
-
style: 'padding: 5px 15px; cursor: pointer; border: none; border-radius: 4px; background: #409eff; color: #fff; font-size: 14px;',
|
|
113
|
-
onClick: () => {
|
|
114
|
-
notification.close();
|
|
115
|
-
window.location.reload();
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
'立即刷新',
|
|
119
|
-
),
|
|
120
|
-
]),
|
|
121
|
-
]),
|
|
122
|
-
type: 'info',
|
|
123
|
-
position: 'bottom-right',
|
|
124
|
-
duration: 0,
|
|
125
|
-
showClose: false,
|
|
126
|
-
onClose: scheduleRecheck,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* 安排延迟重新检测
|
|
132
|
-
* 重置弹窗标记,等待 SNOOZE_INTERVAL 后重新启动 Worker 轮询
|
|
133
|
-
*/
|
|
134
|
-
function scheduleRecheck() {
|
|
135
|
-
hasNotified = false;
|
|
136
|
-
setTimeout(() => {
|
|
137
|
-
worker?.postMessage({ type: 'start', version: currentVersion, url: versionUrl, interval: POLL_INTERVAL });
|
|
138
|
-
}, SNOOZE_INTERVAL);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* 销毁版本检测,应在应用卸载时调用
|
|
143
|
-
* 移除事件监听、停止并终止 Worker,释放资源
|
|
144
|
-
*/
|
|
145
|
-
export function destroyVersionCheck() {
|
|
146
|
-
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
147
|
-
worker?.postMessage({ type: 'stop' });
|
|
148
|
-
worker?.terminate();
|
|
149
|
-
worker = null;
|
|
150
|
-
}
|
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
# 前端版本热更新检测
|
|
2
|
-
|
|
3
|
-
## 概述
|
|
4
|
-
|
|
5
|
-
本项目实现了一套前端版本热更新检测机制,用于在系统发布新版本后,**自动通知用户刷新页面**以获取最新代码。
|
|
6
|
-
|
|
7
|
-
核心思路:每次构建生成唯一版本号,运行时通过 Web Worker 后台轮询版本文件,发现版本不一致即弹窗提示。
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 涉及文件
|
|
12
|
-
|
|
13
|
-
| 文件 | 层级 | 职责 |
|
|
14
|
-
|------|------|------|
|
|
15
|
-
| `build/vite-plugin-version.ts` | 构建层 | Vite 插件,构建时生成版本号并写入 HTML 和 JSON |
|
|
16
|
-
| `src/utils/version-update.ts` | 运行层(主线程) | 初始化检测、管理 Worker 生命周期、弹窗交互 |
|
|
17
|
-
| `src/utils/version-check.worker.ts` | 运行层(Worker 线程) | 后台定时轮询 `version.json` 并比对版本号 |
|
|
18
|
-
| `src/main.ts` | 入口层 | 应用启动后调用 `initVersionCheck()` |
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## 架构总览
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
┌─────────────────────────────────────────────────────────────────┐
|
|
26
|
-
│ 构建阶段 (Vite Build) │
|
|
27
|
-
│ │
|
|
28
|
-
│ vite-plugin-version.ts │
|
|
29
|
-
│ ├─ 生成版本号: "1742000000000.a3f2k1" │
|
|
30
|
-
│ ├─ 注入 <meta name="app-version" content="..."> 到 index.html │
|
|
31
|
-
│ └─ 写入 dist/version.json { "version": "..." } │
|
|
32
|
-
└─────────────────────────────────────────────────────────────────┘
|
|
33
|
-
│
|
|
34
|
-
▼
|
|
35
|
-
┌─────────────────────────────────────────────────────────────────┐
|
|
36
|
-
│ 运行阶段 (Browser Runtime) │
|
|
37
|
-
│ │
|
|
38
|
-
│ main.ts │
|
|
39
|
-
│ └─ initVersionCheck() │
|
|
40
|
-
│ │ │
|
|
41
|
-
│ ▼ │
|
|
42
|
-
│ version-update.ts (主线程) │
|
|
43
|
-
│ ├─ 读取 <meta> 中的当前版本号 │
|
|
44
|
-
│ ├─ 创建 Web Worker │
|
|
45
|
-
│ ├─ 监听 Worker 消息 │
|
|
46
|
-
│ ├─ 监听 visibilitychange 事件 │
|
|
47
|
-
│ │ │
|
|
48
|
-
│ │ postMessage onmessage │
|
|
49
|
-
│ │ ─────────────► ┌──────────────────────┐ │
|
|
50
|
-
│ │ │ version-check │ │
|
|
51
|
-
│ │ ◄───────────── │ .worker.ts │ │
|
|
52
|
-
│ │ postMessage │ (Worker 线程) │ │
|
|
53
|
-
│ │ │ │ │
|
|
54
|
-
│ │ │ 每 60s fetch │ │
|
|
55
|
-
│ │ │ version.json │ │
|
|
56
|
-
│ │ └──────────────────────┘ │
|
|
57
|
-
│ │ │ │
|
|
58
|
-
│ │ ▼ │
|
|
59
|
-
│ │ 版本不一致? │
|
|
60
|
-
│ │ ├─ 是 → postMessage('update-available') │
|
|
61
|
-
│ │ └─ 否 → 等待下一轮 │
|
|
62
|
-
│ │ │
|
|
63
|
-
│ ▼ │
|
|
64
|
-
│ 弹窗提示用户 │
|
|
65
|
-
│ ├─ "立即刷新" → window.location.reload() │
|
|
66
|
-
│ └─ "稍后提醒" → 5 分钟后重新轮询 │
|
|
67
|
-
└─────────────────────────────────────────────────────────────────┘
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## 详细流程
|
|
73
|
-
|
|
74
|
-
### 1. 构建阶段
|
|
75
|
-
|
|
76
|
-
**文件:`build/vite-plugin-version.ts`**
|
|
77
|
-
|
|
78
|
-
构建时,Vite 插件自动执行以下操作:
|
|
79
|
-
|
|
80
|
-
1. **生成版本号**:使用 `Date.now()` + 6 位随机字符串,确保每次构建唯一
|
|
81
|
-
```
|
|
82
|
-
例如:"1742000000000.a3f2k1"
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
2. **注入 HTML meta 标签**:通过 `transformIndexHtml` 钩子,在 `<head>` 中注入:
|
|
86
|
-
```html
|
|
87
|
-
<meta name="app-version" content="1742000000000.a3f2k1">
|
|
88
|
-
```
|
|
89
|
-
这个标签会随 `index.html` 一起被浏览器缓存,代表用户当前加载的版本。
|
|
90
|
-
|
|
91
|
-
3. **生成 version.json**:通过 `closeBundle` 钩子,在产物目录写入:
|
|
92
|
-
```json
|
|
93
|
-
{ "version": "1742000000000.a3f2k1" }
|
|
94
|
-
```
|
|
95
|
-
这个文件部署后可被远程访问,代表服务器上的最新版本。
|
|
96
|
-
|
|
97
|
-
> 插件设置了 `apply: 'build'`,仅在生产构建时生效,开发模式不产生任何输出。
|
|
98
|
-
|
|
99
|
-
---
|
|
100
|
-
|
|
101
|
-
### 2. 应用启动
|
|
102
|
-
|
|
103
|
-
**文件:`src/main.ts`**
|
|
104
|
-
|
|
105
|
-
应用挂载到 DOM 后调用 `initVersionCheck()`:
|
|
106
|
-
|
|
107
|
-
```typescript
|
|
108
|
-
app.mount('#app');
|
|
109
|
-
|
|
110
|
-
import { initVersionCheck } from './utils/version-update';
|
|
111
|
-
initVersionCheck();
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
放在 `mount()` 之后是因为需要确保 DOM 中的 `<meta>` 标签已经可以被 `querySelector` 读取到。
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
### 3. 初始化检测
|
|
119
|
-
|
|
120
|
-
**文件:`src/utils/version-update.ts` → `initVersionCheck()`**
|
|
121
|
-
|
|
122
|
-
```
|
|
123
|
-
开发环境?─── 是 ──→ 直接返回(不检测)
|
|
124
|
-
│
|
|
125
|
-
否
|
|
126
|
-
▼
|
|
127
|
-
读取 <meta name="app-version"> 的 content
|
|
128
|
-
│
|
|
129
|
-
空?─── 是 ──→ 直接返回
|
|
130
|
-
│
|
|
131
|
-
否
|
|
132
|
-
▼
|
|
133
|
-
创建 Web Worker(version-check.worker.ts)
|
|
134
|
-
│
|
|
135
|
-
失败?── 是 ──→ 静默降级,不检测
|
|
136
|
-
│
|
|
137
|
-
否
|
|
138
|
-
▼
|
|
139
|
-
监听 Worker 的 onmessage 事件
|
|
140
|
-
│
|
|
141
|
-
▼
|
|
142
|
-
发送 start 指令给 Worker(携带版本号、URL、轮询间隔)
|
|
143
|
-
│
|
|
144
|
-
▼
|
|
145
|
-
注册 visibilitychange 事件监听
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
---
|
|
149
|
-
|
|
150
|
-
### 4. 后台轮询
|
|
151
|
-
|
|
152
|
-
**文件:`src/utils/version-check.worker.ts`**
|
|
153
|
-
|
|
154
|
-
Worker 收到 `start` 指令后:
|
|
155
|
-
|
|
156
|
-
1. **立即执行一次检测**(不等第一个间隔周期)
|
|
157
|
-
2. **启动定时器**,默认每 **60 秒** 执行一次检测
|
|
158
|
-
3. 每次检测:
|
|
159
|
-
- `fetch` 请求 `version.json?t={timestamp}`(时间戳参数绕过 CDN/浏览器缓存)
|
|
160
|
-
- 解析返回的 JSON,提取 `version` 字段
|
|
161
|
-
- 与启动时传入的 `currentVersion` 比较
|
|
162
|
-
- **不一致** → `postMessage({ type: 'update-available' })` 通知主线程,并停止轮询
|
|
163
|
-
- **一致** → 什么都不做,等待下一轮
|
|
164
|
-
- **网络错误** → 静默忽略,下一轮自动重试
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
|
|
168
|
-
### 5. 页面可见性优化
|
|
169
|
-
|
|
170
|
-
**文件:`src/utils/version-update.ts` → `handleVisibilityChange()`**
|
|
171
|
-
|
|
172
|
-
监听 `document.visibilitychange` 事件:
|
|
173
|
-
|
|
174
|
-
| 场景 | 动作 | 目的 |
|
|
175
|
-
|------|------|------|
|
|
176
|
-
| 用户切到其他标签页 / 最小化窗口 | 发送 `pause` → Worker 清除定时器 | 避免后台无意义的网络请求 |
|
|
177
|
-
| 用户切回页面 | 发送 `resume` → Worker 重新启动轮询 | 回到前台立即检测一次并恢复定时 |
|
|
178
|
-
|
|
179
|
-
---
|
|
180
|
-
|
|
181
|
-
### 6. 弹窗交互
|
|
182
|
-
|
|
183
|
-
**文件:`src/utils/version-update.ts` → `showUpdateDialog()`**
|
|
184
|
-
|
|
185
|
-
主线程收到 `update-available` 消息后,使用 TDesign 的 `DialogPlugin.confirm` 弹出确认框:
|
|
186
|
-
|
|
187
|
-
```
|
|
188
|
-
┌─────────────────────────────────┐
|
|
189
|
-
│ 发现新版本 │
|
|
190
|
-
│ │
|
|
191
|
-
│ 系统已更新,请刷新页面 │
|
|
192
|
-
│ 以获取最新功能和修复。 │
|
|
193
|
-
│ │
|
|
194
|
-
│ [稍后提醒] [立即刷新] │
|
|
195
|
-
└─────────────────────────────────┘
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
用户操作对应的行为:
|
|
199
|
-
|
|
200
|
-
| 操作 | 行为 |
|
|
201
|
-
|------|------|
|
|
202
|
-
| **立即刷新** | `window.location.reload()` 强制刷新,加载新版本 |
|
|
203
|
-
| **稍后提醒** | 进入 5 分钟静默期,之后重新开始轮询 |
|
|
204
|
-
| **关闭弹窗**(点 X) | 同"稍后提醒" |
|
|
205
|
-
|
|
206
|
-
---
|
|
207
|
-
|
|
208
|
-
### 7. 稍后提醒机制
|
|
209
|
-
|
|
210
|
-
**文件:`src/utils/version-update.ts` → `scheduleRecheck()`**
|
|
211
|
-
|
|
212
|
-
用户选择"稍后提醒"后:
|
|
213
|
-
|
|
214
|
-
1. 重置 `hasNotified = false`(允许再次弹窗)
|
|
215
|
-
2. 设置 **5 分钟**(`SNOOZE_INTERVAL`)的 `setTimeout`
|
|
216
|
-
3. 超时后重新向 Worker 发送 `start` 指令,开始新一轮轮询
|
|
217
|
-
4. 检测到新版本后再次弹窗,如此循环直到用户刷新
|
|
218
|
-
|
|
219
|
-
---
|
|
220
|
-
|
|
221
|
-
### 8. 销毁清理
|
|
222
|
-
|
|
223
|
-
**文件:`src/utils/version-update.ts` → `destroyVersionCheck()`**
|
|
224
|
-
|
|
225
|
-
应用卸载时调用,执行以下清理:
|
|
226
|
-
|
|
227
|
-
1. 移除 `visibilitychange` 事件监听
|
|
228
|
-
2. 向 Worker 发送 `stop` 指令(清除 Worker 内部定时器)
|
|
229
|
-
3. `worker.terminate()` 终止 Worker 线程
|
|
230
|
-
4. 置空 Worker 引用
|
|
231
|
-
|
|
232
|
-
---
|
|
233
|
-
|
|
234
|
-
## 时间参数
|
|
235
|
-
|
|
236
|
-
| 参数 | 值 | 说明 |
|
|
237
|
-
|------|------|------|
|
|
238
|
-
| `POLL_INTERVAL` | 60 秒 | Worker 正常轮询间隔 |
|
|
239
|
-
| `SNOOZE_INTERVAL` | 5 分钟 | 用户点击"稍后提醒"后的静默期 |
|
|
240
|
-
|
|
241
|
-
---
|
|
242
|
-
|
|
243
|
-
## 为什么使用 Web Worker?
|
|
244
|
-
|
|
245
|
-
1. **不阻塞主线程**:`setInterval` + `fetch` 在 Worker 中执行,不影响 UI 渲染和交互响应
|
|
246
|
-
2. **独立生命周期**:Worker 有自己的事件循环,即使主线程繁忙(如大量 DOM 操作),轮询也不会被延迟
|
|
247
|
-
3. **干净的通信模型**:通过 `postMessage` 解耦,主线程只关心"有没有新版本",不关心轮询细节
|
|
248
|
-
|
|
249
|
-
---
|
|
250
|
-
|
|
251
|
-
## 开发环境行为
|
|
252
|
-
|
|
253
|
-
开发环境下,版本检测**完全不生效**,双重保障:
|
|
254
|
-
|
|
255
|
-
1. **构建层**:`vite-plugin-version` 设置了 `apply: 'build'`,`dev` 模式不注入 meta、不生成 version.json
|
|
256
|
-
2. **运行层**:`initVersionCheck()` 第一行 `if (import.meta.env.DEV) return` 直接跳过
|
|
257
|
-
|
|
258
|
-
---
|
|
259
|
-
|
|
260
|
-
## 缓存策略
|
|
261
|
-
|
|
262
|
-
- **`version.json`**:请求时附带 `?t={timestamp}` 查询参数,绕过浏览器缓存和 CDN 缓存,确保每次拿到最新内容
|
|
263
|
-
- **`index.html`**:服务器应配置为 `Cache-Control: no-cache`,确保用户刷新后能拿到最新的 HTML(包含新的 meta 标签)
|
|
264
|
-
- **JS/CSS 资源**:Vite 构建产物自带 hash 文件名,天然支持长期缓存,新版本会生成新的文件名
|
|
265
|
-
|
|
266
|
-
---
|
|
267
|
-
|
|
268
|
-
## 边界场景处理
|
|
269
|
-
|
|
270
|
-
| 场景 | 处理方式 |
|
|
271
|
-
|------|------|
|
|
272
|
-
| 浏览器不支持 Web Worker | `try/catch` 捕获异常,静默降级,不影响正常使用 |
|
|
273
|
-
| `<meta>` 标签缺失 | `initVersionCheck()` 检查到空值后直接返回 |
|
|
274
|
-
| 网络请求失败 | Worker 中 `catch` 静默忽略,等下一轮自动重试 |
|
|
275
|
-
| `version.json` 返回非 200 | `res.ok` 检查不通过,跳过本次比对 |
|
|
276
|
-
| 用户反复点击"稍后提醒" | 每次都重新进入 5 分钟静默期,不会叠加弹窗 |
|
|
277
|
-
| 同一检测周期内重复触发 | `hasNotified` 标记防止重复弹窗 |
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { writeFileSync } from 'node:fs';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
|
|
4
|
-
import type { Plugin } from 'vite';
|
|
5
|
-
|
|
6
|
-
export function vitePluginVersion(): Plugin {
|
|
7
|
-
const version = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
8
|
-
let outDir: string;
|
|
9
|
-
|
|
10
|
-
return {
|
|
11
|
-
name: 'vite-plugin-version',
|
|
12
|
-
apply: 'build',
|
|
13
|
-
|
|
14
|
-
configResolved(config) {
|
|
15
|
-
outDir = resolve(config.root, config.build.outDir);
|
|
16
|
-
},
|
|
17
|
-
|
|
18
|
-
transformIndexHtml() {
|
|
19
|
-
return [
|
|
20
|
-
{
|
|
21
|
-
tag: 'meta',
|
|
22
|
-
attrs: { name: 'app-version', content: version },
|
|
23
|
-
injectTo: 'head' as const,
|
|
24
|
-
},
|
|
25
|
-
];
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
closeBundle() {
|
|
29
|
-
const filePath = resolve(outDir, 'version.json');
|
|
30
|
-
writeFileSync(filePath, JSON.stringify({ version }, null, 2));
|
|
31
|
-
|
|
32
|
-
console.log(`\n✅ Version file generated: ${version}\n`);
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
}
|
package/test/1/vite.config.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
|
|
3
|
-
import vue from '@vitejs/plugin-vue';
|
|
4
|
-
import vueJsx from '@vitejs/plugin-vue-jsx';
|
|
5
|
-
import type { ConfigEnv, UserConfig } from 'vite';
|
|
6
|
-
import { loadEnv } from 'vite';
|
|
7
|
-
import svgLoader from 'vite-svg-loader';
|
|
8
|
-
|
|
9
|
-
import { vitePluginVersion } from './build/vite-plugin-version';
|
|
10
|
-
|
|
11
|
-
const CWD = process.cwd();
|
|
12
|
-
|
|
13
|
-
// https://vitejs.dev/config/
|
|
14
|
-
export default ({ mode }: ConfigEnv): UserConfig => {
|
|
15
|
-
const { VITE_BASE_URL, VITE_API_URL_PREFIX } = loadEnv(mode, CWD);
|
|
16
|
-
return {
|
|
17
|
-
base: VITE_BASE_URL,
|
|
18
|
-
resolve: {
|
|
19
|
-
alias: {
|
|
20
|
-
'@': path.resolve(__dirname, './src'),
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
-
css: {
|
|
25
|
-
preprocessorOptions: {
|
|
26
|
-
less: {
|
|
27
|
-
modifyVars: {
|
|
28
|
-
hack: `true; @import (reference) "${path.resolve('src/style/variables.less')}";`,
|
|
29
|
-
},
|
|
30
|
-
math: 'strict',
|
|
31
|
-
javascriptEnabled: true,
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
|
|
36
|
-
plugins: [vue(), vueJsx(), svgLoader(), vitePluginVersion()],
|
|
37
|
-
|
|
38
|
-
server: {
|
|
39
|
-
port: 3002,
|
|
40
|
-
host: '0.0.0.0',
|
|
41
|
-
proxy: {
|
|
42
|
-
[VITE_API_URL_PREFIX]: 'http://127.0.0.1:3000/',
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
};
|