使用 VUE 和 Go 触摸 WebAssembly

使用 VUE 和 Go 触摸 WebAssembly

本文将展示如何在 Go 中使用 WebAssembly。本文一起来学习如何从 Go 代码构建到 WebAssembly,通过VUE来展示使用 WebAssembly 的API。

本文涉及的 Go 需要 Go1.11 或更高版本的 Go 开发环境,这里将忽略 Go 环境的配置。前端将使用 VUE2 来构建。

文章涉及代码:https://github.com/QuintionTang/Go-WebAssembly

webassembly展示平台

什么是 WebAssembly?

WebAssembly(wasm)是指一种可以在浏览器及其外围技术和工具中运行的编程语言。它以二进制格式表示并由堆栈机器实现处理。与 JavaScript 一样,它由浏览器直接解释,但正在开发和规范,目标是在速度方面超越 JavaScript。

WebAssembly 大多由 C、C++、Rust 等各种高级语言编译而成,而不是程序员直接编写二进制代码。同样在 Go 中,将 Go 代码编译为 WebAssembly 的功能从 Go1.11正式添加为 Go 的标准功能。

更多内容如下:

HelloWorld

Go 有一个叫做交叉编译的特性。不仅可以为正在编译的机器的体系结构和操作系统构建二进制文件,还可以为其他体系结构和操作系统构建二进制文件。

例如,在 macOS 上为 Windows 和 Linux 交叉编译二进制文件非常容易。可以通过指定以下环境 GOOS 变量来像往常一样进行交叉编译:GOARCH go bulid

# 为 Windows 编译(32 位)
$ GOOS=windows GOARCH=386 go build

# 为 Linux 编译(64 位)
$ GOOS=linux GOARCH=amd64 go build

创建目录 go-webassembly,进入目录,再创建 helloworld ,进入 helloworld 目录,执行命令:

go mod init go-webassembly/helloworld

创建文件 main.go ,代码如下:

package main

func main() {
	println("Hello, WebAssembly!")
}

前端实现将使用 VUE 框架来展示其调用效果,因此需要创建文件夹 vue ,将把 WebAssembly 生成的 wasmjs 文件存储到项目目录 public/wasms

现在以交叉方式编译 WebAssembly,在目录下并执行如下命令。请注意,此处将输出文件名指定为选项,但即使不指定也可以构建。GOOS js GOARCH wasm go build-o

GOOS=js GOARCH=wasm go build -o ../vue/public/wasms/helloworld/main.wasm

执行完命令后,将在目录下生成文件 main.wasm,同时将在 GOROOT 目录下生成 wasm_exec.js 文件,完整路径为 /usr/local/go/misc/wasm,将文件复制到路径 /vue/public/wasms/helloworld/ 下,命令如下:

cp /usr/local/go/misc/wasm/wasm_exec.js .
cp /usr/local/go/misc/wasm/wasm_exec_node.js .
cp /usr/local/go/misc/wasm/wasm_exec.html .

这样目录 /vue/public/wasms/helloworld 就有两个文件 jswasm,接下来执行命令:

node wasm_exec_node.js main.wasm

输出的结果如下:

Hello, WebAssembly!

接下来就是在 VUE 中来展示 Helloworld 的调用,在 public/index.html 中引入JS,如下:

<script src="./wasms/helloworld/wasm_exec.js"></script>

构建组件 Helloworld,完整代码如下:

<template>
    <div class="card">
        <div class="card-header">
            <h4>Helloworld</h4>
        </div>
        <div class="card-body">
            <p class="text-muted">
                点击“运行”,在控制台输出日志 <code>Hello, WebAssembly!</code>
            </p>
            <div class="live-preview">
                <button
                    @click="run()"
                    class="btn btn-success"
                    id="runButton"
                    disabled
                >
                    运行
                </button>
            </div>
        </div>
    </div>
</template>

