Highlight
这是 Safari 年度更新汇总,覆盖过去七个版本(Safari 15 到 16)的所有新功能。Session 按类别组织:HTML、CSS、Web Inspector、Web API、JavaScript/WebAssembly、安全与隐私。
核心内容
过去一年,Safari 和 WebKit 连续发布了七个版本。WebKit 团队在开场说,这一年累计带来了 162 项 Web 平台特性和改进(01:18)。这些更新集中回答了开发者长期反馈的几个问题:CSS 组件复用难、父级状态选择难、调试布局不直观、Web App 与原生 App 的能力差距仍然存在。
这场 Session 用一个服装交换网站串起全部内容。页面里有请求物品的弹窗、收到请求的轮播、不同宽度下复用的商品卡片、跨窗口同步的库存状态,以及需要离线保存的草稿。每个功能都对应 Safari 15 到 16 中的一组新能力。
HTML:少写自定义弹窗和交互状态
以前做一个弹窗,通常要自己处理焦点、背景遮罩、键盘导航和无障碍状态。Safari 现在支持 <dialog> 和 ::backdrop,让弹窗和遮罩回到浏览器原生能力里(02:58)。如果页面上有轮播或隐藏面板,inert 属性还能把非当前内容从点击、键盘导航和辅助技术访问中排除(03:49)。
图片加载也有直接的 HTML 写法。对首屏外的商品图片,加上 loading="lazy",浏览器会在用户滚动到图片附近时再加载它们(04:20)。这类能力适合先替换站点里最常见的自定义代码。
CSS:组件根据容器工作
CSS 部分的重点是架构。WebKit 团队明确说,开发者对新 Web 技术的首要请求是 Container Queries,并宣布它会随 Safari 16 发布(05:10)。商品卡片可以在侧边栏里竖排,在主区域里横排,在中等宽度下变成两列。判断依据来自组件所在容器,不再只能来自整个视口。
@layer 解决另一类长期问题:大型项目里,设计系统、工具类、业务覆盖样式混在一起,选择器优先级会越来越难控。Cascade Layers 让开发者自己定义层级,整个层级的顺序高于单个选择器的 specificity(07:14)。
:has() 则让父元素能根据子元素状态应用样式(08:30)。表单里的“Urgent?” 复选框被选中时,整个消息表单可以直接变色,不需要额外 JavaScript 维护状态。
Web API 与 JavaScript:Web App 更像完整应用
Web API 部分从 Web Push 开始。Safari 16 在 macOS Ventura 支持 Web Push,iOS 和 iPadOS 支持计划在下一年到来(20:22)。它是基于标准的实现,已有 Web Push 代码如果能在其他浏览器工作,在 Safari 中也应无需修改(20:41)。
同一来源的多个页面也有了更直接的协作方式。Broadcast Channel 可以把一个窗口里的“商品已被领取”消息发送到其他同源窗口(22:11)。Origin private file system 则给站点提供按 origin 隔离的私有文件存储,用来保存草稿这类数据(22:49)。
JavaScript 更新里,Shared Worker 允许同源的多个页面共享一个 worker,减少每个窗口重复启动后台任务带来的内存压力(25:08)。数组也新增了 findLast()、findLastIndex() 和 at(),让从尾部查找和负索引访问更直接(25:56)。
详细内容
原生弹窗:<dialog> 与 ::backdrop
(02:58)服装交换网站需要一个“Request Item”弹窗。<dialog> 提供原生 overlay 容器,适合承载请求表单。
<!-- <dialog> element -->
<dialog method="dialog">
<form id="dialogForm">
<label for="givenName">Given name:</label>
<input class="focus" type="text" name="givenName">
<label for="familyName">Family name:</label>
<input class="focus" type="text" name="familyName">
<label>
<input type="checkbox"> Can trade in person
</label>
<button>Send</button>
</form>
</dialog>
关键点:
<dialog method="dialog">声明一个原生对话框容器。<form id="dialogForm">把提交内容放在对话框内部。- 两组
<label>和<input>收集请求人的姓名。 - checkbox 表达“是否能当面交易”这个请求条件。
button负责提交表单。
(03:08)弹窗背后的遮罩不需要额外 DOM。::backdrop 直接选中 dialog 的背景层。
/* ::backdrop pseudo-element */
dialog::backdrop {
background: linear-gradient(rgba(233, 182, 76, 0.7), rgba(103, 12, 0, 0.6));
animation: fade-in 0.5s;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
关键点:
dialog::backdrop选中 modal 背后的浏览器生成背景层。background给背景层加渐变颜色。animation为背景层绑定淡入动画。@keyframes fade-in定义从透明到不透明的变化。
交互禁用:inert
(03:49)轮播里只有当前卡片应该可交互。inert 会禁用元素及其子元素的点击、键盘导航和辅助技术访问。
// inert attribute
function switchToIndex(index) {
this.items.forEach(item => item.inert = true);
this.items[index].inert = false;
this.currentIndex = index;
}
关键点:
switchToIndex(index)接收目标轮播项索引。this.items.forEach(...)先把所有轮播项设为 inert。this.items[index].inert = false只恢复当前项的交互能力。this.currentIndex = index保存当前索引,供后续切换使用。
图片懒加载:loading="lazy"
(04:20)商品列表通常有大量图片。首屏之外的图片可以延迟加载。
<img src="images/shirt.jpg" loading="lazy"
alt="a brown polo shirt"
width="500" height="600">
关键点:
src指向实际图片资源。loading="lazy"请求浏览器延迟加载这张图片。alt提供图片替代文本。width和height帮助浏览器预留布局空间。
容器查询:组件按自身空间切换布局
(06:35)同一个商品卡片会出现在侧边栏、主内容区和网格里。用 Container Queries 后,布局判断写在组件内部。
/* Container queries */
.container {
container-type: inline-size;
container-name: clothing-card;
}
.content {
display: grid;
grid-template-rows: 1fr;
gap: 1rem;
}
@container clothing-card (width > 250px) {
.content {
grid-template-columns: 1fr 1fr;
}
/* additional layout code */
}
关键点:
.container是被测量的容器元素。container-type: inline-size表示查询容器的 inline 方向尺寸。container-name: clothing-card给容器命名,便于在@container中引用。.content先定义基础的一列网格布局。@container clothing-card (width > 250px)在容器宽度超过 250px 时应用内部样式。grid-template-columns: 1fr 1fr把内容切换为两列。
Cascade Layers:把样式优先级变成架构
(07:14)Cascade Layers 允许团队把工具类、业务覆盖和用户默认样式放进不同层级。层级之间的顺序先于单个选择器的 specificity。
/* Author Styles - Layer A */
@layer utilities {
div {
background-color: red;
}
}
/* Author Styles - Layer B */
@layer customizations {
div {
background-color: teal;
}
}
/* Author Styles - Layer C */
@layer userDefaults {
div {
background-color: yellow;
}
}
关键点:
@layer utilities定义工具样式层。@layer customizations定义项目覆盖样式层。@layer userDefaults定义另一组作者样式层。- 每个层内部都使用同一个
div选择器。 - 最终优先级由层的定义顺序决定,而非这里的选择器差异。
:has():父元素根据子元素状态改样式
(08:54)消息表单里,复选框被选中时整个表单变色。过去常见做法是用 JavaScript 给父元素加类。:has() 让这个状态直接留在 CSS 里。
<!-- :has() pseudo-class -->
<style>
form:has(input[type="checkbox"]:checked) {
background: #ff927a;
}
</style>
<form class="message">
<textarea rows="5" cols="60" name="text"
placeholder="Enter text"></textarea>
<div class="checkbox">
<input type="checkbox" value="urgent">
<label>Urgent?</label>
</div>
<button>Send Message</button>
</form>
关键点:
form:has(...)匹配内部存在指定元素状态的 form。input[type="checkbox"]:checked指向已选中的 checkbox。background: #ff927a把父级表单背景改成提示色。textarea是消息正文。- checkbox 的
value="urgent"表达这条消息的紧急状态。
Web App 图标:manifest 与 apple-touch-icon
(21:13)用户把 Web App 加到 Home Screen 时,图标可以来自 Web App Manifest。
// Manifest file
"icons": [
{
"src": "orange-icon.png",
"sizes": "120x120",
"type": "image/png"
}
]
关键点:
icons列出可用于安装入口的图标资源。src指向图标文件。sizes标出图标尺寸。type标出图片 MIME 类型。
(21:28)如果 HTML head 中声明了 apple-touch-icon,它会用于 Apple 设备图标。演讲中说明,若希望 manifest 图标优先,需要确保 HTML head 没有定义 apple-touch-icon。
<!-- HTML head -->
<link rel="apple-touch-icon" href="blue-icon.png" />
关键点:
rel="apple-touch-icon"声明 Apple 设备使用的 Home Screen 图标。href="blue-icon.png"指向这张设备专用图标。- 这适合为 iOS 和 iPadOS 提供不同于其他平台的图标。
多窗口同步:Broadcast Channel
(22:20)同一个服装交换网站可能同时开在两个窗口里。一个窗口领取了商品,另一个窗口需要知道商品已经不可用。
// State change
broadcastChannel.postMessage("Item is unavailable");
关键点:
broadcastChannel表示同源浏览上下文之间的通信通道。postMessage(...)向其他同源页面发送状态变化。- 字符串消息表达“商品已不可用”。
Origin private file system:站点私有文件
(22:49)File System Access API 的 origin private file system 给每个 origin 独立的私有存储空间。演讲用草稿文件示例展示读写入口。
// Accessing the origin private file system
const root = await navigator.storage.getDirectory();
// Create a file named Draft.txt under root directory
const draftHandle = await root.getFileHandle("Draft.txt", { "create": true });
// Access and read an existing file
const existingHandle = await root.getFileHandle("Draft.txt");
const existingFile = await existingHandle.getFile();
关键点:
navigator.storage.getDirectory()取得当前 origin 的根目录。root.getFileHandle("Draft.txt", { "create": true })创建或取得草稿文件句柄。root.getFileHandle("Draft.txt")重新取得已存在文件的句柄。existingHandle.getFile()从文件句柄读取File对象。
Shared Worker:同源页面共享后台任务
(25:08)多个同源页面需要后台计算时,可以共享一个 worker,减少重复 worker 带来的内存压力。
// Create Shared Worker
let worker = new SharedWorker("SharedWorker.js");
// Listen for messages from Shared Worker
worker.port.addEventListener("message", function(event) {
console.log("Message received from worker: " + event);
});
// Send messages to Shared Worker
worker.port.postMessage("Send message to worker");
关键点:
new SharedWorker("SharedWorker.js")创建共享 worker。worker.port.addEventListener("message", ...)监听 worker 发回的消息。console.log(...)在收到消息时输出内容。worker.port.postMessage(...)通过端口向 worker 发送消息。
数组尾部操作:findLast()、findLastIndex() 与 at()
(25:56)从数组尾部查找时,不必先 reverse() 改变数组。
const list = ["shirt","pants","shoes","hat","shoestring","dress"];
const hasShoeString = (string) => string.includes("shoe");
console.log(list.findLast(hasAppString));
// shoestring
console.log(list.findLastIndex(hasAppString));
// 4
关键点:
list是待搜索的商品名称数组。hasShoeString是匹配包含shoe的回调函数。findLast(...)从数组尾部向前找第一个匹配项。findLastIndex(...)从数组尾部向前找第一个匹配项的索引。- 代码片段中调用名写作
hasAppString;按上下文应传入前面声明的匹配函数。
(26:17)at() 支持负索引,适合读取尾部附近的元素。
const list = ["shirt","pants","shoes","hat","shoestring","dress"];
// Instead of this:
console.log(list[list.length - 2]);
// It's as easy as:
console.log(list.at(-2));
关键点:
list[list.length - 2]用长度计算倒数第二个元素。list.at(-2)直接读取倒数第二个元素。- 负索引让尾部访问更短,也减少手写长度计算。
CSP Level 3:strict-dynamic
(29:04)Content Security Policy Level 3 中的 strict-dynamic 允许用 nonce 信任一个脚本,再把信任扩展到这个脚本加载的后续脚本,减少显式域名 allow list。
// strict-dynamic source expression
// Without strict-dynamic
Content-Security-Policy: script-src desired-script.com dependent-script-1.com
dependent-script-2.com dependent-script-3.com; default-src "self";
// With strict-dynamic
Content-Security-Policy: default-src "self"; script-src "nonce-desired" "strict-dynamic";
关键点:
- 第一条策略需要列出目标脚本和多个依赖脚本域名。
- 长 allow list 容易扩大被允许加载脚本的范围。
- 第二条策略使用 nonce 标记受信任脚本。
strict-dynamic把信任传递给受信任脚本加载的脚本。
核心启发
-
做一个无障碍优先的商品请求弹窗:用
<dialog>管理弹窗结构,用::backdrop管理遮罩,用inert排除非当前轮播项。入口从站点里最复杂的自定义 modal 开始。 -
重写一个可复用商品卡片组件:把组件容器标成
container-type: inline-size,用@container控制窄栏、主栏和 hero 卡片布局。这样组件移动到不同区域时,不需要新增页面级 media query。 -
把表单状态样式从 JavaScript 移回 CSS:用
form:has(input[type="checkbox"]:checked)处理紧急消息、同意协议、筛选项选中这类父级状态。先选一个只为改样式而写的 JS class 切换逻辑替换。 -
给多窗口 Web App 加状态广播:在库存、草稿、通知设置这类状态变化点调用
broadcastChannel.postMessage(...),让同源窗口保持一致。适合管理后台、购物车和协作工具。 -
为 Web App 增加私有草稿存储:用
navigator.storage.getDirectory()取得 origin private file system 根目录,再用getFileHandle()保存未发布内容。适合文章编辑器、表单草稿和本地缓存。
关联 Session
- What’s new in WKWebView — WKWebView 是把 Web 内容嵌入 App 的入口,和 Safari/WebKit 平台更新互补。
- Meet Web Push for Safari — 深入讲解本场 Session 中提到的 Safari Web Push 标准实现。
- What’s new in Safari Web Extensions — 展开 Safari Web Extensions、Manifest V3 和扩展能力更新。
- Create Safari Web Inspector Extensions — 对应本场提到的 Web Inspector 扩展能力,讲如何创建自定义检查器工具。
- What’s new in web accessibility — 继续讲 dialog、自定义控件和 Web 无障碍实现细节。
评论
GitHub Issues · utterances