JavaScript 回调 Go


在〈Go 调用 JavaScript〉看过如何在 Go 中获取 JavaScript 的函数,然后予以调用,若你曾稍微了解过〈WebAssembly〉,就会发觉,这跟 WebAssembly 导入函数至 WebAssembly 的方式不同。

这是 JavaScript 的 wasm_exec.js 以及 Go 的syscall/js居中之缘故,在 wasm_exec.html 中你也可以看到加载、编译、实例化 WebAssembly 的过程:

if (!WebAssembly.instantiateStreaming) { // polyfill
    WebAssembly.instantiateStreaming = async (resp, importObject) => {
        const source = await (await resp).arrayBuffer();
        return await WebAssembly.instantiate(source, importObject);
    };
}

const go = new Go();
let mod, inst;
WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then((result) => {
    mod = result.module;
    inst = result.instance;
    document.getElementById("runButton").disabled = false;
});

async function run() {
    console.clear();
    await go.run(inst);
    inst = await WebAssembly.instantiate(mod, go.importObject); // reset instance
}

Go 有自己的导入对象,也就是go.importObject,这个对象主要是 JavaScript 环境与 Go 编译出来的 WebAssembly 之桥梁,将 JavaScript 的值与 Go 的结构实例作了个对应,因此,不用自己导入某个函数,只要获取某个作为命名空间的 JavaScript 对象,获取上头对应的特性,像是函数,就可以在 Go 中操作。

也就是说,如果想要在 Go 中定义函数,然后在 JavaScript 中调用,就是将 Go 中定义的函数,设定给某个对应的 JavaScript 对象,之后就可以在 JavaScript 环境中使用了,只不过在定义时,必须留意 JavaScript 与 Go 的类型对应。

可以被 JavaScript 环境调用的 Go 函数,必须被包装为js.Callback类型,这个结构类型内嵌js.Value,也就是它也是一种值,想要创建js.Callback实例,可以透过js.NewCallback函数(定义在 callback.go)。

要能被 JavaScript 调用的 Go 函数,参数类型是[]js.Value,也就是js.Valuesliceslice的元素代表着调用函数时传入的实参,你可以想像 JavaScript 函数中arguments的对应类型。

例如,显示累加至某个指定 DOM 对象的函数,可以如下定义:

package main

import "syscall/js"

func main() {
    // 注册在 JavaScript 全局
    js.Global().Set("printSumTo", js.NewCallback(printSum))
    // 阻断 main 流程
    select {}   
}

func printSum(args []js.Value) {
    c1 := args[0]         // 结果显示到这个 div 
    numbers := args[1:]   // 接下来是要累加的数字
    c1.Set("innerHTML", sum(numbers))
}

func sum(numbers []js.Value) int {
    var sum int
    for _, val := range numbers {
        sum += val.Int()
    }
    return sum
}

目前 Go 给 JavaScript 回调用的函数不支持返回值,未来也许会进一步支持,如果你想将结果带回 JavaScript 环境,就是以副作用的方式实现,例如改变某个 JavaScript 对象的状态,像是这边是改变某个 DOM 的innerHTML

因为 Go 的main执行完,模块的程序就结束了,这样 Go 中定义的函数就没有了,然而,事件会是在之后才发生,因而要被回调的函数必须存活着,为了这个目的,范例中使用select {}来阻断流程,视需求而定,你也可以用别的方式来设计某种阻断。

至于 JavaScript 的部份,来稍微修改一下 wasm_exec.html:

<!doctype html>
<!--
Copyright 2018 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<html>

<head>
    <meta charset="utf-8">
    <title>Go wasm</title>
</head>

<body>

    <script src="wasm_exec.js"></script>
    <script>
        if (!WebAssembly.instantiateStreaming) { // polyfill
            WebAssembly.instantiateStreaming = async (resp, importObject) => {
                const source = await (await resp).arrayBuffer();
                return await WebAssembly.instantiate(source, importObject);
            };
        }

        const go = new Go();
        let mod, inst;
        WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then((result) => {
            mod = result.module;
            inst = result.instance;
            document.getElementById("runButton").disabled = false;
        }).then(_ => {   // 实例化模块之后就执行
            console.clear();
            go.run(inst);       
        });
    </script>   

    <script>
        function run() {
            // 调用 Go 定义的回调函数
            printSumTo(document.getElementById('c1'), 
                1, 2, 3, 4, 5);
        }
    </script>

    <button onClick="run();" id="runButton" disabled>Run</button>
    <div id="c1"></div>

</body>
</body>

</html>

按下 Run 之后,会调用runAndPrintSum,这会先执行run函数,执行 WebAssembly 模块实例,对应的就是执行 Go 定义的main,因为run是非同步的,接下来就会执行printSumTo,因此 1 到 5 的累加结果,就会显示到idc1div元素之中。

至于 WebAssembly API 的调整,想要了解这部份的话,可以看看〈WebAssembly〉中前三篇的说明。

故且不讨论 WebAssembly API 怎么写,在自定义的 JavaScript 代码中,想要调用 Go 中定义的函数,其实感觉就是多了些额外的手续,而且不自然。

如果把一切都带到 Go 中做,将 Go 中定义的函数,当成是某事件的回调,会比较单纯一些,例如:

package main

import (
    "strconv"
    "syscall/js"
)

func main() {
    // 注册按钮事件
    dom("runButton").Call("addEventListener", "click", js.NewCallback(cal))
    select {}
}

// 根据 id 获取 DOM 对象
func dom(id string) js.Value {
    return js.Global().Get("document").Call("getElementById", id)
}

// 按下 Run 的事件处理器
func cal(args []js.Value) {
    n1, _ := inputValue("n1")
    n2, _ := inputValue("n2")
    dom("r").Set("innerHTML", n1+n2)
}

// 获取输入字段值
func inputValue(id string) (int, error) {
    return strconv.Atoi(dom(id).Get("value").String())
}

至于 wasm_exec.html 可以如下调整:

<!doctype html>
<!--
Copyright 2018 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<html>

<head>
    <meta charset="utf-8">
    <title>Go wasm</title>
</head>

<body>
    <input id="n1"> + <input id="n2"> = <span id="r"></span><br>
    <button id="runButton" disabled>Run</button>


    <script src="wasm_exec.js"></script>
    <script>
        if (!WebAssembly.instantiateStreaming) { // polyfill
            WebAssembly.instantiateStreaming = async (resp, importObject) => {
                const source = await (await resp).arrayBuffer();
                return await WebAssembly.instantiate(source, importObject);
            };
        }

        const go = new Go();
        let mod, inst;
        WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then((result) => {
            mod = result.module;
            inst = result.instance;
            document.getElementById("runButton").disabled = false;
        }).then(_ => {
            console.clear();
            go.run(inst);       
        });
    </script>   
</body>

</html>

这样就可以进行页面操作,就是个简单的加法器:

JavaScript 回调 Go

(这也许才是 Go 希望的,要你把东西都带入 Go 中来做,JavaScript 环境的事件会调用 Go 的函数,然后在 Go 中计算,在 Go 中改变对象状态、画面等。)


展开阅读全文