langliu1216@gmail.com

Go WebAssembly 入门 - 2022/11/03

Go WebAssembly 入门

VSCode 配置

在 VSCode 中引入 “syscall/js” 时会提示错误,需要通过设置环境变量的方式解决:

{
  "go.toolsEnvVars": {
    "GOOS": "js",
    "GOARCH": "wasm"
  }
}

示例

package main

import (
    "syscall/js"
)

func main() {
    // 获取全局的 alert 对象
    alert := js.Global().Get("alert")
    // 等价于在 js 中调用 `window.alert("Hello World")`
    alert.Invoke("Hello World")
}

main.go build 成WebAssembly(简写为wasm)二进制文件:

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

把 JavaScript 依赖拷贝到当前路径:

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .

创建一个 index.html 文件,并引入 wasm_exec.js 文件,调用刚才build的 main.wasm

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go WebAssembly</title>
</head>

<body>
    <script src="wasm_exec.js"></script>
    <script>
        const go = new Go();
        WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
            go.run(result.instance);
        });
    </script>
</body>

</html>

此时浏览器访问这个 html 文件则会出现一个 alert 弹窗,提示文字为 Hello World

Hello World

函数注册

在 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 中调用。

js.Func() 接受一个函数类型作为其参数,该函数的定义必须是:

func(this Value, args []Value) interface{}  
// this 即 JavaScript 中的 this  
// args 是在 JavaScript 中调用该函数的参数列表。  
// 返回值需用 js.ValueOf 映射成 JavaScript 的值
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go WebAssembly</title>
</head>

<body>
    <input id="num" type="number" />
    <button id="btn" onclick="ans.innerHTML=fibFunc(num.value * 1)">Click</button>
    <p id="ans">1</p>
    <script src="wasm_exec.js"></script>
    <script>
        const go = new Go();
        WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
            go.run(result.instance);
        });
    </script>
</body>
  
</html>

操作 DOM

在上面的例子中,仅仅是注册了全局函数 fibFunc,事件注册,调用,对 DOM 元素的操作都是在 HTML
中通过原生的 JavaScript 函数实现的。这些事情,能不能全部在 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", ...) 渲染计算结果。

回调函数

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  
}

参考