0%

使用 Golang WebAssembly 創建一個 TODO 應用

前言

在本文中,我們將使用 Golang WebAssembly 創建一個簡單的 TODO 應用,完全使用 Golang 來操作 DOM,整個代碼的複雜度不高,但足以用來學習 Golang 撰寫 WebAssembly 的基本技巧。

成果

環境

  • go version go1.19.3 linux/amd64

讓開發順利的前置作業

若是使用 Visual Studio Code 開發 WebAssembly,大概率會遇到 could not import syscall/js 的提示,這是因為 syscall/js 是在 wasm 架構上才能引用的,而 VSCode 預設不會將 wasm 視為編譯的目標架構 (GOARCH),這時候需要在 .vscode 目錄中創建如下內容的 settings.json,讓 VSCode 知道我們的目標架構是 wasm

.vscode/settings.json
1
2
3
4
5
6
7
8
{
"gopls": {
"build.env": {
"GOOS": "js",
"GOARCH": "wasm"
}
}
}

TODO 應用原始碼說明

Source Code On GitHub 提供完整的程式碼可以參考,原始碼中有非常詳細的註解,請讀者直接閱讀註解來學習,這裡僅說明 syscall/js 相關函數的使用方法。

js.Value

對 DOM 操作取得的資料 (包含 Element, 資料等等) 為 js.Value 型態。例如:js.Global() 為取得 window 物件 (在 Browser 環境) 或 global 物件 (在 Node 環境) 的 Function,回傳的型態為 js.Value

js.Value.Get()

js.Value.Get() 可以取得該物件的屬性 (Property)。

例如:js.Global().Get("document") 就是取得 window 物件的 document 屬性,即等同 JavaScript 的 window.document

1
document = js.Global().Get("document")

js.Value.Call()

js.Value.Call() 可以呼叫該物件的方法 (Method),並提供輸入參數。

例如:document.Call("getElementById", "root") 就是呼叫 document 物件的 getElementById Method 並將 "root" 作為該 Method 的輸入參數,即等同 JavaScript 的 document.getElementById("root")

1
root = document.Call("getElementById", "root")

js.Value.Set()

js.Value.Set() 可以設定該物件的屬性 (Property),並提供要設定的值。

例如:header.Set("textContent", "Golang Web Assembly for TODO :)") 就是設定該 Element 的 Property textContent"Golang Web Assembly for TODO :)",即等同 JavaScript 的 header.textContent = "Golang Web Assembly for TODO :)"

1
header.Set("textContent", "Golang Web Assembly for TODO :)")

js.Value.Bool()

當確定 js.ValueBool 型態時,可以透過 js.Value.Bool() 將其轉換為 Bool

例如:event.Get("currentTarget").Get("checked").Bool() 就是將 event.currentTarget.checked 的值轉為 Bool

1
todo.Completed = event.Get("currentTarget").Get("checked").Bool()

當然還有像 js.Value.String(), js.Value.Int() 等等取值的 Methods 可以使用,這裡就不一一列舉。要注意的一點是若轉換失敗會 panic,例如:該 js.Value 不是 Bool,卻使用 Bool() Method 取值則會 panic

js.FuncOf

當需要將 Golang 的 Function 暴露給 Browser 使用時,必須使用 js.FuncOf 將符合以下簽名形式的 Function 包裝起來

1
func(this js.Value, args []js.Value) any
  • this: 能透過此參數使用 JavaScript 中的 this 物件。
  • args: 呼叫該 Function 時所帶的輸入參數。
  • 回傳值:從 Golang 回傳給 Browser 的值。

原始碼中會看到每個 addEventListener 都是使用這樣的 Function,例如

1
2
3
4
5
6
7
8
9
checkBox.Call("addEventListener", "change", js.FuncOf(func(this js.Value, args []js.Value) any {
// Get the event object.
event := args[0]
// Set the task Completed property to the value of the checkbox.
todo.Completed = event.Get("currentTarget").Get("checked").Bool()
// Inform the app that the screen should be re-rendered.
changed <- struct{}{}
return nil
}))

編譯

指定 GOARCH=wasmGOOS=js,同時將編譯出的 wasm 檔案存到 ./build/ 目錄下且命名為 main.wasm

1
GOARCH=wasm GOOS=js go build -o ./build/main.wasm

index.html

導入 main.wasmhtml 使用前,還有一件重要的事要先做,也就是需要先引用 wasm_exec.js (它的位置在 $(go env GOROOT)/misc/wasm/wasm_exec.js,可以將它複製到目標目錄下),我們才可以執行 new Go(),再透過 WebAssembly.instantiateStreaming 取得 main.wasm 並編譯成 Browser 可以執行的代碼,最後再使用 go.run() 運行就大功告成

1
2
3
4
5
6
7
8
9
10
11
12
13
<head>
<script src="./build/wasm_exec.js"></script>
</head>
<body>
<div id="root">
</div>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch('./build/main.wasm'), go.importObject).then((result) => {
go.run(result.instance);
});
</script>
</body>

進一步練習的方向

  1. (難度 ★☆☆) 讓 Task 的順序可以被調整。

  2. (難度 ★★☆) 將資料儲存在 Browser,讓資料在頁面重新整理後不會消失。

  3. (難度 ★★★) 目前 Re-render 機制是當每修改一個資料就要執行一次整個清單的 Render,這樣的機制會造成效率低下,讀者可以思考如何只重新 Render 需要 Render 的地方。

結語

在這篇文章中,我們學習了如何使用 Golang WebAssembly 編寫一個 TODO 應用,然後將其編譯成 (*.wasm),並將 *.wasm 引入到 HTML 頁面中。最後,我們成功地創建了一個可運行在瀏覽器中的 TODO 應用程序。

使用 WebAssembly,可以編寫高效的應用程序,並將其部署到 Web 平臺上。由於 WebAssembly 能夠與現有 Web 技術相互操作,因此它在實現高效計算、加速圖形和數據處理等方面具有潛力。希望這篇文章能夠幫助讀者更好地理解如何使用 Golang WebAssembly 編寫 Web 應用程序。

參考文獻

  1. WebAssembly

  2. could not import syscall/js (no required module provides package "syscall/js" #1799

  3. Go WebAssembly -Wasm- 簡明教程

很高興能在這裡幫助到您,歡迎登入 Liker 為我鼓掌 5 次,或者成為我的讚賞公民,鼓勵我繼續創造優質文章。
以最優質的內容回應您的鼓勵