Frontend

iOS 15 打开论坛帖子页就报错,排查后给构建加了兼容性检查

1822 字约 7 分钟

摘要有同学用 iOS 15 的 Safari 和手机 QQ 打开论坛,帖子页一直显示页面出错。原因是代码里用了 iOS 16 才有的 toSorted。记录了怎样从访问日志定位、用 WebKit 复现,以及怎样让构建在发布前就拦住这类问题。

我做的论坛上线不久,有同学反馈说,用 iPhone 的 Safari 打开论坛会显示“页面出错,请稍后重试”,在手机 QQ 里直接点链接也一样。这类问题最麻烦的地方在于,我没法直接看到他手机上的报错。能用的线索只有一个,他说是在刚才 21:34 左右打开的。

先从访问日志里找到他

iOS 上的手机 QQ 内置浏览器用的也是系统自带的 WebKit,只要系统版本相同,行为就一样。这解释了为什么 Safari 和手机 QQ 的表现一致。

接着去查服务器的访问日志。21:33 到 21:34 这一分钟里,只有一个 iOS 15.4.1 的 Safari 在访问,时间对得上。把它的请求按顺序排出来是这样的。

那位同学的访问记录
  1. 21:33 打开首页,正常,头像和列表都加载了。
  2. 21:33 进入登录页并完成登录,正常。
  3. 21:34 点开一个帖子,3 秒后退回首页。
  4. 21:34 又点开另一个帖子。

首页和登录都没有问题,出问题的是帖子页。

一个 iOS 16 才有的方法

帖子页里,每个楼层渲染表情回应的时候,都会对表情做一次排序,用的是 Array.prototype.toSorted。它返回排好序的新数组,原数组不变,是 2023 年才进入标准的新方法,iOS 16 的 Safari 才开始支持。

iOS 15 上调用它,会直接抛出 TypeError。这个错误没有被局部处理,一路冒泡到页面最外层的错误边界,于是整页显示成“页面出错”。

toSorted 和 sort 有什么不同

sort() 会直接改动原数组,toSorted() 则返回一个排好序的副本。后者写起来更符合“不改原数据”的习惯,所以现在的写法大多推荐用它,同类的新方法还有 toReversed、toSpliced 和 with。

它们都是 ES2023 加入的,需要较新的浏览器才有。

用 WebKit 复现

光凭推断还不够,我想亲眼看到它出错。助手用 Playwright 自带的 WebKit 打开线上页面,在页面脚本最前面把 iOS 16 新增的四个方法 toSorted、toReversed、toSpliced、with 删掉,模拟 iOS 15。

结果首页正常,帖子页显示“页面出错”,和同学描述的一模一样。不删掉这几个方法,两个页面都正常。这就能确认原因了。

为什么会写出这样的代码

问题一旦明确,下一个问题就是它为什么会出现。翻了一遍代码,我发现三个原因叠在一起。

  • 项目的 TypeScript 配置里,语言库用的是 ESNext,所以写 toSorted 时类型检查不会报任何错。
  • Lint 工具 oxlint 里有两条 unicorn 规则,看到 .sort() 和 .reverse() 就要求改成 toSorted() 和 toReversed()。原来的 toSorted 其实是照着规则改出来的。
  • 我从来没有明确写过“论坛支持到哪个浏览器版本”,所以谁也不知道这条线该画在哪里。

另外,前端还有几处用了这些新方法,发帖框里编辑投票选项和奖品时用了 with 和 toSpliced,后台统计图用了 toReversed,上报阅读进度用了 toSorted。只改表情回应那一处,下一个人会在别的页面碰到同样的问题。

先看看有多少人受影响

要不要为旧系统花功夫,要看有多少人用。服务器上有访问记录,近 30 天里 80 个登录过的用户,只有这一位的系统低于 iOS 16,也没有人使用低于 110 版本的 Chrome。

看起来影响面很小,但这次的反馈就是真实的用户,而且改动本身不大。我决定修,并且要让它以后不再出现。

先定下最低支持的版本

我把最低支持的浏览器写进了 package.json 的 browserslist,也写进了设计文档。

浏览器最低版本理由
iOS Safari15.4群里真有人在用。这个版本起才有 <dialog> 和层叠层(cascade layers),再旧的话 Tailwind 的样式会整片失效
Chrome100照顾没有更新系统 WebView 的安卓手机 QQ,访客里见过 101、104 的版本

Firefox 则定在 128。这几个数字是我根据访客数据定的,想调整只需要改这一处,构建配置里的名单同步改。

让构建来拦住它

光有一份名单还不够,没人会每次提交都对照着看。我想要的是,只要有人写了名单之外的东西,构建就失败,不可能发到线上。

具体做了三件事。

  1. 在 Vite 的构建配置里,用同一份名单设置编译目标,让它把旧浏览器没有的语法改写成等价写法。它自己的默认值只到 iOS 16.4。打包后的脚本体积增加了约 0.2%。
  2. 在 pnpm build 里加一步 es-check,检查打包结果。只要用到名单里的浏览器没有的语法或内置方法,构建就失败。持续集成和镜像构建都会跑这一步。
  3. 关掉那两条要求改用 toSorted 和 toReversed 的 lint 规则,并在配置旁边写明原因,免得有人好心再改回去。

我试过把 toSorted 临时加回去,构建确实失败了,报错写明了出问题的文件,以及哪个版本的浏览器缺这个方法。

构建管不到的地方

es-check 能查语法和部分内置方法,但查不到所有东西。浏览器提供的各种 API,比如 requestSubmit、cookieStore,迭代器上的新方法,以及正则的后行断言,都不在它能判断的范围里。

所以助手又用 MDN 的兼容性数据,把打包结果里用到的浏览器 API 逐个过了一遍,额外找出了几处。确认后提交表单、下拉框选中后提交,用了 requestSubmit(),成员页保存列设置用了 cookieStore,表情选择器则要求 Chrome 100。不改的话,旧系统上删除草稿时点了确认也删不掉,后台筛选的下拉框也会失效。

这几处都改成了旧浏览器也支持的写法。剩下无法自动检查的部分,我把规则写进了开发约定里,靠写代码时留意,不可能百分之百。

验证

最后一步是回到最初的场景。在 WebKit 里同样删掉 iOS 15.4 没有的所有方法,包括 requestSubmit 和 cookieStore,再逐项操作,看帖和点赞、离开帖子时上报阅读进度、删除草稿、成员页的筛选和改等级、发帖框里的编辑、后台统计页,全部正常。线上发布之后,又用同样的方式模拟 iOS 15.4 打开首页和那位同学点过的帖子,也都正常。

适用边界

兼容性检查只是静态检查,它保证打包结果里没有名单之外的语法和内置方法,并不等于在真实的旧手机上跑过一遍。WebKit 里删方法也只是模拟,真机的表现我没有测。另外最低版本是按我自己论坛的访客分布定的,换一个访客不同的网站,这条线应该画在别处。

评论