作者:毛小俊
单位:中国移动智慧家庭运营中心
Tips:
本文知识目录:
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得知弱引用的对象被垃圾回收的时机,然后在注册的回调中执行一些清理操作。
其中关键的几个概念:
下面是一段伪代码:
class StopWatchWithOneButton {
_startTime = 0;
click(){
//...
}
//some detAIl implimentation...
}
const allStopWatches = new Map();
var nextAvailableIdentifier = 1;
function removeStopwatch(identifier){
/*
当map中引用的StopWatchWithOneButton对象由于某种原因(生命周期结束/手动销毁)被系统回收后,
需要将当前的Map数据清理一下。
*/
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));
/*将stopwatch注册到finalizationRegistry这个注册表中,当stopwatch被垃圾回收时,
便会调用上面的removeStopwatch函数,实现allStopWatches这个map数据的清理。
*/
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之间的依赖管理变得更加简单,比如像下面这样:
上述await方法的使用,有以下两个效果:
1️⃣stopwatch = new StopWatchWithOneButton();会在import执行完成之后再执行。
2️⃣如果被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.Segmenter,可以实现语句的分词功能,在做一些算法的时候进行分词是一项基本的工作,在此基础之上可以做很多有趣的功能,更详细的代码参见demo,感兴趣的同学不妨一试。
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增加了多项功能的支持,具体包括在以下几个方面:
这部分主要是介绍部分新颖的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的基础上增加了很多的新特性,比如:
由于旧版本的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的支持。
由于不同设备对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的视频内容,通常有两种方式:
1.直接从video.domain获取内容。
2.创建一个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().
}
});
为了增加适用范围,今年又新增了两个特性:
1.可以在per-page scope中申请用户授权,这样做的目的就是一旦用户对一个第三方iframe进行了授权,在同一页面上的所有其他资源也可以获得相同的访问授权,也就不用为每一个iframe都进行访问授权了。
2.允许嵌套在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].
为了便于大家调试本文提及的部分功能,我把demo的代码放在了这里[6]。
可以使用Mac自带的Apache进行调试,调试的步骤如下:
⚠注意: 不再需要使用后一定要记得退出,否则会消耗电脑性能。
为了增强用户体验和提高开发效率,Web开发近些年增加的亮点还是不少的,总体可以总结如下:
希望对Web开发者有所帮助~
参考文献
[1]https://developer.apple.com/safari/technology-preview/
[2]https://developer.mozilla.org/en-US/
[3]https://www.funkykarts.rocks/demo.html
[4]https://webassembly.org/roadmap/
[5]https://developer.apple.com/videos/play/wwdc2021/10189/
[6]https://gitee.com/lysqj/wwdc2021/tree/develop/