看到检测到新版本后自动更新版本, 第一反应想到就是ServiceWorker, 是的, 使用ServiceWorker好处多多, 包括提示更新版本, 但是使用ServiceWorker有一个硬性条件, 那就是必须是 https 或者 localhost, 一些时候需要部署在内网时, 就没法实现了, 那么就必须换个思路来

大概思路就是: 请求一个文件, 读取该文件内容里的时间戳, 和本地缓存里的时间戳做对比, 如果不一致就给出提示, 让用户点击刷新

第一步: 那就是编译时, 附带生成一个文件, 并记录编译时的时间戳
这个我们可以写一个简单的vite插件, 编译后自动生成一个json文件, 当然不嫌麻烦, 也可以手动来改动一个文件

export default defineConfig(({ mode }: ConfigEnv) => {
    // 编译后保存文件的文件, 一般是dist文件夹
    const outDir = 'dist'
    return {
        // 其他配置
        plugins: [
            // 其他配置
            {
                name: 'generate-timestamp',
                closeBundle() {
                    // 我们记录这几个数据
                    const buildInfo = {
                        buildTime: new Date().toISOString(),
                        timestamp: Date.now(),
                        buildMode: process.env.VITE_APP_ENV || 'production',
                        outDir,
                    }

                    const content = JSON.stringify(buildInfo, null, 2)
                    const outputPath = path.resolve(__dirname, outDir, 'timestamp.json')

                    // 确保 outputPath 目录存在
                    fs.mkdirSync(path.dirname(outputPath), { recursive: true })

                    // 写入文件
                    fs.writeFileSync(outputPath, content)
                    console.log(`时间戳文件已生成: ${outputPath}`)
                },
            },
        ],
    }
})
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

第二步: 编写检测更新的逻辑

interface TimestampData {
    timestamp: string | number
}

class TimestampChecker {
    private readonly LOCAL_STORAGE_KEY = 'last_timestamp'
    private readonly CHECK_INTERVAL = 2000 // 2秒
    private readonly API_URL = '/timestamp.json'

    private intervalId: number | null = null

    /**
     * 开始定时检查时间戳
     */
    start(): void {
        if (this.intervalId !== null) {
            console.warn('时间戳检查器已经在运行中')
            return
        }

        // 立即执行一次检查
        this.checkTimestamp()

        // 设置定时器
        this.intervalId = window.setInterval(() => {
            this.checkTimestamp()
        }, this.CHECK_INTERVAL)

        console.log('时间戳检查器已启动')
    }

    /**
     * 停止定时检查
     */
    stop(): void {
        if (this.intervalId !== null) {
            window.clearInterval(this.intervalId)
            this.intervalId = null
            console.log('时间戳检查器已停止')
        }
    }

    /**
     * 检查时间戳
     */
    private async checkTimestamp(): Promise<void> {
        try {
            const remoteTimestamp = await this.fetchTimestamp()
            this.compareTimestamp(remoteTimestamp)
        }
        catch (error) {
            console.error('获取时间戳失败:', error)
        }
    }

    /**
     * 获取远程时间戳
     */
    private async fetchTimestamp(): Promise<string> {
        const response = await fetch(this.API_URL)

        if (!response.ok) {
            throw new Error(`HTTP错误: ${response.status}`)
        }

        const data: TimestampData = await response.json()

        // 确保时间戳是字符串类型
        return String(data.timestamp)
    }

    /**
     * 比较时间戳
     */
    private compareTimestamp(remoteTimestamp: string): void {
        const localTimestamp = localStorage.getItem(this.LOCAL_STORAGE_KEY)

        if (localTimestamp === null) {
            // 本地存储为空,写入时间戳
            localStorage.setItem(this.LOCAL_STORAGE_KEY, remoteTimestamp)
            console.log('初始化本地时间戳:', remoteTimestamp)
        }
        else if (localTimestamp !== remoteTimestamp) {
            // 时间戳发生变化,给出提示
            this.notifyTimestampChanged(localTimestamp, remoteTimestamp)

            // 更新本地存储的时间戳
            localStorage.setItem(this.LOCAL_STORAGE_KEY, remoteTimestamp)
        }
        else {
            // 时间戳相同,无变化
            console.debug('时间戳未变化:', remoteTimestamp)
        }
    }

    /**
     * 时间戳变化通知
     */
    private notifyTimestampChanged(oldTimestamp: string, newTimestamp: string): void {
        const message = `时间戳已更新:\n旧时间戳: ${oldTimestamp}\n新时间戳: ${newTimestamp}`

        console.log('时间戳变化通知:', message)

        // 触发自定义事件
        const event = new CustomEvent('timestamp-changed', {
            detail: { oldTimestamp, newTimestamp },
        })
        window.dispatchEvent(event)
    }

    /**
     * 获取当前本地存储的时间戳
     */
    getCurrentTimestamp(): string | null {
        return localStorage.getItem(this.LOCAL_STORAGE_KEY)
    }

    /**
     * 清空本地存储的时间戳
     */
    clearTimestamp(): void {
        localStorage.removeItem(this.LOCAL_STORAGE_KEY)
        console.log('已清空本地时间戳')
    }
}

export default TimestampChecker

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

第三步: 在入口文件里调用

// main.ts

// 其他代码
app.mount('#app')

// 这里判断下, 不在开发环境和SSR环境执行
if (!import.meta.env.DEV && import.meta.env.SSR !== true) {
    const timestampChecker = new TimestampChecker()

    // 开始检查
    timestampChecker.start()

    // 如果需要停止检查
    // timestampChecker.stop();

    // 获取当前存储的时间戳
    // const current = timestampChecker.getCurrentTimestamp();

    // 清空时间戳
    // timestampChecker.clearTimestamp();

    // 监听自定义事件
    window.addEventListener('timestamp-changed', (event: Event) => {
        const customEvent = event as CustomEvent
        console.log('时间戳变化事件:', customEvent.detail)
        ElNotification({
            type: 'success',
            title: '通知',
            dangerouslyUseHTMLString: true,
            message: '<div>新内容可用,单击<b style="color: red; cursor: pointer;">这里</b>更新 (刷新前请确认所有内容都已保存)</div>',
            onClick() {
                window.location.reload()
            },
            duration: 20000,
        })
    })
}
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