Go WebAssembly -Wasm- 簡明教程
源代碼 / 數據集已上傳到 Github - 7days-golang
1 WebAssembly 簡介
WebAssembly 是一種新的編碼方式,可以在現代的網絡瀏覽器中運行 - 它是一種低級的類彙編語言,具有緊湊的二進制格式,可以接近原生的性能運行,併爲諸如 C / C ++ 等語言提供一個編譯目標,以便它們可以在 Web 上運行。它也被設計爲可以與 JavaScript 共存,允許兩者一起工作。 —— MDN web docs - mozilla.org
從 MDN 的介紹中,我們可以得出幾個結論:
- 1)WebAssembly 是一種二進制編碼格式,而不是一門新的語言。
-
- WebAssembly 不是爲了取代 JavaScript,而是一種補充(至少現階段是這樣),結合 WebAssembly 的性能優勢,很大可能集中在對性能要求高(例如遊戲,AI),或是對交互體驗要求高(例如移動端)的場景。
- 3)C/C++ 等語言可以編譯 WebAssembly 的目標文件,也就是說,其他語言可以通過編譯器支持,而寫出能夠在瀏覽器前端運行的代碼。
Go 語言在 1.11 版本 (2018 年 8 月) 加入了對 WebAssembly (Wasm) 的原生支持,使用 Go 語言開發 WebAssembly 相關的應用變得更加地簡單。Go 語言的內建支持是 Go 語言進軍前端的一個重要的里程碑。在這之前,如果想使用 Go 語言開發前端,需要使用 GopherJS,GopherJS 是一個編譯器,可以將 Go 語言轉換成可以在瀏覽器中運行的 JavaScript 代碼。新版本的 Go 則直接將 Go 代碼編譯爲 wasm 二進制文件,而不再需要轉爲 JavaScript 代碼。更巧的是,實現 GopherJS 和在 Go 語言中內建支持 WebAssembly 的是同一撥人。
Go 語言實現的函數可以直接導出供 JavaScript 代碼調用,同時,Go 語言內置了 syscall/js 包,可以在 Go 語言中直接調用 JavaScript 函數,包括對 DOM 樹的操作。
2 Hello World
如果對 Go 語言不熟悉,推薦 Go 語言簡明教程,一篇文章快速入門。
接下來,我們使用 Go 語言實現一個最簡單的程序,在網頁上彈出 Hello World
。
第一步,新建文件 main.go,使用 js.Global().get(‘alert’) 獲取全局的 alert 對象,通過 Invoke 方法調用。等價於在 js 中調用 window.alert("Hello World")
。
package main
import "syscall/js"
func main() {
alert := js.Global().Get("alert")
alert.Invoke("Hello World!")
}
第二步,將 main.go 編譯爲 static/main.wasm
如果啓用了
GO MODULES
,則需要使用 go mod init 初始化模塊,或設置 GO111MODULE=auto。
$ GOOS=js GOARCH=wasm go build -o static/main.wasm
第三步,拷貝 wasm_exec.js (JavaScript 支持文件,加載 wasm 文件時需要) 到 static 文件夾
$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" static
第四步,創建 index.html,引用 static/main.wasm
和 static/wasm_exec.js
。
<html>
<script src="static/wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("static/main.wasm"), go.importObject)
.then((result) => go.run(result.instance));
</script>
</html>
第五步,使用 goexec 啓動 Web 服務
如果沒有安裝 goexec,可用
go get -u github.com/shurcooL/goexec
安裝,需要將 $GOBIN 或 $GOPATH/bin 加入環境變量
當前的目錄結構如下:
demo/
|--static/
|--wasm_exec.js
|--main.wasm
|--main.go
|--index.html
$ goexec 'http.ListenAndServe(`:9999`, http.FileServer(http.Dir(`.`)))'
瀏覽器訪問 localhost:9999,則會有一個彈出窗口,上面寫着 Hello World!。
爲了避免每次編譯都需要輸入繁瑣的命令,可將這個過程寫在 Makefile
中
all: static/main.wasm static/wasm_exec.js
goexec 'http.ListenAndServe(`:9999`, http.FileServer(http.Dir(`.`)))'
static/wasm_exec.js:
cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.js" static
static/main.wasm : main.go
GO111MODULE=auto GOOS=js GOARCH=wasm go build -o static/main.wasm .
這樣一個敲一下 make 就夠了,代碼已經上傳到 7days-golang - github.com。
3 註冊函數 (Register Functions)
在 Go 語言中調用 JavaScript 函數是一方面,另一方面,如果僅僅是使用 WebAssembly 替代性能要求高的模塊,那麼就需要註冊函數,以便其他 JavaScript 代碼調用。
假設我們需要註冊一個計算斐波那契數列的函數,可以這麼實現。
package main
import "syscall/js"
func fib(i int) int {
if i == 0 || i == 1 {
return 1
}
return fib(i-1) + fib(i-2)
}
func fibFunc(this js.Value, args []js.Value) interface{} {
return js.ValueOf(fib(args[0].Int()))
}
func main() {
done := make(chan int, 0)
js.Global().Set("fibFunc", js.FuncOf(fibFunc))
<-done
}
- fib 是一個普通的 Go 函數,通過遞歸計算第 i 個斐波那契數,接收一個 int 入參,返回值也是 int。
- 定義了 fibFunc 函數,爲 fib 函數套了一個殼,從 args[0] 獲取入參,計算結果用 js.ValueOf 包裝,並返回。
- 使用 js.Global().Set() 方法,將註冊函數 fibFunc 到全局,以便在瀏覽器中能夠調用。
js.Value
可以將 Js 的值轉換爲 Go 的值,比如 args[0].Int(),則是轉換爲 Go 語言中的整型。js.ValueOf
,則用來將 Go 的值,轉換爲 Js 的值。另外,註冊函數的時候,使用 js.FuncOf 將函數轉換爲 Func
類型,只有 Func 類型的函數,才能在 JavaScript 中調用。可以認爲這是 Go 與 JavaScript 之間的接口 / 約定。
js.Func()
接受一個函數類型作爲其參數,該函數的定義必須是:
func(this Value, args []Value) interface{}
在 main 函數中,創建了信道 (chan) done,阻塞主協程 (goroutine)。fibFunc 如果在 JavaScript 中被調用,會開啓一個新的子協程執行。
A wrapped function triggered during a call from Go to JavaScript gets executed on the same goroutine. A wrapped function triggered by JavaScript’s event loop gets executed on an extra goroutine. —— FuncOf - golang.org
接下來,修改之前的 index.html,在其中添加一個輸入框 (num),一個按鈕(btn) 和一個文本框(ans,用來顯示計算結果),並給按鈕添加了一個點擊事件,調用 fibFunc,並將計算結果顯示在文本框(ans) 中。
<html>
...
<body>
<input type="number" />
<button onclick="ans.innerHTML=fibFunc(num.value * 1)">Click</button>
<p>1</p>
</body>
</html>
使用之前的命令重新編譯 main.go,並在 9999 端口啓動 Web 服務,如果我們已經將命令寫在 Makefile 中了,只需要運行 make
即可。
接下來訪問 localhost:9999,可以看到如下效果。輸入一個數字,點擊Click
,計算結果顯示在輸入框下方。
4 操作 DOM
在上一個例子中,僅僅是註冊了全局函數 fibFunc,事件註冊,調用,對 DOM 元素的操作都是在 HTML
中通過原生的 JavaScript 函數實現的。這些事情,能不能全部在 Go 語言中完成呢?答案可以。
首先修改 index.html,刪除事件註冊部分和 對 DOM 元素的操作部分。
<html>
...
<body>
<input type="number" />
<button>Click</button>
<p>1</p>
</body>
</html>
修改 main.go:
package main
import (
"strconv"
"syscall/js"
)
func fib(i int) int {
if i == 0 || i == 1 {
return 1
}
return fib(i-1) + fib(i-2)
}
var (
document = js.Global().Get("document")
numEle = document.Call("getElementById", "num")
ansEle = document.Call("getElementById", "ans")
btnEle = js.Global().Get("btn")
)
func fibFunc(this js.Value, args []js.Value) interface{} {
v := numEle.Get("value")
if num, err := strconv.Atoi(v.String()); err == nil {
ansEle.Set("innerHTML", js.ValueOf(fib(num)))
}
return nil
}
func main() {
done := make(chan int, 0)
btnEle.Call("addEventListener", "click", js.FuncOf(fibFunc))
<-done
}
- 通過
js.Global().Get("btn")
或document.Call("getElementById", "num")
兩種方式獲取到 DOM 元素。 - btnEle 調用
addEventListener
爲 btn 綁定點擊事件 fibFunc。 - 在 fibFunc 中使用
numEle.Get("value")
獲取到 numEle 的值(字符串),轉爲整型並調用 fib 計算出結果。 - ansEle 調用
Set("innerHTML", ...)
渲染計算結果。
重新編譯 main.go,訪問 localhost:9999,效果與之前是一致的。
5 回調函數 (Callback Functions)
在 JavaScript 中,異步 + 回調是非常常見的,比如請求一個 Restful API,註冊一個回調函數,待數據獲取到,再執行回調函數的邏輯,這個期間程序可以繼續做其他的事情。Go 語言可以通過協程實現異步。
假設 fib 的計算非常耗時,那麼可以啓動註冊一個回調函數,待 fib 計算完成後,再把計算結果顯示出來。
我們先修改 main.go,使得 fibFunc 支持傳入回調函數。
package main
import (
"syscall/js"
"time"
)
func fib(i int) int {
if i == 0 || i == 1 {
return 1
}
return fib(i-1) + fib(i-2)
}
func fibFunc(this js.Value, args []js.Value) interface{} {
callback := args[len(args)-1]
go func() {
time.Sleep(3 * time.Second)
v := fib(args[0].Int())
callback.Invoke(v)
}()
js.Global().Get("ans").Set("innerHTML", "Waiting 3s...")
return nil
}
func main() {
done := make(chan int, 0)
js.Global().Set("fibFunc", js.FuncOf(fibFunc))
<-done
}
- 假設調用 fibFunc 時,回調函數作爲最後一個參數,那麼通過 args[len(args)-1] 便可以獲取到該函數。這與其他類型參數的傳遞並無區別。
- 使用
go func()
啓動子協程,調用 fib 計算結果,計算結束後,調用回調函數callback
,並將計算結果傳遞給回調函數,使用 time.Sleep() 模擬 3s 的耗時操作。 - 計算結果出來前,先在界面上顯示
Waiting 3s...
接下來我們修改 index.html,爲按鈕添加點擊事件,調用 fibFunc
<html>
...
<body>
<input type="number" />
<button onclick="fibFunc(num.value * 1, (v)=> ans.innerHTML=v)">Click</button>
<p></p>
</body>
</html>
- 爲 btn 註冊了點擊事件,第一個參數是待計算的數字,從 num 輸入框獲取。
- 第二個參數是一個回調函數,將參數 v 顯示在 ans 文本框中。
接下來,重新編譯 main.go,訪問 localhost:9999,隨便輸入一個數字,點擊 Click。頁面會先顯示 Waiting 3s...
,3s 過後顯示計算結果。
6 進一步的嘗試
6.1 工具框架
- WebAssembly 的二進制分析工具 WebAssembly Code Explorer
- 使用 NodeJs 或瀏覽器測試 Go Wasm 代碼 Github Wiki
- 借鑑 Vue 實現的 Golang WebAssembly 前端框架 Vugu,完全使用 Go,不用寫任何的 JavaScript 代碼。
6.2 Demo / 項目
6.3 相關文檔
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://geektutu.com/post/quick-go-wasm.html