diff --git a/docs/README.md b/docs/README.md index 3bb3fd1..b7a3ad6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,7 @@ The project README introduces reader. Use the guides below for installation, eve | --- | --- | --- | | 安装、首次登录、添加书籍、更新 / Installation and first steps | [入门指南](getting-started.zh-CN.md) | [Getting started](getting-started.en.md) | | 书架、阅读、朗读、来源和离线 / Shelves, reading, TTS, sources, offline | [使用手册](user-guide.zh-CN.md) | Chinese only | +| 安装到桌面、离线启动和版本更新 / PWA installation, offline startup and updates | [PWA 指南](pwa.zh-CN.md) | Chinese only | | 环境变量、目录权限、HTTPS、排错 / Settings, permissions, HTTPS, troubleshooting | [部署配置](configuration.zh-CN.md) | [Configuration](configuration.en.md) | | 本地开发、验证、镜像与 Android 构建 / Development, checks, Docker and Android builds | [开发指南](development.zh-CN.md) | [Development](development.en.md) | diff --git a/docs/pwa.zh-CN.md b/docs/pwa.zh-CN.md new file mode 100644 index 0000000..003367b --- /dev/null +++ b/docs/pwa.zh-CN.md @@ -0,0 +1,15 @@ +# PWA 安装与离线使用 + +生产构建(`cd web && npm run build`)自动生成应用清单、桌面图标和 Service Worker,随现有 Web 服务一起部署,无需额外服务。 + +- 使用 HTTPS 访问;本机 `localhost` / `127.0.0.1` 可使用 HTTP。通过局域网 IP 的普通 HTTP 访问仍能在线阅读,但浏览器不会启用 PWA 离线缓存。 +- Chrome / Edge:通过地址栏安装图标或浏览器菜单中的安装应用功能安装。 +- iPhone / iPad:在 Safari 中选择“分享 → 添加到主屏幕”。 +- 首次联网打开后,等待页面资源缓存完成,再离线使用。书籍正文需要在书架中提前下载;页面缓存不会自动下载整个书库。 +- 离线时保留原有登录状态即可打开已缓存书架和已下载书籍。首次登录、未下载内容和需要服务器的功能仍需联网。 +- 新版本会在访问时后台下载,关闭该站点的所有页面及独立应用窗口,再次打开后生效。更新不会强制刷新正在阅读的页面。 +- Service Worker 仅缓存应用静态资源,不缓存 API、令牌或书籍响应;书籍和阅读数据继续使用原有按账号隔离的存储。 + +开发服务器不注册 Service Worker;Android 原生 WebView 继续使用本地资源。清理站点数据会同时删除离线页面及本地阅读缓存。 + +验证:`cd web && npm run build && node tools/pwa/smoke.mjs`。测试需要 Playwright 的 Chromium,可通过 `npx playwright install chromium` 安装。 diff --git a/server/src/http/routes/web.ts b/server/src/http/routes/web.ts index 186e37a..4f8e27c 100644 --- a/server/src/http/routes/web.ts +++ b/server/src/http/routes/web.ts @@ -81,11 +81,9 @@ export function registerWebRoutes(app: FastifyInstance, ctx: AppContext): void { return; } if (existsSync(candidate) && statSync(candidate).isFile()) { - // Vite writes content-hashed file names, so a JS/CSS asset is immutable: - // its bytes cannot change without the name changing too. `index.html` is - // the one file whose name never changes, so it must never be cached or a - // deployment would never pick up a new bundle. - const immutable = extname(candidate) !== '.html'; + // The Android-compatible build uses fixed names (client.js, sw.js, etc.). + // Only explicitly fingerprinted assets can safely be cached forever. + const immutable = raw.startsWith('assets/') && /-[a-zA-Z0-9_-]{6,}\.[^.]+$/.test(raw); return sendFile( candidate, reply, diff --git a/server/test/web.test.ts b/server/test/web.test.ts index 06d251a..7d6d20d 100644 --- a/server/test/web.test.ts +++ b/server/test/web.test.ts @@ -23,6 +23,9 @@ before(async () => { await writeFile(join(root, 'index.html'), '