PWA 与 iOS 部署
PWA(Progressive Web App)在 iOS Safari 上的缓存管理、Service Worker 更新策略和 safe-area 布局适配经验。
概述
clawline-client-web 作为 PWA 应用部署在 iOS Safari 和 standalone 模式下,遇到了一系列与缓存更新、布局适配相关的坑。本页记录这些经验教训和最终解决方案,供未来 PWA 项目参考。
iOS Service Worker 缓存问题
症状
- 部署新版本后,iOS 用户始终看到旧版页面
UpdateBanner在 iOS 上经常不显示- 下拉刷新不足以触发更新
根因分析
| 因素 | 说明 |
|---|---|
| iOS SW 更新延迟 | iOS Safari 检测 SW 更新有延迟,可能等 24 小时 |
| 旧 SW 无 skipWaiting | 旧版 SW 不会主动检查更新,新 SW 的 skipWaiting 无法影响旧 SW |
| cache-first 策略 | 旧 SW 使用 cache-first,拿到的永远是旧资源 |
| PWA standalone 隔离 | PWA 和 Safari 浏览器有独立的 SW 缓存 |
解决方案
在 index.html 中内联缓存清理脚本:
<script>
// 页面加载时检查版本,版本不匹配则清除所有 SW 缓存
if ('serviceWorker' in navigator) {
const BUILD_HASH = '';
// 对比 localStorage 存储的旧版本
// 不一致则 unregister + caches.delete
}
</script>关键原则:
- SW 安装后立即
skipWaiting(),不等用户手动点 UpdateBanner - index.html 加
<meta http-equiv="cache-control" content="no-cache"> - BUILD_HASH 在 vite.config 中只生成一次(单一模块级变量),Profile 页、SW、缓存清理脚本三处共用
BUILD_HASH 统一的坑
之前有两个 hash 生成点(defineConfig 和 writeBundle plugin),各自用 Date.now() 算 MD5,导致 Profile 页显示的 hash 与 SW 使用的 hash 不一致。
修复:统一成一个模块级 const BUILD_HASH,所有地方引用同一个值。
iOS safe-area 布局适配
PWA vs 浏览器的差异
| 环境 | safe-area-inset-bottom | 原因 |
|---|---|---|
| Safari 浏览器 | 很小或 0 | 底部有浏览器工具栏 |
| PWA standalone | ~34px | 无工具栏,需要避开 home indicator |
输入框底部间距计算
底部区域组成(PWA standalone 下):
safe-area-inset-bottom≈ 34px(home indicator)- 输入框 pill ≈ 48px
- SuggestionBar ≈ 36px
- padding ≈ 4px
总计 ~122px — 在 iPhone 上占据大量空间。
适配方案
- 使用完整
env(safe-area-inset-bottom)而非缩小值(PWA 需要完整间距避开 home indicator) - 缩小 SuggestionBar pills 尺寸(
py-1代替py-1.5) - 输入框按钮
w-8 h-8代替w-10 h-10 touch-action: none在可拖拽元素上防止 iOS 橡皮筋效果
iOS 特有交互问题
ActionSheet 被输入栏遮挡
根因:ActionSheet 渲染在 overflow-y-auto 的 scroll 容器内。iOS Safari 对 overflow:auto 容器内的 position: fixed 有 z-index bug。
修复:把 ActionSheet 移到最外层容器,和 Input Area 同级。
文字选择被 ActionSheet 劫持
根因:整个消息容器绑了 onTouchStart/onTouchEnd,长按 400ms 触发 ActionSheet,抢占了系统文字选中。
修复:消息文本区域 stopPropagation 阻止 touch 事件冒泡。长按文字 → 系统选中复制;长按 avatar/header → ActionSheet。
iOS 键盘收起留白
键盘收起后页面底部留下空白区域,需要在 resize 事件或 focusout 后强制触发 window.scrollTo(0, 0) 或重新计算布局高度。
Caddy 缓存配置
Caddy 默认不设 Cache-Control header,浏览器可能做 aggressive cache。对 HTML 文件需要显式设置:
header /index.html Cache-Control "no-cache, no-store, must-revalidate"
JS/CSS 文件因为 Vite hash 命名,可以用长期缓存 max-age=31536000, immutable。
UpdateBanner 设计
最终方案:
- SW 自动
skipWaiting()激活新版本 - UpdateBanner 从底部向上弹出(z-[70]),带 safe-area 底部间距
- 用户可手动触发页面刷新加载新版本