Tips:
- 本文主要面向使用JavaScript、WebAssembly、WebGL的Web 应用开发者。
- 本文提及部分特性或API在Safari14.2以下版本中可能暂未支持,可以使用Safari Technology Preview - 14.2[1]调试。
- 具体API的使用和支持情况可以参考 MDN Web Docs[2],但文档更新可能会有延迟。
本文知识目录:
1JavaScript语法增强
1.1 使用#修饰类的属性、静态变量、方法,保证它们仅在类的内部可见
需要注意的是增加#后,#已经是名称的一部分,比如#_startTime才是一个完整的变量名。
//class with private variable and function
class PrivateStopWatchWithOneButton {
//使用#定义私有变量
#_startTime = 0;
//使用#定义私有静态变量
static #stopWatchCount = 0;
click(){
if (!this.#_startTime) {
this.#start();
}else{
this.#stop();
}
}
//使用#定义私有方法
#start() {
PrivateStopWatchWithOneButton.#stopWatchCount++;
this.#_startTime = Date.now();
console.log('StopWatch started');
}
}
function demo(){
var counter = new StopWatchWithOneButton();
counter.click();
counter.#stopWatchCount = 0; //SyntaxError
counter.#start();//SyntaxError
}
1.2 WeakRef一种新的弱引用方法
Map和Set是JavaScript中常用的集合类型,为了实现更高效的垃圾回收,在部分情况下需要通过WeakMap和WeakSet实现对集合对象的弱引用,但是WeakMap和WeakSet没有Iterator接口,因而无法实现迭代的逻辑。所以Apple今年给出了几个新的接口,比如通过WeakRef获得对象的弱引用,同时可以通过FinalizationRegistry得知弱引用的对象被垃圾回收的时机,然后在注册的回调中执行一些清理操作。
其中关键的几个概念:
- WeakRef:允许您保留对另一个对象的弱引用,而不会阻止被弱引用对象被GC回收。
- FinalizationRegistry:可以让你在对象被垃圾回收时请求一个回调。
- deref:返回WeakRef实例的目标对象,如果目标对象已被垃圾收集,则返回undefined 。
下面是一段伪代码:
class StopWatchWithOneButton {
_startTime = 0;
click(){
//...
}
//some detail implimentation...
}
const allStopWatches = new Map();
var nextAvailableIdentifier = 1;
function removeStopwatch(identifier){
allStopWatches.delete(identifier);
}
//通过FinalizationRegistry新建一个注册表,同时注册关联的回调函数
const finalizationRegistry = new FinalizationRegistry(removeStopwatch);
function createStopwatch(){
let identifier = nextAvailableIdentifier++;
let stopwatch = new StopWatchWithOneButton();
//WeakRef()获得stopwatch的弱引用
allStopWatches.set(identifier, new WeakRef(stopwatch));
finalizationRegistry.register(stopwatch, identifier);
return stopwatch;
}
function clickAllStopwatches(){
console.log('ready to click all buttons');
for(let weakStopwatch in allStopWatches.values()){
//迭代获取weakStopwatch,通过deref()判断对象是否被GC
weakStopwatch.deref()?.click();
}
}
但是由于FinalizationRegistry的运行依赖于GC,GC的运行又依赖于event loop机制,所以存在一些不确定性。比如回调时机可能和你预期的不一致,所以在使用之前要评估下你的场景是否适用这几个方法,避免掉到坑里。
1.3 采用await方式import Module
await这个概念出现在了很多的编程语言中,它的最主要特征就是简化异步调用,让代码的可读性极大增强。原来await只能在async函数中使用,但是现在也可以在import module的时候使用,让module之间的依赖管理变得更加简单,比如像下面这样:
//Another file stopwatchInModule.js
export class StopWatchWithOneButton {
_startTime = 0;
click(){
//...
}
//some detail implimentation...
}
上述await方法的使用,有以下两个效果:
- stopwatch = new StopWatchWithOneButton();会在import执行完成之后再执行。
- 如果被import的stopwatchInModule.js中有异步任务执行,stopwatch = new StopWatchWithOneButton();会在异步任务执行完成后继续执行。
需要注意的是,await用来import module的时候仅在module类型的script中有效,其他类型的script会直接报错。
1.4 在worker中使用module
由于JavaScript采用的是单线程模型,Web worker则为JavaScript创造了多线程环境,主线程可以通过创建Worker在子线程中执行一些脚本,将一些计算密集型或者高延迟的任务放到后台运行,保证UI交互的流畅性。而Module则可以实现动态import、对加载和执行实现优化、实现依赖管理。所以在worker中使用Module可以更轻松的将一些heavy work转移到后台线程。module现在可以应用于多种不同类型的worker中,比如:web worker、service worker和worklet。
具体的使用方法如下:
//在web worker中的用法
let worker = new Worker(moduleScriptURL,{type:"module"})
//在service worker中的用法
nivagator.serviceWorker.register(scriptURL,{type:"module"});
//在worklet中的用法
var audioContext = new AudioContext();
dusioContext.audioWorklet.addModule(moduleScriptURL);
1.5 Internationalization API的更新
更新了5个国际化的API,分别如下:
- Intl.NumberFormat 设置数字显示格式
- Intl.DateTimeFormat 根据不同国家设置时间/日期的显示格式
- Intl.Segmenter 根据不同语言的语法规则进行分词
- Intl.ListFormat 根据不同语言的语法进行连词
- Intl.DisplayNames 自动根据当前页面设置的语言,展示语言切换内容
其中最值得一提的是Intl.Segmenter,可以实现语句的分词功能,在做一些算法的时候进行分词是一项基本的工作,在此基础之上可以做很多有趣的功能,更详细的代码参见demo,感兴趣的同学不妨一试。
2WebAssembly
2008年很多浏览器中开始引入JITs,实现了js运行速度的骤然提升,而WebAssembly被认为可能是web应用性能提升的又一个转折点,funkykarts[3]就是一个采用WebAssembly的例子,其实funkykarts的源码是使用C++来实现的,那在Web中的这一切又是怎么做到的呢?
WebAssembly 可以理解为一种web版的汇编,其实它并不是一种编程语言,但是可为C/C++/Rust等高级语言提供一个高效的编译目标,使Web应用程序获得和原生App相媲美的性能。这就意味着,对于一个现成的Native应用,为了将它移植到web中,不需要从头开始编写JavaScript代码,通过WebAssembly将它编译成浏览器支持的wasm模块,然后通过Webassembly API执行调用即可。这一过程如下图所示:
上图中Emscripten是一种生成wasm的工具,目前常见的这类工具还包括:
目前Chrome、FireFox和Safari都已支持WebAssembly,在具体的功能上还存在些微差异,具体的支持情况可以可以在 WebAssembly 官网[4]找到。
从WebAssembly 展示的信息可以看到Chrome、FireFox、Safari等浏览器对WebAssembly增加了多项功能的支持,具体包括在以下几个方面:
- 通过采用新的内存指令让批量内存操作具备更好的性能。比如批量的复制和初始化操作。
- 通过新的指令告诉用户进程在部分情况下无需捕获异常。比如在在float和int之间转换时的正溢出。
- 新增了符号扩展运算符,实现低位数转高位数,所谓的低位数转高位数的基本原理就是在低位数的左边补上低位数的符号位,直到数字位数达到要求。用来实现WebAssembly的i64类型和JavaScript的BigInt数据类型之间的转换,这一改进可以提高代码的执行速度并且比之前的实现方法更加简单。
- 增加了新的引用类型,允许WebAssembly模块持有JavaScript和DOM对象的引用,并且可以传递和保存它们。
- 通过数据流式的下载和编译,缩短了整体执行时间。
3New Web APIs
这部分主要是介绍部分新颖的API以及他们各自适用的不同场景,有些功能还是很有意思的,比如Speech Recogintion可以借助Siri引擎实现实时文本转换,Web Share功能今年新增了文件共享,而Storage Access在保证用户Cookies安全性的前提下增加了适用范围。下面分别介绍一下它们:
3.1 WebGL2.0
WebGL是实现页面渲染的不二法门,可以帮助开发者在Web中实现非常绚丽的画面效果,就像下图这样,Apple这次在Safari和Webkit中为我们带来了WebGL2.0的支持,下面我们就简单解下什么是WebGL2.0:
WebGL2.0是基于OpenGL ES 3.0实现的Web API,核心是WebGL2RenderingContext接口,在WebGL1的基础上增加了很多的新特性,比如:
- 增加了3d纹理,能够渲染出像云朵一样的volumetric effects(容积效果)。
它的WebGLSampler可以用来存储一系列采样参数,在着色器中使用纹理更加灵活。
- 增加了Transform Feedback来帮助在GPU上实现高性能的粒子系统效果。
由于旧版本的Safari不支持WebGL2.0,所以之前只能通过WebGL1.0实现部分效果,但是从14.2版本开始,所有苹果设备上的Safari都可以支持WebGL2.0,更重要的是今年Apple将WebGL的底层实现从OpenGL迁移到了Metal,这就意味着可以使用iOS模拟器愉快的调试WebGL代码了,同时可以使用Xcode frame debugger来分析webGL的代码,对开发者来说是真的很香。
但是由于WebGL毕竟是相对底层的API,可能不是那么容易上手,所以Apple推荐开发者使用现成的封装库提高开发的效率,比如A-frame、babylon.js、playcanvas、three.js等.
3.2 WebM & VP9
WebM是一种免版权的视频文件格式,它定义了文件的容器结构、视频和音频格式,WebM文件由使用VP8或VP9视频编解码器压缩的视频流和使用Vorbis或Opus音频编解码器压缩的音频流组成。WebM和MP4等格式相比,在保证出色视频质量的前提下有更高的压缩率,国外的Youtube,国内的腾讯视频都支持WebM格式视频的上传发布。Safari也终于在今年增加了对WebM的支持。
- macOS11.3 支持VP8/VP9视频格式 + Vorbis音频的WebM文件
- macOS12 支持VP8/VP9视频格式 + Vorbis/Opus音频的WebM文件
- iPadOS15 支持通过Media Source Extension API来播放WebM文件
由于不同设备对WebM支持的情况存在差异,在实际编码中可以通过MediaCapabilities API判断当前设备是否支持WebM。
const mediaconfig = {
type = 'media-source',
video:{
contentType: 'video/webm; codecs="vp09.00.10.08"'
width: 1920, height:1080, bitrate:2646242,
}
};
navigator.mediaCapabilities.decodingInfo(mediaConfig).then(
//do something else
)
上文提及的VP9是一种在性能上可以和H265一较高下的视频编码技术,目前可以应用于macOS/iPadOS上的Streaming和WebRTC应用中,但是在其他设备上还需要根据上述的API来判断是否支持。如果希望web内容中的视频具备更好的浏览器兼容性,还是更推荐H264或者HEVC的编码格式,HEVC对高视频的支持更加完善。
3.3 Storage Access
在网页中播放来自第三方的视频内容是一种很常见的应用形态,比如要在main.domain的Web页面中播放来自video.domain的视频内容,通常有两种方式:
- 直接从video.domain获取内容。
- 创建一个iframe用于加载video.domain的内容。
但是出于安全考虑,由于IPT策略的限制,默认情况下第三方的iframe是没有权限访问宿主站点下的storage数据的。也就是说假如video.com的资源请求是从main.com发起的,这个请求就无法访问video.com域名下存储的cookies信息。这就意味着video.com在向授权用户提供资源的时候会出现问题,没有cookies就意味着无法通过认证。
这时候借助The Storage Access API向用户申请了授权,像这样:
那么第三方iframe就可以拿到宿主站点存储的cookies信息了。
这个The Storage Access API现有主流浏览器和webkit已经支持,具体用法如下:
document.hasStorageAccess().then(hasAccess => {
if (hasAccess) {
// storage access has been granted already.
} else {
// storage access hasn't been granted already;
// you may want to call requestStorageAccess().
}
});
为了增加适用范围,今年又新增了两个特性:
- 可以在per-page scope中申请用户授权,这样做的目的就是一旦用户对一个第三方iframe进行了授权,在同一页面上的所有其他资源也可以获得相同的访问授权,也就不用为每一个iframe都进行访问授权了。
- 允许嵌套在iframe中的iframe向宿主获取Cookies信息。
3.4 Media Recorder & Audio Worklet
这部分主要介绍如果通过Media Recorder在Web上实现录音功能,随后通过Audio Worklet实现音频的加工。下面这部分代码就是录音功能的简单实现。需要注意的在处理录音逻辑之前,需要首先通过navigator.mediaDevices.getUserMedia的方式向用户申请录音权限。
var recorder;
async function startRecording() {
try{
//await方式向用户获取录音权限
let stream = await navigator.mediaDevices.getUserMedia({ audio: true});
recorder = new MediaRecorder(stream);
recorder.addEventListener('dataavailable', onDataAvailable);
recorder.addEventListener('stop', onStop);
recorder.start();
} catch(error){
console.log(error);
}
}
function stopRecording() {
if(recorder) {
recorder.stop();
}
}
//create downlaodable data
var dataChunks = [];
function onDataAvailable(event) {
dataChunks.push(event.data);
}
function onStop() {
const blob = new Blob(dataChunks, {'type': 'audio/mp3'});
let audio = document.getElementById('audio');
audio.src = URL.createObjectURL(blob);
Audio Worklet API的作用是通过调用自定义脚本实现音频处理,这里的脚本可以是js或wasm。当前的Module和自定义的js之间通过AudioWorkletNode实现连接。与之前Safari中运行自定义脚本的解决方案ScriptProcessorNode相比,它减少了渲染线程和主线程之间的频繁切换,确保了更低延迟的实现音频处理。使用方法如下:
//使用AudioWorklet自定义语音处理脚本
let stream = await navigator.mediaDevices.getUserMedia({ audio: true});//获取用户授权
//process input data using AudioWorklet API
let audioContext = new AudioContext();
let source = audioContext.createMediaStreamSource(stream);//创建一个source
await audioContext.audioWorklet.addModule('distortion-processor.js');
const workletNode = new AudioWorkletNode(audioContext, 'distortion-processor');
let destination = audioContext.createMediaStreamDestination();
source.connect(workletNode).connect(destination);//把连接了自定义实现的workletNode和输出关联在一起
mediaRecorder = new MediaRecorder(destination.stream);
mediaRecorder.addEventListener('dataavailable', onDataAvailable);
mediaRecorder.addEventListener('stop', onStop);
mediaRecorder.start();
其中distortion-processor.js就是自定义的音频处理脚本,实现如下:
//audio processing script for AudioWorklet
//这个类必须继承自AudioWorkletProcessor,并且实现其中的process方法
class DistorationProcessor extends AudioWorkletProcessor {
process(inputs, outputs) {
const input = inputs[0];
const output = outputs[0];
for (let i = 0; i < output.length; ++i) {
output[i].set(input[i]);//实现自定义的音频处理方法,这里只是为了演示把数据取出来又重新放进去~~
}
return true;//返回true表示当前处理节点仍旧处于活跃状态,用户可以根据自己的业务逻辑确定是否关闭该节点。
}
}
registerProcessor('distortion-processor', DistorationProcessor);//全局注册一下,保证可以创建AudioWorkletNode
3.5 WebShare
通过WebShare API可以唤起系统原生的共享功能,在macOS和iOS系统上支持的渠道包括邮件、备忘录、短信、AirDrop等,但是在此之前由于只支持URL的共享,所以实用性并不是很强,也很少有Web页面会特地去使用这个功能。但是在最新版的Safari中增加了对文件共享的支持,包括图片、视频、音频在内多种形式的内容都可以被分享出去,关于分享渠道,除了前面提及的邮件等,还可以分享到微信、QQ等三方App,甚至可以通过Extension的形式为自己的App在系统的共享功能中增加入口,这样就可以实现Web页面内容的快速社交化分享了~,调用也很简单,通过navigator.canShare()判断是否支持共享,通过navigator.share唤起共享,具体如下:
function share() {
let file = new File([blob], 'memo.mp3');//这是使用前文Media Recorder API生成的音频文件
let filesArray = [file];//注意这里需要array类型的入参,意味着一次可以共享多个文件
if (navigator.canShare && navigator.canShare({files: filesArray})) {
navigator.share({
files: filesArray,
title: 'memo.mp3',
text:'I just created a really interesting recording!',
})
}
}
3.6 Speech Recognition
这是一项很酷的功能,简单来说就是在Web应用中实现语音到文本的实时转换,至于转换的准确率可以不用担心,因为这套API在macOS上采用的就是Siri引擎,同时支持多种语言,只需要在api中明确需要转换的语言类型即可。使用下面的方法就可以初始化并启动强大的识别功能了:
//start and stop speech recognition
var recognition;
function startRecognition(){
if (webkitSpeechRecognition) {
recognition = new webkitSpeechRecognition();
recognition.continuous = true;//要求识别持续进行,直到停止。
recognition.interimResults = true;//设置是否允许临时结果,临时结果是识别的中间过程,这时候返回结果的isFinal = false。
recognition.lang = 'cmn-Hans-CN'; //普通话 (中国大陆)
recognition.onresult = onRecognitionResult;//收到结果回调时执行的方法
recognition.onend = onRecognitionEnd;//识别结束时调用的方法
recognition.start();
}
}
function stopRecognition(){
if(recognition){
recognition.stop();
}
}
在demo中笔者尝试用Media Recorder录了一段语音,然后使用Speech Recognition进行转换,测试下来整体感觉翻译的很流畅,速度很快,准确率基本上没有问题,需要注意的是由于需要使用Siri引擎,所以要在系统偏好或设置中打开Siri或听写功能。具体的使用效果你们可以感受一下。
这部分功能看起来是比较值得期待的,语音输入作为一个交互入口,应该会有比较强的可用场景,比如语音搜索、在线笔记等。
3.7 MediaSession
当用户在Safari中播放音视频时,macOS的状态栏和iOS的负一屏就会出现一个Now Playing widget,但是点击这个widget后会发现其实它只是展示了一个网页,并没有其他的任何信息,不过现在通过media session API就可以在widget中增加更丰富的内容,比如播放进度、快进、快退、暂停操作等,总之media session API在Web应用和系统的其他组件之间实现了媒体状态的共享,这也是WWDC21 很重要的一部分内容,更详细的内容可以参考另外一个session: Coordinate media playback in Safari with Group Activities [5].
4关于Demo和调试
为了便于大家调试本文提及的部分功能,我把demo的代码放在了这里[6]。
可以使用Mac自带的Apache进行调试,调试的步骤如下:
- 运行 Apache $ sudo apachectl start
- 退出 Apache $ sudo apachectl stop
- 把工程文件夹放到以下位置中 /Library/WebServer/Documents
- 在浏览器中访问:在地址栏中输入地址 http://localhost/工程文件夹名称/,回车。
⚠注意: 不再需要使用后一定要记得退出,否则会消耗电脑性能。
5总结
为了增强用户体验和提高开发效率,Web开发近些年增加的亮点还是不少的,总体可以总结如下:
- JS语法增强是一些十分实用的小功能,对开发者来说增加了不少便利性。
- 在web页面中增加WebM & VP9的支持会是音视频网站的福音。
- 支持语音录制和编辑暂时想到的使用场景是在网页上进行便捷的语音搜索,更强的使用场景有待探索。
- 对Storage Access的改进是一个十分实用场景需求。
- 对WebGL2的支持算是为开发者省去了不少兼容性的烦恼。
希望对Web开发者有所帮助~