WWDC Quick Look 💓 By SwiftGGTeam
What's new in Safari and WebKit

What's new in Safari and WebKit

观看原视频

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 提供图片替代文本。
  • widthheight 帮助浏览器预留布局空间。

容器查询:组件按自身空间切换布局

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:17at() 支持负索引,适合读取尾部附近的元素。

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

评论

GitHub Issues · utterances