本文做者:劉觀宇,360奇舞團高級前端工程師、技術經理,曾參與360導航、360影視、360金融、360遊戲等多個大型前端項目。關注W3C標準、IOT、機器學習的最新進展,現爲W3C CSS工做組成員。javascript
Web應用的蓬勃發展,使得JavaScript、Web前端,乃至整個互聯網都發生了深入的變化。前端開始承擔起了更多的職責,因而對於執行效率的訴求也就更爲急迫。除了在語言自己的進化,Web從業者以及各大瀏覽器廠商,也在不停地進行探索。2012年Mozillia的工程師提出了Asm.js和Emscripten,使得C/C++以及多種編程語言編寫的高效程序轉譯爲JavaScript並在瀏覽器運行成爲可能。html
更進一步地,WebAssembly(簡稱wasm)技術被提出,並迅速成立了各類研發組織,各類周邊工具鏈的不斷完善,相關實驗數據也有力地佐證了這條優化和加速路線的可行性。前端
特別是2018年,W3C的WebAssembly工做組發佈了第一個工做草案,包含了核心標準、JavaScript API以及Web API。另外,除了C/C++和Rust以外,Golang語言也正式支持了wasm的編譯。咱們罕見的看到,各大主流瀏覽器一致表示支持這一新的技術,也許一個嶄新的Web時代即將到來。java
打開wasm的官網,咱們能夠看到其宏偉的技術目標。除了定義一個可移植、精悍、載入迅捷的二進制格式以外,還有對移動設備、非瀏覽器乃至IoT設備支持的規劃,而且還會逐步創建一系列工具鏈。感興趣的讀者,能夠從這裏看到wasm官方的闡述。node
簡單的說,wasm並非一種編程語言,而是一種新的字節碼格式,目前,主流瀏覽器都已經支持 wasm。與 JavaScript 須要解釋執行不一樣的是,wasm字節碼和底層機器碼很類似可快速裝載運行,所以性能相對於 JavaScript 解釋執行有了很大的提高。webpack
下面這張圖,展現了目前(2018年7月)主流瀏覽器對於wasm的支持狀況。git
除了在瀏覽器上能夠運行外,目前wasm已經能夠在包括NodeJS等命令行環境下運行。github
按照最初的設想,各類高級語言經過本身的前端編譯工具,將本身的源代碼編譯成爲底層虛擬機(LLVM)可識別的中間語言表示(LLVM IR)。此時,底層的LLVM能夠將LLVM IR根據不一樣的CPU架構生成不一樣的機器碼,同時能夠對這些機器碼進行編譯時的空間與性能的優化。大多數的高級語言都是按照這樣的結構來支持wasm的。上述提到的兩個步驟,也依次被成爲編譯器前端和編譯器後端。web
編譯到wasm的代碼,是最終進行實際工做的程序。對此,有一種名爲S-表達式的文本格式,擴展名爲.wast,以方便程序猿閱讀。藉助wabt工具鏈能夠實現wasm和wast的互轉。一個S-表達式形如:typescript
(module
(type $iii (func (param i32 i32) (result i32)))
(memory $0 0)
(export "memory" (memory $0))
(export "add" (func $assembly/module/add))
(func $assembly/module/add (; 0 ;) (type $iii) (param $0 i32) (param $1 i32) (result i32)
;;@ assembly/module.ts:2:13
(i32.add
;;@ assembly/module.ts:2:9
(get_local $0)
;;@ assembly/module.ts:2:13
(get_local $1)
)
)
)
複製代碼
目前已經有多種高級語言支持對wasm的編譯,特別是AssemblyScript,這種以TypeScript爲基礎語言,經過AssemblyScript的工具鏈支持,能夠完成最終到wasm的轉換。
根據上述架構,瀏覽器以及各類運行環境提供者,各自經過提供不一樣的運行支持以抹平各個CPU架構不一樣形成的差別,使得須要支持wasm高級語言,只須要支持編譯到中間語言表示層。能夠預見的是,隨着開發環境的溫馨度逐步提升,愈來愈多的高級語言也會加入支持wasm的陣營。
下面的實踐,咱們須要藉助AssemblyScript來完成,AssemblyScript定義了一個TypeScript的子集,意在幫助TS背景的同窗,經過標準的JavaScript API來完成到wasm的編譯,從而消除語言的差別,讓程序猿能夠快樂的編碼。
AssemblyScript項目主要分爲三個子項目:
這裏須要說明的是,目前工具鏈還在開發過程當中,個別步驟可能還不太穩定。咱們儘可能保證安裝配置過程的嚴謹,若是遇到有變更,請以官方描述爲準。
爲了支持編譯,咱們首先須要安裝AssemblyScript的支持。爲了編譯的順利進行,首先須要保證你的Node版本在8.0以上。同時,你須要安裝好TypeScript運行環境。
下面讓咱們開始吧:
爲了不後面依賴的問題,咱們首先安裝AssemblyScript支持
git clone https://github.com/AssemblyScript/assemblyscript.git
cd assemblyscript
npm install
npm link
複製代碼
執行上述命令後,你可使用命令asc
來斷定是否安裝正確。若是正常安裝,命令行會顯示asc命令的使用說明。
接下來,咱們新建一個NPM項目,如:wasmExample。若是須要,能夠加入ts-node和typescript的devDependencies,並安裝好依賴。 而後,在項目根目錄下,咱們新建一個目錄:assembly。 咱們進入assembly目錄,同時咱們在這裏加入tsconfig.json,內容以下:
{
"extends": "../node_modules/assemblyscript/std/assembly.json",
"include": [
"./**/*.ts"
]
}
複製代碼
下面,咱們在這個目錄下加入簡單的ts代碼,以下:
export function add(a: i32, b: i32): i32 {
return a + b;
}
複製代碼
咱們把上面這段TypeScript代碼存儲爲:module.ts。 那麼,如今從項目根目錄來看,咱們的文件結構以下圖:
爲了後面運行簡便,咱們把build步驟加入到npm scripts裏面,方法是打開項目根目錄的package.json,更新scripts字段爲:
"scripts": {
"build": "npm run build:untouched && npm run build:optimized",
"build:untouched": "asc assembly/module.ts -t dist/module.untouched.wat -b dist/module.untouched.wasm --validate --sourceMap --measure",
"build:optimized": "asc assembly/module.ts -t dist/module.optimized.wat -b dist/module.optimized.wasm --validate --sourceMap --measure --optimize"
}
複製代碼
爲了項目整潔,咱們把編譯目標放到項目根目錄的dist文件夾,此時,咱們須要在項目根目錄下新建dist目錄。
如今,在項目根目錄下,咱們來運行:npm run build
若是沒有報錯的話,你會看到,在dist目錄下生成了6個文件。
咱們先沒必要深究文件的具體內容。此時,咱們的編譯工做已經作好。細心的讀者可能看到了,在上面的編譯命令裏面使用了不一樣的參數。這些參數,咱們能夠直接在命令行下鍵入asc
來查詢命令以及參數的使用細節。
如今咱們有了編譯的結果。目前,因爲wasm還只能由JavaScript引入,所以咱們還須要將編譯出的wasm引入到JavaScript程序中。
咱們在項目根目錄加入一個module引入代碼:module.js,以下:
const fs = require("fs");
const wasm = new WebAssembly.Module(
fs.readFileSync(__dirname + "/dist/module.optimized.wasm"), {}
);
module.exports = new WebAssembly.Instance(wasm).exports;
複製代碼
同時,咱們須要一個使用module的代碼。如:index.js,以下:
var myModule = require("./module.js");
console.log(myModule.add(1, 2));
複製代碼
激動人心的時刻到了,咱們在項目根目錄下運行node index.js
,看看結果是否正如咱們所期待。讀者自行能夠修改index.js裏面的調用數據,來測試模塊的正確性。須要注意的是,由於是wasm是有數據類型概念的,並且數據類型比TypeScript 更爲精確。因此,上面的例子中,若是輸入的不是整數(上例指wasm定義的i32),會和傳統的JavaScript結果不一致,好比你的調用是myModule.add(2.5, 2)
,結果多是4。所以,咱們須要在調用wasm程序時候,嚴格關注數據類型。
上面的段落,咱們展現瞭如何與NodeJS整合,其實對於效率提高更爲顯著的,當屬在瀏覽器中。那麼如何在瀏覽器中使用咱們編譯好的代碼呢?
對於JavaScript調用wasm,通常採用以下步驟:
完整的步驟,也能夠參見下面的流程圖:
這裏提供一個異步代碼的例子,咱們將其命名爲async_module.js:
// 異步引入例子
const fs = require("fs");
const readFile = require("util").promisify(fs.readFile);
const getInstance = async (wasm, importObject={}) => {
let buffer = new Uint8Array(wasm)
return await WebAssembly.instantiate(wasm, importObject)
}
let ins;
const noop = () => {};
const exportFun = (obj, funName) => {
return (typeof obj[funName] === "function")
? obj[funName] : noop;
}
async function getModuleFun(filePath, funName ,importObject={}) {
if (ins){
return exportFun(ins, funName)
}
const wasmText = await readFile(filePath);
const mod = await getInstance(wasmText, importObject);
return exportFun(mod.instance.exports, funName)
}
module.exports = getModuleFun;
複製代碼
調用時候,咱們只需以下代碼,便可愉快地利用wasm對象進行編碼了:
var myModule = require("./async_module.js");
// 調用代碼
(async () => {
const fun = await myModule(__dirname + "/dist/module.optimized.wasm", "add")
console.log(fun(1, 2))
console.log(fun(4, 10000))
})()
複製代碼
這裏是目前所有的JavaScript中與wasm協做的API說明
從webpack4開始,官方提供了默認的wasm的加載方案。若是你的webpack是webpack4之前的版本,可能須要安裝諸如assemblyscript-typescript-loader
等開發包。
筆者目前所使用的webpack版本爲:4.16.2,對於wasm的原生支持已經比較完善。根據官方的信息,以後的webpack5,會對wasm進行更爲穩健的支持。
以下代碼便可簡單的引入wasm模塊,運行npx webpack
能夠將代碼自動編譯:
import("./module.optimized.wasm").then(module => {
const container = document.createElement("div");
container.innerText = "Hello, WebAssembly.";
container.innerText += " add(1, 2) is " + module.add(1, 2);
document.body.appendChild(container);
});
複製代碼
因爲wasm目前不能直接操做Dom,若是須要這種操做,可能須要藉助JavaScript的能力,這種狀況下,咱們須要在wasm中調用JavaScript。
WebAssembly.instance 和 WebAssembly.instantiate 函數均支持第二個參數 importObject,這個importObject 參數的做用就是 JavaScript 向 wasm 傳入須要調用的JavaScript模塊。
做爲演示,咱們把上面的module.js代碼修改一下,把相加的結果,用「*」的個數來表示。這裏咱們爲了演示方便,依然使用同步代碼,實際上,異步代碼更爲經常使用。
const fs = require("fs");
const wasm = new WebAssembly.Module(
fs.readFileSync(__dirname + "/dist/module.optimized.wasm"), {}
);
module.exports = new WebAssembly.Instance(wasm, {
window:{
show: function (num){
console.log(Array(num).fill("*").join(""))
}
}
}).exports;
複製代碼
調用方index.js
修改成:
var myModule = require("./module.js");
myModule.add(1, 2);
複製代碼
同時,咱們須要修改TypeScript源碼:
// 聲明從外部導入的模塊類型
declare namespace window {
export function show(v: number): void;
}
export function add(a: i32, b: i32): void {
window.show(a + b);
}
複製代碼
咱們回到項目根目錄,從新運行npm run build
。
以後,運行node index.js
咱們看到,原來的結果,改成用*的個數來表示了。說明WebAssembly調用JavaScript代碼成功。
對於wasm技術,咱們總結以下:
儘管如此,筆者仍然很是看好wasm的前景,在性能要求很高的如遊戲、影音應用等領域,或許會有不錯的發展。
本文選題過程,參考了安佳、李鬆峯、劉宇晨等同事的建議。成文後,李鬆峯老師和劉宇晨給出了不少中肯的修訂意見,在此一併表示誠摯的謝意。
《奇舞週刊》是360公司專業前端團隊「奇舞團」運營的前端技術社區。關注公衆號後,直接發送連接到後臺便可給咱們投稿。