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>

关键原则

  1. SW 安装后立即 skipWaiting(),不等用户手动点 UpdateBanner
  2. index.html 加 <meta http-equiv="cache-control" content="no-cache">
  3. BUILD_HASH 在 vite.config 中只生成一次(单一模块级变量),Profile 页、SW、缓存清理脚本三处共用

BUILD_HASH 统一的坑

之前有两个 hash 生成点defineConfigwriteBundle 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 下):

  1. safe-area-inset-bottom ≈ 34px(home indicator)
  2. 输入框 pill ≈ 48px
  3. SuggestionBar ≈ 36px
  4. 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 底部间距
  • 用户可手动触发页面刷新加载新版本

相关主题