WebAssembly(Wasm)是 Web 瀏覽器中相對較新的功能,但它地擴展了把 Web 作為服務應用平台的功能潛力。
對於 Web 開發人員來說,學習使用 WebAssembly 可能會有一個艱難的過程,但是 AssemblyScript 提供了一種解決方法。首先讓我們看一下為什么 WebAssembly 是一項很有前途的技術,然后再看怎樣 AssemblyScript 挖掘潛力。
WebAssembly
WebAssembly 是瀏覽器的低級語言,為開發人員提供了除 JavaScript 之外的 Web 編譯目標。它使網站代碼可以在安全的沙盒環境中以接近本機的速度運行。
它是根據所有主流瀏覽器(Chrome,Firefox,Safari 和 Edge)所代表的意見開發的,他們達成了設計共識,這些瀏覽器現在都支持 WebAssembly。
WebAssembly 以二進制格式交付,這意味着與 JavaScript 相比,WebAssembly 在大小和加載時間上都具有優勢。但是它也有易於理解的文本表示形式。
當 WebAssembly 首次發布時,一些開發人員認為它有可能最終取代 JavaScript 作為 Web 的主要語言。但是最好把 WebAssembly 看作是與現有 Web 平台良好集成的新工具,這是它的高級目標。
WebAssembly 並沒有取代 JavaScript 現有的用例,而是吸引了更多人,因為它引入了新的用例。 目前 WebAssembly 還不能直接訪問 DOM,大多數網站都希望使用 JavaScript,經過多年的優化,JavaScript 已經相當快了。以下 WebAssembly 可能的使用案例列表的示例:
- 游戲
- 科學的可視化和模擬
- CAD應用
- 圖像/視頻編輯
這些應用共同特點是,它們通常會被看作是桌面應用。通過為 CPU 密集型任務提供接近本機的性能,WebAssembly 使得將這些程序遷移至 Web 成為可行。
現有網站也可以從 WebAssembly 中受益。 Figma(https://www.figma.com/) 提供了一個真實的例子,它通過使用 WebAssembly 大大縮短了其加載時間。如果網站使用進行大量計算的代碼,則可以將其替換為 WebAssembly 以提高性能。
也許現在你對怎樣使用 WebAssembly 感興趣。你可以學習語言本身並直接編寫,但實際上它打算成為其他語言的編譯目標。它被設計為對 C 和 C++ 具有良好的支持,Go語言在 version 1.11 中增加了實驗性支持的版本中,Rust 也對其進行了大量投入。
但是也許你並不想為了使用 WebAssembly 而學習或使用其中某種語言。這就是 AssemblyScript 存在的意義。
AssemblyScript
AssemblyScript 是一個把 TypeScript 轉換到 WebAssembly 的編譯器。由微軟開發的 TypeScript 將類型添加到了 JavaScript 中。它已經變得相當受歡迎,即使對於不熟悉它的人,AssemblyScript 只允許 TypeScript 的有限功能子集,因此不需要花太多時間就可以上手。。
因為它與 JavaScript 非常相似,所以 AssemblyScript 使 Web 開發人員可以輕松地將 WebAssembly 整合到他們的網站中,而不必使用完全不同的語言。
試用
讓我們編寫第一個 AssemblyScript 模塊(以下所有代碼均可在 GitHub 上找到)。我們需要 Node.js 的最低版本為 8 才能得到 WebAssembly 的支持。 轉到一個空目錄,創建一個 package.json 文件,然后安裝 AssemblyScript。請注意,我們需要直接從它的 GitHub 存儲庫安裝。它尚未在 npm 上發布,因為 AssemblyScript 開發人員還沒有考慮編譯器是否已經准備好能夠支持廣泛使用。
mkdir assemblyscript-demo cd assemblyscript-demo npm init npm install --save-dev github:AssemblyScript/assemblyscript
使用 asinit 命令生成腳手架文件:
npx asinit .
我們的 package.json 現在應該包含以下腳本:
{
"scripts": { "asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --sourceMap --validate --debug", "asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --sourceMap --validate --optimize", "asbuild": "npm run asbuild:untouched && npm run asbuild:optimized" } }
頂層的 index.js 看起來像這樣:
const fs = require("fs"); const compiled = new WebAssembly.Module(fs.readFileSync(__dirname + "/build/optimized.wasm")); const imports = { env: { abort(_msg, _file, line, column) { console.error("abort called at index.ts:" + line + ":" + column); } } }; Object.defineProperty(module, "exports", { get: () => new WebAssembly.Instance(compiled, imports).exports });
它使我們能夠像使用普通的 JavaScript 模塊一樣輕松地 require WebAssembly 模塊。
assembly 目錄中包含我們的 AssemblyScript 源代碼。生成的示例是一個簡單的加法函數。
export function add(a: i32, b: i32): i32 { return a + b; }
函數簽名就像在 TypeScript 中那樣,它之所以使用 i32 的原因是 AssemblyScript 使用了 WebAssembly 的特定整數和浮點類型,而不是 TypeScript 的通用 number 類型。
讓我們來構建示例。
npm run asbuild
build 目錄現在應包含以下文件:
optimized.wasm optimized.wasm.map optimized.wat untouched.wasm untouched.wasm.map untouched.wat
我們得到了構建的普通版本和優化版本。對於每個構建版本,都有一個 .wasm 二進制文件,一個 .wasm.map 源碼映射,以及二進制文件的 .wat 文本表示形式。文本表示形式是為了供人閱讀,但現在我們無需閱讀或理解它——使用 AssemblyScript 的目的之一就是我們不需要使用原始 WebAssembly。
啟動 Node 並像其他模塊一樣使用編譯模塊。
$ node
Welcome to Node.js v12.10.0. Type ".help" for more information. > const add = require('./index').add; undefined > add(3, 5) 8
這就是從 Node 調用 WebAssembly 所需要的全部!
添加監視腳本
為了便於開發,我建議你在每次更改源代碼時都用onchange 自動重建模塊,因為 AssemblyScript 尚不包括監視模式。
npm install --save-dev onchange
在 package.json 中添加一個 asbuild:watch 腳本。包含 -i flag,即可在運行命令后立即運行初始構建。
{
"scripts": { "asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --sourceMap --validate --debug", "asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --sourceMap --validate --optimize", "asbuild": "npm run asbuild:untouched && npm run asbuild:optimized", "asbuild:watch": "onchange -i 'assembly/**/*' -- npm run asbuild" } }
現在你可以運行 asbuild:watch,而不必不斷地重新運行 asbuild。
性能
讓我們寫一個基本的基准測試,用來了解究竟可以獲得什么樣的性能提升。 WebAssembly 的專長是處理諸如數字計算之類的 CPU 密集型任務,所以我們用一個函數來確定整數是否為質數。
我們的參考實現如下所示。這是一種幼稚的暴力解決方案,因為我們的目標是執行大量計算。
function isPrime(x) { if (x < 2) { return false; } for (let i = 2; i < x; i++) { if (x % i === 0) { return false; } } return true; }
等效的 AssemblyScript 版本僅需要一些類型注釋:
function isPrime(x: u32): bool { if (x < 2) { return false; } for (let i: u32 = 2; i < x; i++) { if (x % i === 0) { return false; } } return true; }
我們將使用 Benchmark.js。
npm install --save-dev benchmark
創建benchmark.js:
const Benchmark = require('benchmark'); const assemblyScriptIsPrime = require('./index').isPrime; function isPrime(x) { for (let i = 2; i < x; i++) { if (x % i === 0) { return false; } } return true; } const suite = new Benchmark.Suite; const startNumber = 2; const stopNumber = 10000; suite.add('AssemblyScript isPrime', function () { for (let i = startNumber; i < stopNumber; i++) { assemblyScriptIsPrime(i); } }).add('JavaScript isPrime', function () { for (let i = startNumber; i < stopNumber; i++) { isPrime(i); } }).on('cycle', function (event) { console.log(String(event.target)); }).on('complete', function () { const fastest = this.filter('fastest'); const slowest = this.filter('slowest'); const difference = (fastest.map('hz') - slowest.map('hz')) / slowest.map('hz') * 100; console.log(`${fastest.map('name')} is ~${difference.toFixed(1)}% faster.`); }).run();
在我的機器上,運行 node benchmark 時得到了以下結果:
AssemblyScript isPrime x 74.00 ops/sec ±0.43% (76 runs sampled) JavaScript isPrime x 61.56 ops/sec ±0.30% (64 runs sampled) AssemblyScript isPrime is ~20.2% faster.
請注意,這個測試是一個 microbenchmark,我們應該謹慎閱讀。
對於一些更多的 AssemblyScript 基准測試,我建議你查看 WasmBoy 基准測試和波動方程式基准測試。
資源搜索網站大全 http://www.szhdn.com 廣州VI設計公司https://www.houdianzi.com
加載模塊
接下來,在網站中使用我們的模塊。
先創建 index.html:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>AssemblyScript isPrime demo</title> </head> <body> <form id="prime-checker"> <label for="number">Enter a number to check if it is prime:</label> <input name="number" type="number" /> <button type="submit">Submit</button> </form> <p id="result"></p> <script src="demo.js"></script> </body> </html>
再創建 demo.js。加載 WebAssembly 模塊有多種方式,但是最有效的方法是通過使用 WebAssembly.instantiateStreaming 函數以流的方式編譯和實例化。請注意,如果 assertion 失敗的話,我們需要提供 abort 函數。
(async () => { const importObject = { env: { abort(_msg, _file, line, column) { console.error("abort called at index.ts:" + line + ":" + column); } } }; const module = await WebAssembly.instantiateStreaming( fetch("build/optimized.wasm"), importObject ); const isPrime = module.instance.exports.isPrime; const result = document.querySelector("#result"); document.querySelector("#prime-checker").addEventListener("submit", event => { event.preventDefault(); result.innerText = ""; const number = event.target.elements.number.value; result.innerText = `${number} is ${isPrime(number) ? '' : 'not '}prime.`; }); })();
現在安裝 static-server。因為要使用WebAssembly.instantiateStreaming,我們需要創建服務,該模塊需要使用 MIME type 的 application/wasm。
npm install --save-dev static-server
將腳本添加到 package.json 中。
{
"scripts": { "serve-demo": "static-server" } }
運行 npm run serve-demo 並在瀏覽器中打開 localhost URL。提交表單中的數字,你將收到一條消息,指出該數字是否為素數。現在,我們已經實現了從用 AssemblyScript 編碼到在網站中實際使用的整個過程。
結論
WebAssembly 以及通過 AssemblyScript 的擴展,不會使每個網站都神奇地變得更快,但是這並不重要。 WebAssembly 之所以令人興奮,是因為它可以使更多的應用在 Web 變得中可行。
類似地,AssemblyScript 使更多開發人員可以使用 WebAssembly,這使我們很容易默認使用 JavaScript,但是當需要大量運算工作時,可以用 WebAssembly。
