WebAssembly(簡稱Wasm),是一個安全、可移植(無硬體和作業系統相依)的低階語言(類似組合語言),由W3C制定標準,並由世界四大瀏覽器Mozilla Firefox、Microsoft Edge、Google Chrome和Apple Safari的提供商共同開發,能用來製作高效能且體積小的程式,通常是應用在Web上,但也不限於此。WebAssembly並不是利用新語法來編譯出優化過的JavaScript語法,而是一個嶄新的、可以在一次編譯後直接在各個主流網頁瀏覽器上執行的程式語言。效能敏感的程式若使用WebAssembly來開發,可以讓它的效能比原先用JavaScript開發還要好上幾倍,甚至是數十倍。
既然WebAssembly這麼厲害,那它會取代掉JavaScript嗎?
儘管JavaScript引擎再怎麼強大,JavaScript程式的效能始終無法和C/C++、Rust等程式語言針對CPU架構和作業系統製作出來的原生程式相提並論,因此WebAssembly的出現主要就是為了解決這個問題,讓程式就算在網頁瀏覽器上運行,也還是可以保有接近原生程式的效能。不過WebAssembly也並不是想要取代JavaScript,雖然WebAssembly的程式效能很好、體積也小,但在某些場合下,JavaScript也不見得會比它慢,而且JavaScript的程式碼通常會比WebAssembly原始碼編譯出來的二進制檔還要小很多,不用花費太多下載時間。
現階段在網頁瀏覽器或是Node.js上,WebAssembly編譯好的二進制檔會透過膠水代碼(glue code),以模組的形式載入到JavaScript中,其實就有點像是在Node.js使用C/C++的附加程式(Add-on)啦!因此或許WebAssembly最終會取代JavaScript,但那大概也是十年後的事了。
WebAssembly的程式碼
WebAssembly的程式碼檔案使用的副檔名為.wat
。
以下面這個JavaScript來舉例:
function addTwo(a, b) {
return a + b;
}
可以改用WebAssembly來完成相同的功能,如下:
(module
(func $addTwo (param i32 i32) (result i32)
get_local 0
get_local 1
i32.add)
(export "addTwo" (func $addTwo)))
WebAssembly的程式碼需要經過編譯才可以被網頁瀏覽器或是Node.js使用,編譯出來的二進制檔使用的副檔名為.wasm
。這種檔案現階段需要透過JavaScript的膠水代碼來載入到JavaScript中才能被執行,這些膠水代碼是由協助建置WebAssembly專案的工具自動產生,用來處理JavaScript層和WebAssembly層的轉換。這個部份會在之後的章節做更詳細的介紹。
上面的addTwo
範例可以在這個網頁中實際運行看看,網頁中也提供了其它的範例,都可以開來玩玩看。
我沒有學過組合語言,覺得WebAssembly的語法好難
對於沒有學過組合語言的人來說,WebAssembly類似組合語言的語法結構可能會讓他們有些困擾,不知道要從哪裡下手。
其實不會組合語言也沒關係,許多高階程式語言都可以用來開發WebAssembly的程式,其中就包括C、C++、Rust、Go、JavaScript和Python。而在本系列文章中,主要就會用Rust程式語言來開發WebAssembly的程式。
Rust + WebAssembly的開發環境
使用Rust程式語言來開發WebAssembly程式,並不只是單單引用某個或是某幾個crates.io上的套件來用就好了,還需要安裝JavaScript相關的工具,以及用來建置、測試及發佈WebAssembly程式專案的工具。
Rust程式語言
如果您還不熟悉Rust程式語言,甚至是連Rust開發環境都還沒有的話,請先參考筆者撰寫的《Rust學習之路》系列文章。
TypeScript程式語言
雖然不是必要,但本系列的教學會撰寫TypeScript程式來編譯出JavaScript程式。因為TypeScript要比JavaScript容易維護多了,最好儘可能地使用TypeScript。如果您還不熟悉TypeScript程式語言,甚至是連TypeScript開發環境都還沒有的話,請先參考筆者撰寫的《TypeScript學習之路》系列文章。
Webpack(Node.js)
本系列的教學還需要用到Webpack來打包WebAssembly的TypeScript/JavaScript程式,有關於Webpack的開發環境設定和基礎教學,可以參考這兩篇文章:
wasm-pack
wasm-pack
是Rust生態圈提供的WebAssembly開發工具。可以使用以下指令來安裝:
cargo-generate
cargo-generate
可以快速地從遠端的Git倉庫(repository)中下載Cargo程式專案,當作是新的Cargo程式專案的模板。可以使用以下指令來安裝:
加入編譯目標(target)
要使Rust程式能夠被編譯為.wasm
檔案,必須將編譯目標設為wasm32-unknown-unknown
。
以下指令可以加入wasm32-unknown-unknown
目標:
Hello World
學習一個程式語言,依照慣例,第一支程式都會是「Hello World」。現在,我們就來從無到有,一步步地從建立新的Cargo程式專案開始,一直到能夠在網頁瀏覽器上執行WebAssembly程式,並用對話框秀出Hello, world!
這幾個字吧!
建立專案目錄
首先,執行以下指令,用cargo-generate
建立出名叫hello-webassembly
的應用程式專案。
若您有興趣的話可以自行看一下wasm-pack-template
這個模板產生出來的程式碼,底下會繼續介紹程式的實作流程,就不多說明了。
wasm_bindgen
先用編輯器開啟src/lib.rs
,應該會看到有兩個部份的程式被加上#[wasm_bindgen]
屬性。如下:
...
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello, hello-webassembly!");
}
...
若在extern
關鍵字所組成的程式區塊加上#[wasm_bindgen]
屬性,表示要將這個區塊裡面定義的函數連結到JavaScript提供的函數。
如果在有使用pub
來定義的函數加上#[wasm_bindgen]
屬性,表示要暴露(export)這個函數到JavaScript中。
Rust程式實作
為了能在對話框秀出Hello, world!
,我們要將程式修改如下:
...
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello, world!");
}
...
建置專案
寫完Rust程式後,就可以執行以下指令將它編譯成.wasm
檔案了!
編譯後產生出來的東西會是一個完整的JavaScript套件,檔案會被放在Cargo程式專案根目錄的pkg
目錄中,列表如下:
hello_webassembly_bg.js
:這個就是前面提到過的膠水代碼。hello_webassembly_bg.wasm
:看副檔名就知道這個就是WebAssembly的二進制檔。hello_webassembly_bg.wasm.d.ts
:TypeScript定義檔,可以使WebAssembly的二進制檔(WASM模組)被用於TypeScript中。hello_webassembly.js
:用來引用膠水代碼和定義其它JS相關的東西,我們主要會引用這個檔案來做東西。hello_webassembly.d.ts
:這個是hello_webassembly.js
的TypeScript定義檔,可以使膠水代碼用於TypeScript中。package.json
:這個JavaScript套件的設定檔。README.md
:這個就是Cargo程式專案根目錄README.md
,wasm-pack
只是把它複製過來而已。
要怎麼把這些檔案套用在HTML網頁?
我們可以在Cargo程式專案目錄中,新增一個www
目錄,然後在這個目錄中參考以下連結的文章來建立TypeScript函式庫專案:
在TypeScript專案中執行以下指令來安裝必要套件:
修改TypeScript程式碼,如下:
export { greet } from "../../pkg/hello_webassembly.js";
新增www/src/index.ts
檔案,內容如下:
import { greet } from "./lib.js";
greet();
新增www/views/index.html
檔案,內容如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Greeting</title>
</head>
<body>
You should see an alert message.
<script src="./js/bundle.min.js"></script>
</body>
</html>
新增www/webpack.config.ts
檔案,即Webpack的設定檔,內容如下:
import HtmlWebpackPlugin from "html-webpack-plugin";
import TerserPlugin from "terser-webpack-plugin";
import { Configuration } from "webpack";
const config: Configuration = {
entry: "./src/index.ts",
output: {
clean: true,
filename: "./js/bundle.min.js",
},
plugins: [
new HtmlWebpackPlugin({
template: "./views/index.html",
filename: "./index.html",
minify: {
collapseBooleanAttributes: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
removeEmptyAttributes: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
minifyCSS: true,
minifyJS: true,
sortAttributes: true,
useShortDoctype: true,
},
inject: false,
}),
],
module: {
rules: [
{
test: /\.ts$/i,
use: [
{
loader: "babel-loader",
options: { presets: ["@babel/preset-env", "@babel/preset-typescript"] },
},
],
},
{
test: /\.js$/i,
use: [
{
loader: "babel-loader",
options: { presets: ["@babel/preset-env"] },
},
],
},
],
},
resolve: { extensionAlias: { ".js": [".ts", ".js"] } },
optimization: {
minimizer: [
new TerserPlugin({
extractComments: false,
terserOptions: { format: { comments: false } },
}),
],
},
experiments: { asyncWebAssembly: true },
};
export default config;
修改www/.eslintrc
檔案,將www/webpack.config.ts
加進import/no-extraneous-dependencies
規則的devDependencies
的例外中。
在www/package.json
中添加腳本:
{
...
"scripts": {
...
"build:webpack": "webpack --mode production",
...
}
...
}
然後就可以在www
目錄中執行npm run build:webpack
指令來打包專案。接著再使用VSCode的Live Server開啟Webpack輸出的index.html
,正常情況下會成功在網頁瀏覽器看到Hello, world!
對話框。
這邊要注意的是,在Webpack設定檔中要將實驗性的WebAssembly功能開啟。Webpack本身就有支援.wasm
檔案的打包,所以可不必替.wasm
檔案撰寫載入器規則。Webpack在輸出.wasm
檔案時,會將檔名被替換成雜湊值。
如果要更變.wasm
檔案的輸出檔名,或是輸出路徑的話,可以在Webpack設定的output
欄位再加上webassemblyModuleFilename
欄位,這個欄位的值預設為./[modulehash].module.wasm
,把它指定成我們要的路徑和檔名即可。例如:
module.exports = {
...
output: {
...
webassemblyModuleFilename: "./wasm/game-of-life.wasm",
...
}
...
};
總結
在這個章節中,我們稍微瞭解了WebAssembly,以及WebAssembly和JavaScript的關聯。並且也學會了從無到有,用Rust程式語言和Webpack來製作出應用了WebAssembly的網頁。
在接下來的章節中,會以更多例子來介紹用Rust程式語言開發WebAssembly程式的方式。
下一章:將高效能的Rust函式庫套用在網頁上。