Go WebAssembly -Wasm- 簡明教程

源代碼 / 數據集已上傳到 Github - 7days-golang

1 WebAssembly 簡介

WebAssembly 是一種新的編碼方式,可以在現代的網絡瀏覽器中運行 - 它是一種低級的類彙編語言,具有緊湊的二進制格式,可以接近原生的性能運行,併爲諸如 C / C ++ 等語言提供一個編譯目標,以便它們可以在 Web 上運行。它也被設計爲可以與 JavaScript 共存,允許兩者一起工作。 —— MDN web docs - mozilla.org

從 MDN 的介紹中,我們可以得出幾個結論:

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.wasmstatic/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
}

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
}

重新編譯 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
}

接下來我們修改 index.html,爲按鈕添加點擊事件,調用 fibFunc

<html>
...
<body>
	<input type="number" />
	<button onclick="fibFunc(num.value * 1, (v)=> ans.innerHTML=v)">Click</button>
	<p></p>
</body>
</html>

接下來,重新編譯 main.go,訪問 localhost:9999,隨便輸入一個數字,點擊 Click。頁面會先顯示 Waiting 3s...,3s 過後顯示計算結果。

6 進一步的嘗試

6.1 工具框架

6.2 Demo / 項目

6.3 相關文檔

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://geektutu.com/post/quick-go-wasm.html