高階Web應用開發前沿技術簡述

語言: CN / TW / HK

作者|毛小俊,單位:中國移動智慧家庭運營中心

​Labs 導讀

Web應用作為網際網路內容的重要組成部分。隨著Web2.0概念的蓬勃發展和包括WebAssembly、WebGL2.0等技術的演進,Web應用在很多場景下已經具備和原生相媲美的效能,近些年Web應用又有了哪些新的進展,Safari和Webkit 中有一些怎樣有趣又好玩的功能呢,讓我們一起來探索一下吧~

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){
  /*
  當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之間的依賴管理變得更加簡單,比如像下面這樣:

<!-- wait until module is imported with top-level await-->
<script type='module' id='inline-module'>
    var stopwatch;
    try{
        let {StopWatchWithOneButton} = await import('./stopwatchInModule.js');
        stopwatch = new StopWatchWithOneButton();

    } catch(error){
        console.log(error);
    }
</script>

//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開發近些年增加的亮點還是不少的,總體可以總結如下:

  1. JS語法增強是一些十分實用的小功能,對開發者來說增加了不少便利性。
  2. 在web頁面中增加WebM & VP9的支援會是音視訊網站的福音。
  3. 支援語音錄製和編輯暫時想到的使用場景是在網頁上進行便捷的語音搜尋,更強的使用場景有待探索。
  4. 對Storage Access的改進是一個十分實用場景需求。
  5. 對WebGL2的支援算是為開發者省去了不少相容性的煩惱。

希望對Web開發者有所幫助~