Highlight
Safari 13.1 和 Safari 14 给 Web 开发者加入 Web Animations、ResizeObserver、Async Clipboard、CSS Shadow Parts、WebP、HDR 媒体、现代 JavaScript 语法和 App Clips 网站横幅等能力,让网站、主屏幕 Web App 和嵌入式 WebKit 内容能更接近原生平台体验。
核心内容
Web 开发者的日常麻烦,往往来自同一套页面在浏览器、主屏幕 Web App、App 内 WebView 里表现不一致。2020 年这场 session 一开始就把重点放在互操作性上:Safari 这一年新增通过了 140,000 个 Web Platform Tests,覆盖 service workers、XHR + Fetch、pointer events、CSS、SVG、WebAssembly 等区域。
性能也是同一个问题的另一面。Safari 14 打开未访问网站快 13%,打开最近访问网页快 42%,地址栏打开最近访问网页快 52%。Instant back 能缓存更多页面,滚动 CPU 使用量降到三分之一,IndexedDB 操作最多快 10 倍。页面先变得更快,后面的 API 才有空间承担更多交互。
Apple 随后把更新拆成几条线:Web API 让页面能控制动画、监听元素尺寸、访问剪贴板和封装组件;CSS 更新让排版、字体和选择器更接近大型站点需要;媒体更新把 WebP、HDR、远程播放、画中画和 HLS 元数据补齐;JavaScript 更新带来 BigInt、空值合并、可选链、逻辑赋值、公共类字段和 replaceAll。
这不是单个框架的发布会。它更像一张 Safari 和 WebKit 的迁移清单:先找出页面里靠 polyfill、库或平台私有能力撑起来的部分,再逐项换成标准 Web API。对已有网站来说,最实际的收益是少写兼容代码,同时把 Safari、iOS、iPadOS 和 macOS 上的体验拉齐。
详细内容
1. Web API 从页面级响应走向组件级响应
(04:22)Web Animations API 在 Safari 13.1 中可用。它让 JavaScript 直接创建和控制 CSS 动画与过渡,不需要手动改一组元素属性。session 的例子是在点击 WebKit logo 时,让指南针指针再旋转一次。
// Web Animations API Code Example
let needle = document.getElementById("needle");
let logo = document.getElementById("logo");
logo.addEventListener("click", () => {
needle.animate({
transform: [
"rotateX(35deg) rotateZ(13deg)",
"rotateX(35deg) rotateZ(733deg)",
],
easing: ["ease-out"],
}, 800);
});
关键点:
getElementById("needle")和getElementById("logo")分别取到被动画控制的元素和触发点击的元素。addEventListener("click", ...)把动画绑定到真实用户交互。needle.animate(...)接收关键帧对象和持续时间,直接描述从一个transform到另一个transform的变化。easing: ["ease-out"]让旋转开始快、结束柔和。800是动画时长,单位是毫秒。
(06:43)ResizeObserver 解决的是另一个常见痛点:组件不只会因为视口变化而变窄,也可能因为编辑器、侧栏或子元素变化而改变尺寸。Safari 13.1 支持后,页面可以监听某个容器自己的宽度,并按容器状态切换 CSS class。
// Resize Observer Example
let formatPanelObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
let container = entry.target;
container.classList.toggle("small", entry.contentRect.width < 175);
}
});
formatPanelObserver.observe(document.getElementById("format-panel"));
关键点:
new ResizeObserver(...)创建一个尺寸观察器,回调会收到变化条目。entries.forEach(...)逐个处理发生尺寸变化的元素。entry.target是被观察的容器。entry.contentRect.width < 175用容器自身宽度判断是否进入小尺寸状态。classList.toggle("small", ...)把宽度判断结果同步到 CSS。observe(document.getElementById("format-panel"))指定真正要观察的格式化工具栏。
(08:22)Async Clipboard API 在 Safari 13.1 中可用。它能异步读写系统剪贴板,避免页面在剪贴板访问期间阻塞。纯文本有快捷方法,调用还需要安全上下文和用户交互。
// Programmatic copy
copyButtonElement.addEventListener("click", (event) => {
navigator.clipboard.writeText("Plain text to copy.").then(() => {
// Successful copy
}, () => {
// Copy failed
});
});
// Programmatic paste
pasteButtonElement.addEventListener("click", (event) => {
navigator.clipboard.readText().then((clipText) => {
document.querySelector(".editor").innerText += clipText);
});
});
关键点:
- 复制和粘贴都放在按钮的
clickhandler 中,符合用户交互要求。 navigator.clipboard.writeText(...)写入纯文本。then的两个回调分别处理复制成功和失败。navigator.clipboard.readText()读取纯文本剪贴板内容。document.querySelector(".editor").innerText += clipText把读取到的文本追加到编辑器。
2. Web Components 获得可控的外部样式入口
(10:36)Safari 很早支持 Web Components。2020 年更新的重点是让组件作者和页面作者分工更清楚。组件作者负责内部结构,页面作者只样式化被明确暴露的部分。
let template = document.getElementById("format-button");
window.customElements.define(template.id, class extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
let newButtonElement = template.content.cloneNode(true);
let parts = newButtonElement.querySelectorAll("span");
parts[0].textContent = this.getAttribute("data-icon");
parts[1].textContent = this.textContent;
this.shadowRoot.appendChild(newButtonElement);
this.addEventListener("click", this.handleClick.bind(this));
}
});
关键点:
template.id被用作 custom element 的注册名。- 组件类继承
HTMLElement,构造函数里先调用super()。 attachShadow({mode: "open"})创建 shadow root。template.content.cloneNode(true)复制模板内容。querySelectorAll("span")找到图标和标签两个可填充位置。getAttribute("data-icon")和this.textContent把页面标记中的数据放进组件。appendChild把处理后的 DOM 片段放入 shadow root。- 最后一行把点击事件绑定到组件自己的
handleClick。
(12:30)CSS Shadow Parts 让组件作者把可样式化的内部节点用 part 标出来。页面不需要知道完整 shadow DOM 结构,只依赖这些命名入口。
<template id="format-button">
<button class="format">
<span part="icon" class="icon"></span>
<span part="label" class="label"></span>
</button>
</template>
关键点:
<template id="format-button">仍然是组件模板。part="icon"暴露图标区域,页面作者可以单独设置它的样式。part="label"暴露文本区域。- 内部按钮结构仍由组件保留,页面作者只触碰组件公开的样式点。
(12:38)页面作者随后用 ::part(...) 选择这些公开部件,给不同格式按钮加视觉提示。
#bold::part(icon) {
color: var(--formatting-button-icon-color);
font-weight: bold;
}
#italic::part(icon) {
color: var(--formatting-button-icon-color);
font-style: italic;
}
#underline::part(icon) {
color: var(--formatting-button-icon-color);
text-decoration: underline;
}
关键点:
#bold::part(icon)只样式化 bold 按钮暴露出的 icon 部件。var(--formatting-button-icon-color)让颜色继续由页面主题控制。font-weight: bold、font-style: italic、text-decoration: underline分别映射按钮含义。- 这些规则不会要求页面作者穿透组件内部的完整 DOM。
3. CSS 更新集中在字体、断行和选择器复杂度
(14:32)WebKit 增加了系统字体族。system-ui 映射到平台合适的 UI 字体,ui-serif 对应 New York,ui-monospace 对应 SF Mono,ui-rounded 对应 SF Rounded。Web App 可以更自然地贴近系统界面。
font-family: system-ui;
font-family: ui-sans-serif;
font-family: ui-serif;
font-family: ui-monospace;
font-family: ui-rounded;
关键点:
system-ui是通用系统 UI 字体入口。ui-sans-serif在 Apple 平台上使用 San Francisco 一类的界面字体。ui-serif适合内容区域使用的系统衬线字体。ui-monospace适合代码和等宽内容。ui-rounded适合需要圆角字形气质的界面。
(16:43)line-break: anywhere 用来处理窄容器里的长技术字符串。它允许在溢出前的字符位置断行,避免 API 名称、命令或长标识符撑破布局。
code {
line-break: anywhere;
}
关键点:
- 规则直接作用在
code元素上。 line-break: anywhere允许更积极地寻找断行位置。- transcript 的场景是 Web Inspector 命令行 API 文档里过长语法导致列表项空行和横向溢出。
(18:02):is() 让重复选择器变短。session 用标题相邻时去掉顶部外边距举例,原本要列出大量 h1 + h2、h1 + h3 组合,现在可以归并。
h1, h2, h3, h4, h5, h6 {
margin-top: 3em;
}
:is(h1, h2, h3, h4, h5, h6) + :is(h1, h2, h3, h4, h5, h6) {
margin-top: 0;
}
关键点:
- 第一条规则给所有标题加默认顶部间距。
:is(h1, h2, h3, h4, h5, h6)匹配任意层级标题。- 中间的
+表示后一个标题紧跟在前一个标题之后。 - 第二条规则把连续标题之间的顶部间距归零。
(19:07):where() 的匹配方式类似,但匹配结果的 specificity 永远为零。它适合写默认样式,再让后续规则容易覆盖。
:where(.intro, .pullquote, #hero) + p {
text-transform: uppercase;
}
h2 + p,
h3 + p,
h4 + p,
h5 + p,
h6 + p {
text-transform: none;
}
关键点:
:where(.intro, .pullquote, #hero) + p匹配这些元素后面的段落。:where()把自身 specificity 归零。- 后续
h2 + p到h6 + p规则可以覆盖前面的默认 uppercase。 - 这解决了
:is()因最高 specificity 参与计算而难以覆盖的情况。
4. 媒体更新覆盖图片格式、布局稳定性和视频控制
(19:53)Safari 14 和 iOS 14 支持 WebP。WebP 能提供接近 JPEG 的有损压缩、类似 PNG 的无损压缩,还支持透明和动画。落地时可以用 picture 做渐进 fallback。
<picture>
<source srcset="example.webp" type="image/webp">
<img src="example.jpg" alt="Example Image">
</picture>
关键点:
<picture>让浏览器在多个资源候选中选择可用格式。<source type="image/webp">提供 WebP 版本。<img src="example.jpg">是不支持 WebP 时的 fallback。- transcript 给出的同质量示例里,WebP 有损图比 JPEG 节省 41%,无损图比 PNG 节省 33%。
(21:19)Safari 13.1 和 iOS 13.4 的 WebKit 会从图片标签的 width 与 height 属性计算默认宽高比。开发者补上尺寸后,浏览器能在图片加载前预留空间,减少布局跳动。
<img src="MexicoCity.png" width="560" height="747">
关键点:
src指向图片资源。width="560"和height="747"提供原始尺寸。- WebKit 用这两个属性计算默认 aspect ratio。
- 图片尚未下载完成时,页面也能保留正确空间。
(22:42)Safari 14 on macOS 支持网页中的 HDR 视频。开发者可以用 CSS media query 或 JavaScript 检测高动态范围显示能力,再交付增强内容。
<style>
@media only screen (dynamic-range: high) {
/* HDR-only CSS rules */
}
</style>
<script>
if (window.matchMedia("dynamic-range: high")) {
// HDR-specific JavaScript
}
</script>
关键点:
@media only screen (dynamic-range: high)把样式限定到 HDR 显示场景。- JavaScript 侧通过
window.matchMedia("dynamic-range: high")做同类检测。 - transcript 的建议是渐进增强:有 HDR 能力时提供对应内容,没有时保持普通体验。
(23:19)Remote Playback API 给自定义 Web 媒体播放器提供标准远程播放入口。它能把音频或视频送到连接的电视、音箱和 AirPlay 设备。
<video id="videoElement" src="https://site.example/video.mp4"></video>
<button id="deviceButton">Send video to a remote device</button>
<script>
let videoElement = document.getElementById("videoElement");
let deviceButton = document.getElementById("deviceButton");
deviceButton.addEventListener("click", (event) => {
videoElement.remote.prompt().then(updateRemotePlaybackState);
});
</script>
关键点:
<video>是要被远程播放的媒体元素。<button>是自定义播放器里的设备选择按钮。getElementById分别取到视频和按钮。- 点击按钮后调用
videoElement.remote.prompt()打开设备选择流程。 then(updateRemotePlaybackState)在选择结果返回后更新播放状态。
5. JavaScript 新语法减少防御性样板代码
(27:11)Safari 14 支持 BigInt。它用于超过 JavaScript 最大安全整数的整数场景,常见于密码学。session 特别提醒:不能和普通 number 混用运算,也不能直接用 JSON.stringify 做序列化。
let bigInt = BigInt(Number.MAX_SAFE_INTEGER);
// 9007199254740991n
console.log(8n / 2n);
// 4n
console.log(9n / 2n);
// 4n
关键点:
BigInt(Number.MAX_SAFE_INTEGER)创建 BigInt 值。- 输出里的
n表示 BigInt 字面量。 8n / 2n得到4n。9n / 2n也得到4n,因为 BigInt 除法会丢弃小数部分。
(28:02)空值合并运算符检查的是 null 和 undefined。它保留 false、空字符串和 0 这类有效值,适合处理表单、个人资料和配置对象。
class Person {
constructor(firstName, lastName, age) {
this.firstName = firstName ?? "Unknown";
this.lastName = lastName ?? "Unknown";
this.age = age ?? NaN;
}
}
console.log(new Person());
// { firstName: "Unknown", lastName: "Unknown", age: NaN }
console.log(new Person(false, false, true));
// { firstName: false, lastName: false, age: true }
console.log(new Person("John", "", 0));
// { firstName: "John", lastName: "", age: 0 }
console.log(new Person("John", "Appleseed", 42));
// { firstName: "John", lastName: "Appleseed", age: 42 }
关键点:
- 构造函数接收
firstName、lastName和age。 ??左侧为null或undefined时才使用右侧默认值。new Person()会得到默认姓名和NaN年龄。false、空字符串、0都会保留下来,不会被误替换。
(29:41)可选链运算符让属性、索引和方法访问少写显式 guard。下面的注册函数直接访问可能不存在的 person。
class Person {
constructor(firstName, lastName, age) {
this.firstName = firstName ?? "Unknown";
this.lastName = lastName ?? "Unknown";
this.age = age ?? NaN;
this.name = { firstName: this.firstName, lastName: this.lastName };
}
}
function register(person) {
// With optional chaining
console.log(person?.name.firstName);
}
register(new Person());
undefined
register(new Person("John", "Appleseed"));
"John"
关键点:
Person构造函数仍使用空值合并提供默认字段。this.name保存嵌套的名字对象。register(person)里用person?.name.firstName访问嵌套属性。register(new Person())能安全输出undefined。- 传入完整姓名时,输出
"John"。
(30:52)逻辑赋值运算符把检查和赋值合在一起。session 用 ??= 展示只在左侧为 null 或 undefined 时写入默认内容。
a &&= b // and assignment operator
a ||= b // or assignment operator
a ??= b // nullish assignment operator
// Nullish coalescing approach
element.innerHTML = element.innerHTML ?? "Hello World!"
// Logical assignment operator
element.innerHTML ??= "Hello World!"
关键点:
&&=、||=、??=分别对应逻辑与、逻辑或、空值合并赋值。- 普通空值合并写法会重新给
element.innerHTML赋值。 element.innerHTML ??= "Hello World!"只在当前值为空值时写入默认字符串。- transcript 强调左侧表达式只会求值一次。
核心启发
-
做一个组件自适应评论编辑器:让工具栏根据编辑器容器宽度自动收起文字标签。为什么值得做:ResizeObserver 监听的是元素自身尺寸,不依赖全局窗口宽度。怎么开始:观察编辑器工具栏容器,在低于阈值时切换
smallclass,并用 CSS 隐藏 label。 -
给内容站增加安全的富文本粘贴按钮:为 CMS、论坛或知识库编辑器加入用户触发的粘贴入口。为什么值得做:Async Clipboard API 能异步读取剪贴板,transcript 还说明它支持图片和富文本等多种 item type。怎么开始:先用
readText()做纯文本路径,再把富 HTML 粘贴放到明确的按钮交互里。 -
把设计系统组件开放成可主题化 Web Components:内部结构由组件维护,业务页面只调整
icon、label等公开部件。为什么值得做:CSS Shadow Parts 让组件作者声明可样式化边界,页面作者不用依赖 shadow DOM 细节。怎么开始:在模板内部给可主题化节点加part,再为页面示例提供::part(...)规则。 -
为图片密集页面降低加载跳动和体积:适合相册、商品列表、文章封面流。为什么值得做:WebP 支持减少图片体积,
width和height属性让 WebKit 预留图片空间。怎么开始:为图片生成 WebP 与 JPEG 两套资源,用picture输出格式 fallback,并在img上保留尺寸属性。 -
给自定义视频播放器补齐系统媒体入口:在教学、会议回放或直播站点里加入远程播放和画中画按钮。为什么值得做:Remote Playback API 和 Picture in Picture API 都要求用户交互,适合放进播放器控件。怎么开始:先给
<video>建立自定义按钮,再分别调用remote.prompt()和requestPictureInPicture()。
关联 Session
- Meet Safari Web Extensions — 10663 结尾推荐继续查看 Safari Web Extensions,这场 session 讲扩展创建、迁移和发布流程。
- What’s new in Web Inspector — 10663 多次提到 Web Inspector 的动画和调试能力,10646 展开讲 2020 年工具更新。
- Discover WKWebView enhancements — 10663 面向网站和 WebKit 内容,10188 继续讲 App 内 WKWebView 的脚本、导出和隐私能力。
- Meet Face ID and Touch ID for the web — 10663 点到 Web Authentication API 支持 Face ID 和 Touch ID,10670 讲完整登录流程。
- Shop online with AR Quick Look — 10663 的平台集成部分提到 AR Quick Look 购物横幅,10604 深入讲这条电商体验。
评论
GitHub Issues · utterances