WWDC Quick Look 💓 By SwiftGGTeam
Develop advanced web content

Develop advanced web content

观看原视频

Highlight

Safari 15 在 JavaScript、WebAssembly 和 Web API 三个方向做了大规模更新,包括私有类字段、WeakRef、WebGL2、MediaRecorder 和语音识别 API。

核心内容

Web 开发者在 Safari 上长期面临一个痛点:新特性支持总是慢半拍。WWDC2021 这次一次性补上了大量缺口。

JavaScript 引擎方面,私有类字段终于落地。以前用下划线前缀 _startTime 只是约定,外部代码仍然能访问。现在用 #startTime,语言层面强制执行封装,访问会抛错。

WeakRef 和 FinalizationRegistry 让内存管理更精细。你可以持有对象的弱引用,不阻止垃圾回收;对象被回收时还能收到通知做清理。这对缓存系统、测试框架很有用。

WebAssembly 引擎升级了内存指令、i64/BigInt 互转、引用类型和流式编译。C++/Rust 编译到 Web 的性能又近了一步。

Web API 方面,WebGL2 全平台可用,MediaRecorder 可以录制音视频,Speech Recognition 用 Siri 同款引擎做语音转文字,Web Share 支持分享文件。

详细内容

JavaScript 私有类字段

以前写类,私有成员靠约定:

class StopwatchWithOneButton {
    constructor() {
        this._startTime = null;  // 只是约定,外部仍能访问
    }

    click() {
        if (this._startTime === null) {
            this._startTime = Date.now();
        } else {
            const duration = Date.now() - this._startTime;
            this._startTime = null;
            return duration;
        }
    }
}

现在用 # 前缀,语言强制执行私有:

class StopwatchWithOneButton {
    #startTime = null;  // 真正的私有字段

    click() {
        if (this.#startTime === null) {
            this.#startTime = Date.now();
        } else {
            const duration = Date.now() - this.#startTime;
            this.#startTime = null;
            return duration;
        }
    }

    // 私有方法
    #start() { this.#startTime = Date.now(); }
    #stop() {
        const duration = Date.now() - this.#startTime;
        this.#startTime = null;
        return duration;
    }

    // 静态私有字段
    static #startedCount = 0;
    // 公共静态字段
    static totalCreated = 0;
}

关键点:

  • #field 是语言级私有,外部访问会抛出 TypeError(01:49)
  • 私有方法同样用 # 前缀,只能在类内部调用(03:15)
  • 支持静态私有字段 #staticField 和公共静态字段 staticField(03:39)
  • 不需要 WeakMap hack 来实现私有属性了

WeakRef 和 FinalizationRegistry

需要跟踪一批对象,但又不想阻止它们被垃圾回收:

const allStopwatches = new Set();
const registry = new FinalizationRegistry((id) => {
    // 对象被回收时自动清理
    for (const ref of allStopwatches) {
        if (ref.id === id) {
            allStopwatches.delete(ref);
            break;
        }
    }
});

function addStopwatch(sw) {
    const id = generateId();
    const ref = new WeakRef(sw);
    ref.id = id;
    allStopwatches.add(ref);
    registry.register(sw, id);  // sw 被回收时触发回调
}

function clickAllStopwatches() {
    for (const ref of allStopwatches) {
        const sw = ref.deref();  // 获取对象,可能返回 undefined
        if (sw) sw.click();
    }
}

关键点:

  • WeakRef 持有弱引用,不阻止垃圾回收(04:19)
  • deref() 获取对象,如果已被回收则返回 undefined(05:56)
  • FinalizationRegistry 在对象被回收时触发回调做清理(06:21)
  • 垃圾回收时机不确定,不要依赖它做关键逻辑

Top-level await 和 Module Workers

模块里可以直接用 await

// stopwatch.mjs
const config = await fetch('/config.json').then(r => r.json());
export class Stopwatch { /* 使用 config */ }
// main.mjs
import { Stopwatch } from './stopwatch.mjs';
// stopwatch.mjs 中的 await 完成后才会执行到这里
const sw = new Stopwatch();

Workers 也支持 ES Modules:

// 创建 module worker
const worker = new Worker('./worker.mjs', { type: 'module' });

// Service Worker
navigator.serviceWorker.register('./sw.mjs', { type: 'module' });

// Audio Worklet
await audioContext.audioWorklet.addModule('./processor.mjs');

关键点:

  • top-level await 只在 ES Module 中可用,普通脚本会报语法错误(07:37)
  • 被导入的异步模块会阻塞依赖它的模块执行(08:38)
  • module workers 支持动态导入、优化加载和依赖管理(09:21)
  • web worker、service worker、worklet 都支持 module(09:51)

WebGL2 和 WebM/VP9 支持

WebGL2 新增特性:

  • 3D 纹理,支持体积效果如云雾(19:03)
  • Sampler 对象,着色器中使用纹理更灵活(19:09)
  • Transform feedback,GPU 上实现粒子系统(19:16)
  • 后端从 OpenGL 迁移到 Metal,iOS Simulator 也能用 GPU(20:23)
  • 可用 Xcode Frame Debugger 分析 WebGL 代码(20:40)

