WebKit 新特性
What's new in WebKit
2025年6月11日
一句话判断
如果你的 App 里有 WKWebView,今年最该关注的不是某个 CSS 特性,而是 Web Push 和 callAsyncJavaScript 的增强——这两项直接决定了混合架构能走多远。
这场 Session 讲了什么
WebKit 今年干了两件事:把 Web 开发者喊了好几年的特性补齐了,然后把 Web 和原生之间的墙又凿掉一块。CSS Anchor Positioning、Temporal API、Observable 这些属于前者,WKWebView Web Push 和增强的 JS-Native 通信属于后者。
对纯前端开发者来说,好消息是 Popper.js 和 date-fns 这类库可以开始退场了。浏览器原生方案在性能和维护成本上都有优势,尤其是 Anchor Positioning 的自动翻转能力,以前要写几百行 JS 才能搞定的事情,现在四行 CSS 就够了。
对 iOS 开发者来说,真正有意思的变化是 WKWebView 这侧。Web Push 让 WebView 里的 Web 应用终于能收到系统推送了——这对电商、内容类 App 的 H5 模块是实质性利好。callAsyncJavaScript 的参数序列化能力增强,则让 JS-Native 数据交换不再只能传字符串。
值得深挖的点
CSS Anchor Positioning:Floating UI 们的丧钟?
在 Web 上实现”元素 A 相对于元素 B 定位”一直是件脏活。弹出菜单、工具提示、下拉选择器——所有这些 UI 模式都需要你做同一件事:计算目标元素的 getBoundingClientRect,处理边界溢出,监听 scroll 和 resize 事件重新计算。Floating UI 和 Popper.js 之所以流行,就是因为原生没有好方案。
Anchor Positioning 改变了这个局面。你给触发元素声明 anchor-name: --trigger,给弹出元素声明 position-anchor: --trigger,然后用 anchor() 函数引用锚点的边缘坐标。浏览器接管了所有的计算、更新和重绘。但真正的杀手级特性是 position-try-fallbacks:当下方空间不足时自动翻转到上方,右侧溢出时自动移到左侧。这恰好是 Floating UI 最核心的能力。
不过别急着删 node_modules。Anchor Positioning 在 Safari 和 Chrome 的实现版本不同步,旧浏览器完全没有支持。务实的做法是:新项目直接用原生方案,加一个 @supports 做降级;老项目维持现有库,等浏览器覆盖率到位再迁移。另外需要注意,Anchor Positioning 和 Shadow DOM 的交互还有一些边界情况没完全解决,组件化场景下要多测试。
Observable:Promise 不够用的那些场景终于有救了
Promise 处理的是”一个未来的值”,但 Web 开发中大量场景是”一连串未来的值”:用户输入的搜索关键词流、WebSocket 的消息序列、IntersectionObserver 的回调链。过去你有两个选择:addEventListener 加手动防抖/节流,或者引入 RxJS。前者代码容易乱,后者依赖太重。
Observable 是 TC39 Stage 1 提案,提供了标准化的流处理原语。它支持 map、filter、take、switchMap 这些操作符,而且原生集成了 AbortController——你可以通过 signal 取消订阅,不用担心内存泄漏。对比 RxJS,Observable 胜在零依赖和浏览器原生支持;对比 AsyncIterator,Observable 是推模型(push-based),更适合事件驱动的场景。
一个实际的差异:用 Observable 做搜索框防抖,代码量大概是 addEventListener 版本的三分之一,而且逻辑是声明式的、可组合的。但要提醒的是,这个提案还在 Stage 1,API 随时可能变。现阶段建议在新项目里小范围试用,别全面铺开。
代码片段
/* CSS Anchor Positioning:四行搞定弹出菜单定位 */
.menu-trigger { anchor-name: --trigger; }
.dropdown {
position: fixed;
position-anchor: --trigger;
top: anchor(bottom);
left: anchor(center);
translate: -50% 0;
position-try-fallbacks: flip-block; /* 空间不足时自动翻转 */
}
坑:anchor-name 用的是 CSS 自定义属性的命名规范(—前缀),别写成 class 选择器。
// callAsyncJavaScript 传复杂参数
let result = try await webView.callAsyncJavaScript(
"""
const data = await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify({ userId: $userId, filters: $filters })
});
return await data.json();
""",
arguments: ["userId": "12345", "filters": ["status": "active"]],
contentWorld: .page
)
坑:arguments 字典的值只能是 String、Number、Array、Dictionary 这些基础类型,传闭包或 DOM 节点会静默失败。
// Web Push 权限代理
func webView(
_ webView: WKWebView,
requestNotificationPermissionFor origin: WKSecurityOrigin,
decisionHandler: @escaping (WKNotificationPermission) -> Void
) {
// 别在页面加载时就弹权限——等用户点了"订阅通知"按钮再调
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
decisionHandler(granted ? .grant : .deny)
}
}
坑:系统通知权限被拒绝后,Web Push 的 permission 状态会缓存,用户去设置里手动开启后需要重新加载页面才能生效。
最佳实践
先把项目里的日期处理代码列一遍——凡是有 new Date() 和手动时区计算的地方,新模块直接用 Temporal API,老模块不动。Temporal 的不可变设计从根本上消除了 Date 对象那堆坑,但别指望 polyfill 能做到零成本,Safari 的实现效率比 polyfill 好一个量级。
如果项目用了 Floating UI 或 Popper.js,建议做一个 spike:用 Anchor Positioning 重写一个最复杂的弹出场景,感受一下浏览器原生方案的边界在哪里。如果能覆盖 80% 的场景,就规划迁移;如果连基本的嵌套弹出都搞不定,就继续等。
Web Push 那边,如果 App 在 WKWebView 里跑着电商或内容模块,这周就该让前端同事评估 Push API 的接入成本了。这不是锦上添花,是用户留存的基础设施。
还有什么值得关注
- Temporal API 进了 Safari:终于可以跟
Date的可变性和时区地狱说再见了,新项目优先用它。 - @scope 规则:CSS 的样式隔离终于有了不依赖 Shadow DOM 的方案,组件化开发会舒服很多。
- color-mix() 函数:设计师给的色值过渡终于能用一行 CSS 搞定,不用再写 JS 算中间色了。