vite-plugin-sw-offline 1.0.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.
package/README.md ADDED
@@ -0,0 +1,414 @@
1
+ # vite-plugin-sw-offline
2
+
3
+ 面向 **Vite H5 / uni-app H5** 的 **单一 Vite 插件**:开发环境通过中间件直出 Service Worker、离线页与配套资源;生产构建结束时写入产出目录,并与 `public/` 复制策略协同。
4
+
5
+ ## 目录
6
+
7
+ - [功能概览](#功能概览)
8
+ - [安装与 Peer 依赖](#安装与-peer-依赖)
9
+ - [快速接入](#快速接入)
10
+ - [版本号(swVersion)](#版本号swversion)
11
+ - [维护接口探测(networkProbeUrl)](#维护接口探测networkprobeurl)
12
+ - [开发 vs 生产](#开发-vs-生产)
13
+ - [public 目录约定](#public-目录约定)
14
+ - [配置项](#配置项)
15
+ - [构建时注入占位符](#构建时注入占位符)
16
+ - [SW 缓存策略简述](#sw-缓存策略简述)
17
+ - [页面侧 API(window.swCache)](#页面侧-apiwindowswcache)
18
+ - [程序化导出](#程序化导出)
19
+ - [自定义离线页](#自定义离线页)
20
+ - [包内文件结构](#包内文件结构)
21
+ - [发布到 npm](#发布到-npm)
22
+
23
+ ## 功能概览
24
+
25
+ | 能力 | 说明 |
26
+ |------|------|
27
+ | **Service Worker** | 内置 `runtime/sw.js`:带 hash 静态资源、图片 SWR、可配置 API 白名单、导航离线兜底等 |
28
+ | **离线页** | 默认 `templates/default/offline.html`,支持自定义模板;可注入 Logo、域名 |
29
+ | **默认背景图** | 包内 `assets/offline-bg.jpg` → 产出 `static/offline-bg.jpg`,与离线页 CSS 一致 |
30
+ | **注册脚本** | `sw-register.js` 注册 SW 并挂载 `window.swCache`;`sw-noop.js` 用于卸载场景 |
31
+ | **uni-app** | 识别 CLI `vite build --outDir xxx`,写入实际产出目录 |
32
+
33
+ 构建 / 开发时由插件**替换源码占位符**(API 白名单、探测 URL、超时、版本号等),无需在业务 `public/` 维护 `sw.js`。
34
+
35
+ ## 安装与 Peer 依赖
36
+
37
+ ```bash
38
+ npm install vite-plugin-sw-offline -D
39
+ # pnpm add vite-plugin-sw-offline -D
40
+ # yarn add vite-plugin-sw-offline -D
41
+ ```
42
+
43
+ 需已安装 **Vite 5 或 6**:
44
+
45
+ ```bash
46
+ npm install vite -D
47
+ ```
48
+
49
+ 本包为 **CommonJS**(`main` → `src/index.js`)。在 `vite.config.ts` 中可用 `createRequire`:
50
+
51
+ ```js
52
+ import { createRequire } from 'node:module';
53
+ const require = createRequire(import.meta.url);
54
+ const { vitePluginSwOffline } = require('vite-plugin-sw-offline');
55
+ ```
56
+
57
+ ## 快速接入
58
+
59
+ ### 1. `vite.config.js`
60
+
61
+ ```js
62
+ const { vitePluginSwOffline } = require('vite-plugin-sw-offline');
63
+
64
+ export default defineConfig({
65
+ plugins: [
66
+ vitePluginSwOffline({
67
+ outDir: 'dist',
68
+ offlineLogoPath: '/static/logos/your-logo.png',
69
+ offlineDomain: 'https://www.example.com',
70
+ cacheableApiPaths: ['/user/getUserInfo.do', '/config/queryConfig.do']
71
+ // networkProbeUrl: () => '...', // 可选,见下文
72
+ // serviceWorker: { apiTimeout: 45000 },
73
+ })
74
+ ]
75
+ });
76
+ ```
77
+
78
+ **不必传 `swVersion`**,插件会自动生成(见下节)。
79
+
80
+ ### 2. `index.html`
81
+
82
+ 在入口 HTML 引入注册脚本,并保留版本占位(构建时由插件替换):
83
+
84
+ ```html
85
+ <script src="/sw-register.js?v=__SW_REGISTER_VERSION__"></script>
86
+ ```
87
+
88
+ ### 3. 业务白名单(推荐)
89
+
90
+ 将可缓存 API 路径集中在业务仓库(便于 Code Review),例如:
91
+
92
+ ```js
93
+ // src/configs/sw-cacheable-api-paths.js
94
+ export const CACHEABLE_SW_API_PATHS = [
95
+ '/config/queryConfig.do',
96
+ '/user/getUserInfo.do'
97
+ ];
98
+ ```
99
+
100
+ 在 `vite.config.js` 中 `cacheableApiPaths: CACHEABLE_SW_API_PATHS` 传入。不传则使用插件内置示例列表(**生产务必改为自己的列表**)。
101
+
102
+ ### 4. 本地 monorepo 引用
103
+
104
+ 未发布 npm 前可:
105
+
106
+ ```js
107
+ const { vitePluginSwOffline } = require('./packages/vite-plugin-sw-offline/src/index.js');
108
+ ```
109
+
110
+ 发布后改为 `require('vite-plugin-sw-offline')`。
111
+
112
+ ## 版本号(swVersion)
113
+
114
+ 用于 **缓存破坏**:让 Telegram WebView 等环境在每次部署后拉到新的 `sw.js` / `sw-register.js`。
115
+
116
+ | 注入位置 | 效果 |
117
+ |----------|------|
118
+ | `sw-register.js` | `navigator.serviceWorker.register('/sw.js?v=' + 版本)` |
119
+ | `index.html` | `<script src="/sw-register.js?v=版本">` |
120
+ | `sw.js` | `SW_BUILD_VERSION` 常量(主要用于 SW 控制台日志) |
121
+
122
+ **默认行为(推荐)**
123
+
124
+ - 不传 `swVersion` 时,插件在**实例化时**生成一次 `Date.now()` 字符串。
125
+ - 同一次 `vite dev` 或同一次 `vite build` 内版本不变;重新启动 dev / 重新 build 会换新版本。
126
+ - 终端会打印:`[vite-plugin-sw-offline] swVersion (auto): 1739...`
127
+
128
+ **手动覆盖(可选)**
129
+
130
+ ```js
131
+ vitePluginSwOffline({
132
+ swVersion: process.env.CI_COMMIT_SHA || String(Date.now())
133
+ })
134
+ ```
135
+
136
+ ## 维护接口探测(networkProbeUrl)
137
+
138
+ 导航请求前,SW 会先探测外网是否可达;失败则返回 `offline.html`。
139
+
140
+ | 配置 | 行为 |
141
+ |------|------|
142
+ | **未配置** / 空字符串 / 回调返回 `''` | 不注入 `NETWORK_PROBE_URL`,SW **探测同源 `/sw.js`**(适合纯本地调试) |
143
+ | **完整 URL 字符串** | 写入 `sw.js`,导航时用该地址做 HEAD 探测 |
144
+ | **`(ctx) => string` 回调** | 每次生成 `sw.js` 时调用(dev 中间件、build 写盘各一次) |
145
+
146
+ 回调参数 **`ctx`**:
147
+
148
+ | 字段 | 含义 |
149
+ |------|------|
150
+ | `command` | `serve`(dev)或 `build` |
151
+ | `mode` | Vite 的 `mode`(如 `development`、`production`、`test`) |
152
+ | `phase` | `serve` 或 `build` |
153
+
154
+ ### 网关模式说明(参考函数)
155
+
156
+ 以下针对可选参考函数 `buildSwNetworkProbeUrl(def, productCode)` 中的 **`def`**(环境配置对象):
157
+
158
+ | `def.useApiGateway` | 行为 |
159
+ |---------------------|------|
160
+ | `true` / `'true'` | 从 `baseURL`、`baseURLs` 首条、`maintainURL` 中取第一个合法绝对 URL,拼 `api.{主域}/wh/maintain/checkMaintain?productCode=...` |
161
+ | `false` | 仅用 `maintainURL` + `/maintain/checkMaintain?productCode=...` |
162
+ | 未设置 | 先试 `baseURL` 的 `origin + /wh`,否则 `maintainURL`,再拼 tail |
163
+
164
+ ### 参考实现:`buildSwNetworkProbeUrl`
165
+
166
+ 复制到 `vite.config.js` 或独立模块,按项目 env / 品牌名调整:
167
+
168
+ ```js
169
+ function buildSwNetworkProbeUrl(def, productCode) {
170
+ const PRODUCT_CODE = productCode != null ? String(productCode).trim() : '';
171
+ const hasProductCode = PRODUCT_CODE.length > 0;
172
+ if (!def || !hasProductCode) return '';
173
+
174
+ const isGateway = def.useApiGateway === true || def.useApiGateway === 'true';
175
+
176
+ if (isGateway && hasProductCode) {
177
+ let ref = null;
178
+ for (const raw of [def.baseURL, def.baseURLs && String(def.baseURLs).split(',')[0].trim(), def.maintainURL]) {
179
+ if (!raw) continue;
180
+ try {
181
+ const u = new URL(raw);
182
+ if (u.hostname && (u.protocol === 'http:' || u.protocol === 'https:')) {
183
+ ref = u;
184
+ break;
185
+ }
186
+ } catch {
187
+ /* continue */
188
+ }
189
+ }
190
+ if (!ref) return '';
191
+ const hostname = ref.hostname;
192
+ const parts = hostname.split('.');
193
+ const baseDomain = parts.length >= 3 ? parts.slice(-2).join('.') : hostname;
194
+ return `${ref.protocol}//api.${baseDomain}/wh/maintain/checkMaintain?productCode=${encodeURIComponent(PRODUCT_CODE)}`;
195
+ }
196
+
197
+ const tail = `/maintain/checkMaintain?productCode=${encodeURIComponent(PRODUCT_CODE)}`;
198
+ const trim = (s) => (s && String(s).replace(/\/$/, '')) || '';
199
+
200
+ let whBase = '';
201
+ if (def.useApiGateway === false) {
202
+ whBase = trim(def.maintainURL);
203
+ } else {
204
+ if (def.baseURL) {
205
+ try {
206
+ const u = new URL(def.baseURL);
207
+ if (u.protocol === 'http:' || u.protocol === 'https:') {
208
+ whBase = `${u.origin}/wh`;
209
+ }
210
+ } catch {
211
+ /* 相对路径 baseURL */
212
+ }
213
+ }
214
+ if (!whBase) whBase = trim(def.maintainURL);
215
+ }
216
+ return whBase ? trim(whBase) + tail : '';
217
+ }
218
+ ```
219
+
220
+ ### 回调接入示例
221
+
222
+ ```js
223
+ const configEnv = require('./src/configs/env/xxx/yyy.js');
224
+ const SERIES = require('./src/configs/series/...');
225
+
226
+ vitePluginSwOffline({
227
+ networkProbeUrl: (ctx) => buildSwNetworkProbeUrl(configEnv.default, SERIES.name)
228
+ // networkProbeUrl: 'https://api.example.com/wh/maintain/checkMaintain?productCode=demo',
229
+ });
230
+ ```
231
+
232
+ ## 开发 vs 生产
233
+
234
+ | 行为 | `vite dev` | `vite build` |
235
+ |------|------------|--------------|
236
+ | `/sw.js`、`/offline.html`、`/sw-register.js`、`/sw-noop.js` | 中间件直出(已注入) | 写入产出目录根目录 |
237
+ | `/static/offline-bg.jpg` | 读包内资源直出 | 复制到 `产出目录/static/` |
238
+ | `index.html` 中 `__SW_REGISTER_VERSION__` | `transformIndexHtml` 替换 | 同上 + `closeBundle` 再写一次 `index.html` |
239
+ | `public/` 其余文件 | Vite 默认 | `closeBundle` 复制(排除插件接管的 SW 文件名) |
240
+ | 终端日志 | 启动时 `swVersion (auto)`、dev 中间件说明等 | `sw.js -> dist/...`、`copy-public target` 等 |
241
+
242
+ **说明**:业务代码里的 `console.log` 出现在**浏览器控制台**;插件 `console.log` 出现在**运行 Vite 的终端**。
243
+
244
+ ## public 目录约定
245
+
246
+ 构建时复制 `public/` → 产出目录,但以下文件**由插件生成**,放在 `public/` 也不会覆盖:
247
+
248
+ - `sw.js`
249
+ - `sw-register.js`
250
+ - `offline.html`
251
+ - `sw-noop.js`
252
+
253
+ 若配置 `robotsTxtContent`,构建复制 `robots.txt` 时用该内容覆盖。
254
+
255
+ ## 配置项
256
+
257
+ 除特别说明外均可选。
258
+
259
+ | 选项 | 类型 | 说明 |
260
+ |------|------|------|
261
+ | `outDir` | `string` | 未传 CLI `--outDir` 时的产出目录,默认 `dist`。仅解析路径,不写入 SW。 |
262
+ | `fallbackOutDir` | `string` | 与 `outDir` 同义备选,内部会剔除。 |
263
+ | `swVersion` | `string` | 见 [版本号](#版本号swversion)。未传则自动生成。 |
264
+ | `networkProbeUrl` | `string \| (ctx) => string` | 见 [维护接口探测](#维护接口探测networkprobeurl)。 |
265
+ | `offlineLogoPath` | `string` | 离线页 `__OFFLINE_LOGO__` 与 SW 预缓存 Logo;支持 `https://`、根路径 `/`、`?v=`。 |
266
+ | `offlineDomain` | `string` | 离线页打字机文案 `__OFFLINE_DOMAIN__`。 |
267
+ | `offlineTemplatePath` | `string` | 自定义离线页 HTML(绝对路径或相对 `process.cwd()`)。不存在则回退默认模板。 |
268
+ | `cacheableApiPaths` | `string[]` | API 路径白名单(pathname 包含即走 SWR)。不传用内置示例列表。 |
269
+ | `robotsTxtContent` | `string` | 非空时覆盖产出目录 `robots.txt`。 |
270
+ | `serviceWorker` | `object` | SW 数值参数,见下表。 |
271
+
272
+ ### `serviceWorker` 子项
273
+
274
+ 非法或小于最小值时回退默认。
275
+
276
+ | 字段 | 默认 | 最小 | 含义 |
277
+ |------|------|------|------|
278
+ | `apiTimeout` | `30000` | `1000` | 可缓存 API 的 fetch 超时(ms) |
279
+ | `imageCacheMaxAge` | 30 天 | `1` | 预留,当前 SW 可能未读取 |
280
+ | `imageCacheMaxItems` | `500` | `1` | 图片缓存桶上限 |
281
+ | `staticCacheMaxItems` | `200` | `1` | 静态缓存桶上限 |
282
+ | `networkProbeTimeout` | `15000` | `1000` | 导航前网络探测超时(ms) |
283
+
284
+ 预览合并结果:
285
+
286
+ ```js
287
+ const { getDefaultServiceWorker, resolveServiceWorker } = require('vite-plugin-sw-offline');
288
+ resolveServiceWorker({ serviceWorker: { apiTimeout: 5000 } });
289
+ ```
290
+
291
+ ## 构建时注入占位符
292
+
293
+ | 文件 | 占位符 | 来源 |
294
+ |------|--------|------|
295
+ | `sw.js` | `__CACHEABLE_API_PATHS__` | `cacheableApiPaths` |
296
+ | `sw.js` | `__NETWORK_PROBE_URL__` | `networkProbeUrl` |
297
+ | `sw.js` | `__SW_VERSION__` | `swVersion`(自动或手动) |
298
+ | `sw.js` | `__OFFLINE_LOGO_PATH__` | `offlineLogoPath`(规范化) |
299
+ | `sw.js` | `__SW_RT_*__` | `serviceWorker` 各字段 |
300
+ | `sw-register.js` | `__SW_VERSION__` | `swVersion` |
301
+ | `offline.html` | `__OFFLINE_LOGO__` | `offlineLogoPath` |
302
+ | `offline.html` | `__OFFLINE_DOMAIN__` | `offlineDomain` |
303
+ | `index.html` | `__SW_REGISTER_VERSION__` | `swVersion` |
304
+
305
+ ## SW 缓存策略简述
306
+
307
+ 详见 `runtime/sw.js` 顶部注释,概要:
308
+
309
+ 1. 带 hash 的 JS/CSS/字体 → Cache First
310
+ 2. 图片 → Stale-While-Revalidate(条数上限可配)
311
+ 3. 白名单 API → Stale-While-Revalidate
312
+ 4. 无 hash 同源静态 → Network First
313
+ 5. 导航 → 先外网探测,失败则 `offline.html`
314
+
315
+ ## 页面侧 API(window.swCache)
316
+
317
+ 由 `sw-register.js` 挂载(不支持 SW 的环境为空函数,避免报错):
318
+
319
+ | 方法 | 说明 |
320
+ |------|------|
321
+ | `clearApiCache()` | 通知 SW 清除 API 缓存(如登录/登出) |
322
+ | `clearImageCache()` | 清除图片缓存 |
323
+ | `clearAllCache()` | 清除所有 Cache Storage |
324
+ | `update()` | `registration.update()` 检查 SW 更新 |
325
+
326
+ 应用版本更新仍由业务自身机制(如 `config.json`)负责;本插件只负责 SW 脚本与缓存策略。
327
+
328
+ ## 程序化导出
329
+
330
+ ```js
331
+ const {
332
+ vitePluginSwOffline,
333
+ getRuntimeDir,
334
+ getDefaultOfflineTemplatePath,
335
+ getDefaultOfflineBackgroundPath,
336
+ getDefaultCacheableApiPaths,
337
+ getDefaultServiceWorker,
338
+ resolveServiceWorker,
339
+ normalizeOfflineLogoPath,
340
+ injectOfflineHtml
341
+ } = require('vite-plugin-sw-offline');
342
+ ```
343
+
344
+ | 导出 | 用途 |
345
+ |------|------|
346
+ | `vitePluginSwOffline` | 插件工厂 |
347
+ | `getRuntimeDir()` | 包内 `runtime/` 绝对路径 |
348
+ | `getDefaultOfflineTemplatePath()` | 默认离线页模板路径 |
349
+ | `getDefaultOfflineBackgroundPath()` | 默认背景图路径 |
350
+ | `getDefaultCacheableApiPaths()` | 内置 API 白名单副本 |
351
+ | `getDefaultServiceWorker()` | `serviceWorker` 默认值 |
352
+ | `resolveServiceWorker(partial)` | 合并默认值 |
353
+ | `normalizeOfflineLogoPath(raw)` | Logo 路径规范化 |
354
+ | `injectOfflineHtml(html, options)` | 仅注入离线页占位符 |
355
+
356
+ ## 自定义离线页
357
+
358
+ 通过 `offlineTemplatePath` 指定 HTML,占位符与默认模板相同:
359
+
360
+ - `__OFFLINE_LOGO__`
361
+ - `__OFFLINE_DOMAIN__`
362
+
363
+ 背景图路径建议仍使用 `/static/offline-bg.jpg`(与 SW 预缓存一致),或自行改模板并同步调整 SW 预缓存逻辑。
364
+
365
+ ## 包内文件结构
366
+
367
+ ```
368
+ vite-plugin-sw-offline/
369
+ ├── src/index.js # 插件入口
370
+ ├── runtime/
371
+ │ ├── sw.js # Service Worker 主逻辑
372
+ │ ├── sw-register.js # 注册与 window.swCache
373
+ │ └── sw-noop.js # 空 SW(卸载用)
374
+ ├── templates/default/
375
+ │ └── offline.html # 默认离线页
376
+ ├── assets/
377
+ │ └── offline-bg.jpg # 默认背景图
378
+ └── README.md
379
+ ```
380
+
381
+ ## 发布到 npm
382
+
383
+ ### 1. 检查 `package.json`
384
+
385
+ - `name` 在 npm 上唯一(可用 `@scope/vite-plugin-sw-offline`)
386
+ - 删除 `"private": true`
387
+ - `files` 含 `src`、`runtime`、`templates`、`assets`、`README.md`
388
+ - 建议补 `repository`、`bugs`、`homepage`
389
+
390
+ ### 2. 登录与预演
391
+
392
+ ```bash
393
+ npm login
394
+ cd packages/vite-plugin-sw-offline
395
+ npm pack --dry-run
396
+ ```
397
+
398
+ ### 3. 发布
399
+
400
+ ```bash
401
+ npm publish
402
+ # 作用域包首次公开:npm publish --access public
403
+ ```
404
+
405
+ ### 4. 升级版本
406
+
407
+ ```bash
408
+ npm version patch
409
+ npm publish
410
+ ```
411
+
412
+ ---
413
+
414
+ 如有问题或需求(ESM 入口、`exports` 字段等),欢迎提 Issue 或 PR。
Binary file
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "vite-plugin-sw-offline",
3
+ "version": "1.0.0",
4
+ "description": "Vite 插件:Service Worker、离线页模板与构建注入(适用于 Vite H5 / uni-app H5 等)",
5
+ "main": "src/index.js",
6
+ "files": [
7
+ "src",
8
+ "runtime",
9
+ "templates",
10
+ "assets",
11
+ "README.md"
12
+ ],
13
+ "keywords": [
14
+ "vite-plugin",
15
+ "service-worker",
16
+ "offline",
17
+ "h5",
18
+ "uni-app",
19
+ "pwa"
20
+ ],
21
+ "license": "MIT",
22
+ "peerDependencies": {
23
+ "vite": "^5.0.0 || ^6.0.0"
24
+ },
25
+ "engines": {
26
+ "node": ">=16.0.0"
27
+ }
28
+ }
@@ -0,0 +1,41 @@
1
+ // ============================================
2
+ // No-Op Service Worker(空操作 SW)
3
+ // 用途:当需要移除 SW 时,部署此文件内容替换原 sw.js
4
+ // 效果:清理所有缓存,注销 SW,刷新所有页面
5
+ // ============================================
6
+
7
+ self.addEventListener('install', () => {
8
+ console.log('[SW-NOOP] Installing no-op service worker');
9
+ self.skipWaiting();
10
+ });
11
+
12
+ self.addEventListener('activate', (event) => {
13
+ console.log('[SW-NOOP] Activating and cleaning up');
14
+ event.waitUntil(
15
+ Promise.all([
16
+ // 1. 清理所有缓存
17
+ caches.keys().then((cacheNames) => {
18
+ return Promise.all(
19
+ cacheNames.map((name) => {
20
+ console.log('[SW-NOOP] Deleting cache:', name);
21
+ return caches.delete(name);
22
+ })
23
+ );
24
+ }),
25
+ // 2. 注销自己
26
+ self.registration.unregister().then(() => {
27
+ console.log('[SW-NOOP] Unregistered');
28
+ })
29
+ ]).then(() => {
30
+ // 3. 刷新所有打开的页面
31
+ return self.clients.matchAll({ type: 'window' });
32
+ }).then((clients) => {
33
+ clients.forEach((client) => {
34
+ client.navigate(client.url);
35
+ });
36
+ })
37
+ );
38
+ });
39
+
40
+ // 不拦截任何请求,让它们直接穿透到浏览器
41
+ // 注意:这里故意不添加 fetch 事件监听器
@@ -0,0 +1,103 @@
1
+ // ============================================
2
+ // Service Worker 注册和缓存管理
3
+ // 从 index.html 提取,便于独立维护和调试
4
+ // ============================================
5
+
6
+ (function() {
7
+ // ============================================
8
+ // 缓存管理工具 - 无论 SW 是否支持都挂载到 window 上
9
+ // 避免业务代码调用 window.swCache.xxx 时报错
10
+ // (iOS WKWebView 等环境可能不支持 SW)
11
+ // ============================================
12
+ if (!('serviceWorker' in navigator)) {
13
+ console.log('[App] Service Worker not supported');
14
+ window.swCache = {
15
+ clearApiCache: function() {},
16
+ clearImageCache: function() {},
17
+ clearAllCache: function() {},
18
+ update: function() {}
19
+ };
20
+ return;
21
+ }
22
+
23
+ window.swCache = {
24
+ /** 清除 API 缓存(用户登录/登出时调用) */
25
+ clearApiCache: function() {
26
+ if (navigator.serviceWorker.controller) {
27
+ navigator.serviceWorker.controller.postMessage('clearApiCache');
28
+ console.log('[App] Requested to clear API cache');
29
+ }
30
+ },
31
+ /** 清除图片缓存 */
32
+ clearImageCache: function() {
33
+ if (navigator.serviceWorker.controller) {
34
+ navigator.serviceWorker.controller.postMessage('clearImageCache');
35
+ console.log('[App] Requested to clear image cache');
36
+ }
37
+ },
38
+ /** 清除所有缓存 */
39
+ clearAllCache: function() {
40
+ if ('caches' in window) {
41
+ caches.keys().then(function(names) {
42
+ names.forEach(function(name) {
43
+ caches.delete(name);
44
+ });
45
+ console.log('[App] All caches cleared');
46
+ });
47
+ }
48
+ },
49
+ /** 强制触发 SW 更新检查 */
50
+ update: function() {
51
+ navigator.serviceWorker.getRegistration().then(function(reg) {
52
+ if (reg) {
53
+ reg.update();
54
+ console.log('[App] SW update triggered');
55
+ }
56
+ });
57
+ }
58
+ };
59
+
60
+ // ============================================
61
+ // SW 注册与更新管理
62
+ // SW 只负责缓存策略,不触发 reload
63
+ // 版本更新由 App.vue 的 config.json 机制负责
64
+ // ============================================
65
+
66
+ // SW 版本号(构建时由 vite-plugin-sw-offline 注入)
67
+ // 作用:每次部署生成新 URL,强制 Telegram WebView 等环境更新 SW
68
+ // 占位符未被替换时(开发模式),降级为无版本号
69
+ var SW_VERSION = '__SW_VERSION__';
70
+ var swUrl = SW_VERSION && !SW_VERSION.startsWith('__')
71
+ ? '/sw.js?v=' + encodeURIComponent(SW_VERSION)
72
+ : '/sw.js';
73
+
74
+ console.log('[App] SW_VERSION:', SW_VERSION, '| swUrl:', swUrl);
75
+
76
+ window.addEventListener('load', function() {
77
+ navigator.serviceWorker.register(swUrl, { updateViaCache: 'none' })
78
+ .then(function(registration) {
79
+ console.log('[App] SW registered, scope:', registration.scope);
80
+
81
+ registration.addEventListener('updatefound', function() {
82
+ var newWorker = registration.installing;
83
+ console.log('[App] New SW found, state:', newWorker.state);
84
+
85
+ newWorker.addEventListener('statechange', function() {
86
+ console.log('[App] SW state changed:', newWorker.state);
87
+ });
88
+ });
89
+
90
+ // 定期检查更新(每 5 分钟)
91
+ setInterval(function() {
92
+ registration.update();
93
+ }, 5 * 60 * 1000);
94
+ })
95
+ .catch(function(error) {
96
+ console.error('[App] SW registration failed:', error);
97
+ });
98
+
99
+ navigator.serviceWorker.addEventListener('controllerchange', function() {
100
+ console.log('[App] SW controller changed');
101
+ });
102
+ });
103
+ })();