WebM 和 VP9:

  • macOS 11.3+ 支持 VP8/VP9 视频 + Vorbis 音频(21:19)
  • macOS 12 增加 Opus 音频支持(21:30)
  • iPadOS 15 支持 MSE 流式播放 WebM(21:40)
  • 可用 MediaCapabilities API 检测具体支持情况(21:47)

MediaRecorder 录制音视频

let mediaRecorder;
let recordedChunks = [];

async function startRecording() {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    mediaRecorder = new MediaRecorder(stream, {
        mimeType: 'audio/webm',
        audioBitsPerSecond: 128000
    });

    mediaRecorder.ondataavailable = (event) => {
        if (event.data.size > 0) recordedChunks.push(event.data);
    };

    mediaRecorder.onstop = () => {
        const blob = new Blob(recordedChunks, { type: 'audio/webm' });
        const url = URL.createObjectURL(blob);
        document.querySelector('audio').src = url;
    };

    mediaRecorder.start();
}

function stopRecording() {
    mediaRecorder.stop();
}

关键点:

  • MediaRecorder 从 media element 或 MediaStream 捕获数据(25:23)
  • 可指定 MIME type 和比特率(25:39)
  • ondataavailable 收集数据片段,onstop 组装成 Blob(26:52)
  • 支持从麦克风、摄像头或屏幕录制

Audio Worklet 实时音频处理

// distortion-processor.mjs
class DistortionProcessor extends AudioWorkletProcessor {
    process(inputs, outputs, parameters) {
        const input = inputs[0];
        const output = outputs[0];
        for (let channel = 0; channel < input.length; channel++) {
            const inputChannel = input[channel];
            const outputChannel = output[channel];
            for (let i = 0; i < inputChannel.length; i++) {
                outputChannel[i] = this.distort(inputChannel[i]);
            }
        }
        return true;  // 保持活跃
    }

    distort(x) {
        return x > 0 ? Math.min(x * 2, 1) : Math.max(x * 2, -1);
    }
}

registerProcessor('distortion', DistortionProcessor);
// main.js
async function startRecordingWithEffect() {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    const audioContext = new AudioContext();
    await audioContext.audioWorklet.addModule('./distortion-processor.mjs');

    const source = audioContext.createMediaStreamSource(stream);
    const effectNode = new AudioWorkletNode(audioContext, 'distortion');
    const destination = audioContext.createMediaStreamDestination();

    source.connect(effectNode).connect(destination);

    // 录制处理后的音频
    mediaRecorder = new MediaRecorder(destination.stream);
    mediaRecorder.start();
}

关键点:

  • Audio Worklet 在音频渲染线程运行,减少主线程和渲染线程的切换(27:37)
  • 比旧的 ScriptProcessorNode 延迟更低(27:49)
  • 处理器必须继承 AudioWorkletProcessor 并实现 process() 方法(29:01)
  • registerProcessor() 注册后才能在主线程通过 AudioWorkletNode 使用(29:38)

Web Share 分享文件

async function shareAudio(blob, filename) {
    const file = new File([blob], filename, { type: 'audio/webm' });
    const shareData = {
        files: [file],
        title: 'Voice Memo',
        text: 'Check out my recording'
    };

    if (navigator.canShare && navigator.canShare(shareData)) {
        await navigator.share(shareData);
    }
}

关键点:

  • Web Share API 今年新增文件分享支持(30:32)
  • 可分享图片、视频、音频等文件类型(30:36)
  • 先调用 canShare() 检测是否支持,再调用 navigator.share()(31:37)
  • 分享界面和系统风格一致,支持 Messages、Mail、AirDrop 等目标(30:17)

Speech Recognition 语音识别

const recognition = new webkitSpeechRecognition();
recognition.continuous = true;
recognition.interimResults = false;

recognition.onresult = (event) => {
    let transcript = '';
    for (let i = event.resultIndex; i < event.results.length; i++) {
        if (event.results[i].isFinal) {
            transcript += event.results[i][0].transcript;
        }
    }
    console.log(transcript);
};

recognition.onend = () => {
    console.log('Recognition ended');
};

function startRecognition() {
    recognition.start();
}

function stopRecognition() {
    recognition.stop();
}

关键点:

  • 使用 Siri 同款语音识别引擎,支持多语言(32:23)
  • 目前仍需 webkit 前缀:new webkitSpeechRecognition()(33:25)
  • continuous 属性控制是否持续识别直到手动停止(33:36)
  • 首次使用会弹出隐私授权提示(32:41)
  • 结果按概率排序,取第一个就是最佳识别结果(34:02)

核心启发

  1. 用私有类字段替换 WeakMap 封装#field 语法比 WeakMap 方案简单直观,且语言层面保证私有性。
  2. Web Share 文件分享让 Web App 更像原生。几行代码就能调用系统分享面板发送文件,用户体验和原生 App 一致。
  3. MediaRecorder + Audio Worklet 组合做音频应用。录制、实时处理、分享一条链路都通了,可以在 Web 上实现完整的音频编辑工具。
  4. Speech Recognition 降低语音交互门槛。用 Siri 引擎做语音转文字,不需要自己训练模型或调用第三方 API。
  5. Module Workers 把重计算移到后台。配合 ES Module 的依赖管理和动态导入,复杂计算不再阻塞主线程。

关联 Session

评论

GitHub Issues · utterances