PWA:可安装 + 离线可用(manifest/SW/图标)
- manifest.webmanifest:standalone、theme #312E81、start_url /?source=pwa、4 图标(any+maskable) - sw.js v2:预缓存页面骨架 11 项;导航/静态 network-first,离线回缓存(ignoreSearch 兜 ?v= 指纹);/api/* 与 /mcp/* 一律放行不缓存 - index.html:manifest link、theme-color、apple-touch-icon、iOS meta、onMounted 注册 SW(HTTP 裸访问自动降级) - icons/:Chromium 渲染 SVG 生成 192/512 any + maskable + apple-touch-icon(png/gen_icons.js 可重跑) - 验证:npm test 72/72;png/verify_pwa.js 11 项全 PASS(SW 注册/预缓存/离线启动/manifest 解析)
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
// PWA 验证:SW 注册、预缓存、离线启动、可安装性要素,一次跑完。
|
||||
// 跑法:node png/verify_pwa.js (真实 Chromium,验证后不清理产物,截图留证据)
|
||||
const puppeteer = require('/root/.openclaw/workspace/web_tasks/node_modules/puppeteer');
|
||||
|
||||
const URL_BASE = 'https://daka.kory.kdns.fr';
|
||||
const PROFILE = '/tmp/wht-pwa-verify';
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: '/usr/bin/chromium',
|
||||
headless: 'new',
|
||||
userDataDir: PROFILE,
|
||||
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 390, height: 844, isMobile: true, deviceScaleFactor: 2 });
|
||||
|
||||
const results = [];
|
||||
const ok = (name, cond, detail = '') => {
|
||||
results.push(`${cond ? '✅' : '❌'} ${name}${detail ? ' — ' + detail : ''}`);
|
||||
};
|
||||
|
||||
// ---- 1. 首次在线加载 + SW 注册 ----
|
||||
await page.goto(URL_BASE + '/', { waitUntil: 'networkidle2', timeout: 45000 });
|
||||
await new Promise(r => setTimeout(r, 3500)); // 给 SW install 留时间
|
||||
|
||||
const swState = await page.evaluate(async () => {
|
||||
if (!('serviceWorker' in navigator)) return { supported: false };
|
||||
const reg = await navigator.serviceWorker.ready;
|
||||
const keys = await caches.keys();
|
||||
let precacheCount = 0;
|
||||
for (const k of keys) {
|
||||
if (k.startsWith('wht-precache')) {
|
||||
const c = await caches.open(k);
|
||||
precacheCount = (await c.keys()).length;
|
||||
}
|
||||
}
|
||||
return {
|
||||
supported: true,
|
||||
script: reg.active ? reg.active.scriptURL : null,
|
||||
scope: reg.scope,
|
||||
controlled: !!navigator.serviceWorker.controller,
|
||||
cacheKeys: keys,
|
||||
precacheCount
|
||||
};
|
||||
});
|
||||
ok('SW 支持(安全上下文)', swState.supported === true);
|
||||
ok('SW 注册成功', !!swState.script, swState.script || '');
|
||||
ok('SW 作用域 = /', swState.scope === URL_BASE + '/', swState.scope || '');
|
||||
ok('页面已被 SW 控制', swState.controlled === true);
|
||||
ok('预缓存条目=11', swState.precacheCount === 11, '实际 ' + swState.precacheCount);
|
||||
|
||||
// ---- 2. manifest 可解析 ----
|
||||
const manifest = await page.evaluate(async () => {
|
||||
const res = await fetch('/manifest.webmanifest');
|
||||
const j = await res.json();
|
||||
return { ok: res.ok, name: j.name, icons: (j.icons || []).length, display: j.display };
|
||||
});
|
||||
ok('manifest 200 且可解析', manifest.ok === true && manifest.icons >= 4, JSON.stringify(manifest));
|
||||
|
||||
// ---- 3. 离线重启:骨架必须能起来 ----
|
||||
await page.setOfflineMode(true);
|
||||
await page.reload({ waitUntil: 'domcontentloaded', timeout: 20000 });
|
||||
await new Promise(r => setTimeout(r, 3500));
|
||||
|
||||
const offline = await page.evaluate(() => ({
|
||||
vue: typeof globalThis.Vue !== 'undefined',
|
||||
payroll: typeof globalThis.WHTPayroll !== 'undefined',
|
||||
bootHealVisible: (() => {
|
||||
const el = document.getElementById('boot-heal');
|
||||
return !!el && el.style.display !== 'none' && getComputedStyle(el).display !== 'none';
|
||||
})(),
|
||||
appText: (document.querySelector('#app') || document.body).innerText.slice(0, 60)
|
||||
}));
|
||||
ok('离线:Vue 可用', offline.vue === true);
|
||||
ok('离线:薪酬模块可用', offline.payroll === true);
|
||||
ok('离线:无 boot 恢复卡', offline.bootHealVisible === false);
|
||||
ok('离线:页面有实际内容', offline.appText.trim().length > 20, JSON.stringify(offline.appText.slice(0, 40)));
|
||||
|
||||
await page.screenshot({ path: '/root/.openclaw/workspace/work-hours-tracker/png/pwa_offline_boot.png' });
|
||||
|
||||
// ---- 4. 恢复在线,确认导航回源 ----
|
||||
await page.setOfflineMode(false);
|
||||
await page.reload({ waitUntil: 'networkidle2', timeout: 45000 });
|
||||
const backOnline = await page.evaluate(() => typeof globalThis.Vue !== 'undefined');
|
||||
ok('恢复在线:页面正常', backOnline === true);
|
||||
|
||||
await page.screenshot({ path: '/root/.openclaw/workspace/work-hours-tracker/png/pwa_online.png' });
|
||||
|
||||
await browser.close();
|
||||
console.log(results.join('\n'));
|
||||
const failed = results.filter(r => r.startsWith('❌')).length;
|
||||
console.log(failed === 0 ? 'PWA-VERIFY-ALL-PASS' : `PWA-VERIFY-FAILED x${failed}`);
|
||||
process.exit(failed === 0 ? 0 : 1);
|
||||
})().catch(e => { console.error('FAILED:', e.message); process.exit(1); });
|
||||
@@ -15,7 +15,7 @@
|
||||
* vendor ?v= 指纹变化会触发重下,PRECACHE 键值同步更新即可。旧缓存条目
|
||||
* activate 时统一清理,不做遗留兜底。
|
||||
*/
|
||||
const VERSION = 'v1';
|
||||
const VERSION = 'v2';
|
||||
const PRECACHE = `wht-precache-${VERSION}`;
|
||||
const RUNTIME = `wht-runtime-${VERSION}`;
|
||||
|
||||
@@ -66,7 +66,9 @@ async function networkFirst(request, cacheName) {
|
||||
if (res && res.ok) cache.put(request, res.clone());
|
||||
return res;
|
||||
} catch (e) {
|
||||
const cached = await cache.match(request, { ignoreSearch: request.mode === 'navigate' });
|
||||
// ignoreSearch:离线时 vendor 实际请求带 ?v= 指纹,预缓存里是不带指纹的裸路径;
|
||||
// 不吞掉查询串就永远匹配不上,冷离线直接白屏。指纹内容不一致的风险可接受(离线本来就拿不到新版)。
|
||||
const cached = await cache.match(request, { ignoreSearch: true });
|
||||
if (cached) return cached;
|
||||
// 导航请求最终兜底:预缓存的 index.html(ignoreSearch 吞掉 ?source=pwa 等查询)
|
||||
const pre = await caches.open(PRECACHE);
|
||||
|
||||
Reference in New Issue
Block a user