<script>
export default {
    name: "Helloworld",
    data() {
        return {
            go: null,
            mod: null,
            inst: null,
        };
    },
    mounted() {
        this.init();
    },
    methods: {
        init() {
            if (!WebAssembly.instantiateStreaming) {
                WebAssembly.instantiateStreaming = async (
                    resp,
                    importObject
                ) => {
                    const source = await (await resp).arrayBuffer();
                    return await WebAssembly.instantiate(source, importObject);
                };
            }

            const go = new window.Go();
            this.go = go;
            WebAssembly.instantiateStreaming(
                fetch("/wasms/helloworld/main.wasm"),
                go.importObject
            )
                .then((result) => {
                    console.log(result);
                    this.mod = result.module;
                    this.inst = result.instance;
                    document.getElementById("runButton").disabled = false;
                })
                .catch((err) => {
                    console.error(err);
                });
        },
        async run() {
            console.clear();
            await this.go.run(this.inst);
            this.inst = await WebAssembly.instantiate(
                this.mod,
                this.go.importObject
            );
        },
    },
};
</script>

点击按钮“运行”,在浏览器控制台输入如下:

处理 JavaScript 对象

接下来,学习如何在 JavaScript 中使用对象,为了更好的处理 Go 中的 JavaScript 对象,将使用 Go1.11 标准包中包含的包 syscall/js

syscall/js 里面定义了个新的类型 js.Value,它表示一个JavaScript值,它提供了一个简单的API来操纵任何类型的JavaScript值并与之交互。一个 js.ValueOf() 函数,它接受任何 Go 基本类型并返回相应的 js.Value

Go值和 JavaScript 值对应关系如下:

Go JavaScript
js.Value JavaScript 中的任何值
js.Func function
nil null
bool Boolean
integers 和 floats Number
string String
[]interface{} new array
map[string]interface{} new object

JavaScript 中的 js.Type 类型表示为类型。js.Type 类型定义如下,可以从 js.Value 类型的方法中检索。

type Type int
const (
	TypeUndefined Type = iota
	TypeNull
	TypeBoolean
	TypeNumber
	TypeString
	TypeSymbol
	TypeObject
	TypeFunction
)

js.Value 类型将所有 JavaScript 值表示为单一类型,因此如果每个方法都调用了一个意外的值,panic 就会导致崩溃。例如,Int 方法可以 js.Value 将类型的值视为数字并将 int 值作为 Go 类型检索。但是,js.Value 类型也可以处理函数和字符串值,所以当调用一个不是数字的值时,panic 会发生错误。

因此,js.Type 通过使用 type 值,js.Value可以处理 type 值的具体类型,避免 panic。例如,对于Int 方法,最好只在 Type 方法返回 js.TypeNumber 时调用。如下:

func printNumber(v js.Value) {
        if v.Type() == js.TypeNumber {
                fmt.Printf("%d\n", v.Int())
        }
}

DOM 操作

可以在 Go 中使用 js.Value 来更好的操作 HTML Dom 对象。接下来创建目录 docments ,创建文件 main.go ,代码如下:

package main
import "syscall/js"

func main() {
	// 获取全局对象(网页浏览器为window)
	window := js.Global()
	// window.document.getElementById("helloresult")
	message := window.Get("document").Call("getElementById", "helloresult")
	// HTML
	message.Set("innerHTML", "Hello, WebAssembly")
}

接下来在前端创建一个 id="helloresult" 的 DOM 对象。

按照上面的流程,生成 wasm 文件:

GOOS=js GOARCH=wasm go build -o ../vue/public/wasms/document/main.wasm

事件处理

上面介绍了如何操作 DOM,现在来实现时间的处理。

package main

import "syscall/js"

func main() {
        window := js.Global()
        // window.document.getElementById("clickresult") 
        message := window.Get("document").Call("getElementById", "clickresult")

		cb := js.FuncOf(func(this js.Value, args []js.Value) any { 
				message.Set("innerHTML", "Go 事件触发")
				return nil
		})
        // message.addEventListener("click", cb)
        message.Call("addEventListener", "click", cb)

        select {}
		
}

按照上面的流程,生成 wasm 文件:

GOOS=js GOARCH=wasm go build -o ../vue/public/wasms/events/main.wasm

总结

本文介绍了如何将 Go 代码构建为 WebAssembly、如果实现 Go 与 JavaScript 对象及回调函数。