仓库源文站点原文


title: 初探Go-WebAssembly toc: true cover: 'https://img.paulzzh.com/touhou/random?65' date: 2020-09-23 20:06:47 categories: Golang tags: [Golang, WebAssembly]

description: WebAssembly作为一项新的技术,允许其他后台语言编译出.wasm格式的字节码文件,供浏览器和JS调用;Go言在1.11版本(2018年8月)加入了对WebAssembly(Wasm)的原生支持,使用 Go 语言开发WebAssembly相关的应用变得更加地简单;本文从一个Hello-World的例子开始,带大家初探如何使用Golang开发WebAssembly;

WebAssembly作为一项新的技术,允许其他后台语言编译出.wasm格式的字节码文件,供浏览器和JS调用;Go言在1.11版本(2018年8月)加入了对WebAssembly(Wasm)的原生支持,使用 Go 语言开发WebAssembly相关的应用变得更加地简单;

本文从一个Hello-World的例子开始,带大家初探如何使用Golang开发WebAssembly;

源代码:

<!--more-->

<br/>

初探Go-WebAssembly

<BR/>

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 树的操作!

<BR/>

在Goland中使用Web-Assembly

在Goland中进行开发时需要进行一些简单的配置;

Goland官方文档也说明了如何配置:

https://www.jetbrains.com/help/go/webassembly-project.html

<BR/>

Hello World

使用wasm实现的一个通知alert的例子;

第一步,新建文件main.go,使用 js.Global().get(‘alert’) 获取全局的 alert 对象,通过Invoke方法调用;

等价于在 js 中调用 window.alert("Hello World")

// main.go
package main

import "syscall/js"

func main() {
    alert := js.Global().Get("alert")
    alert.Invoke("Hello World!")
}

第二步,将 main.go 编译为 static/main.wasm

<br/>

如果启用了 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 服务

<BR/>

如果没有安装 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 就够了!

<BR/>

源代码见:https://github.com/JasonkayZK/go-assembly/tree/hello-world

<BR/>

注册函数(Register Functions)

在Go中注册JS函数,以便其他 JavaScript 代码调用 ;

在 Go 语言中调用 JavaScript 函数是一方面,另一方面,如果仅仅是使用 WebAssembly 替代性能要求高的模块,那么就需要注册函数,以便其他 JavaScript 代码调用。

假设我们需要注册一个计算斐波那契数列的函数,可以这么实现:

// main.go
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{}
// this 即 JavaScript 中的 this
// args 是在 JavaScript 中调用该函数的参数列表。
// 返回值需用 js.ValueOf 映射成 JavaScript 的值

在 main 函数中,创建了信道(chan) done,阻塞主协程(goroutine);

fibFunc 如果在 JavaScript 中被调用,会开启一个新的子协程执行:

<br/>

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 id="num" type="number" />
    <button id="btn" onclick="ans.innerHTML=fibFunc(num.value * 1)">Click</button>
    <p id="ans">1</p>
</body>
</html>

使用之前的命令重新编译 main.go,并在 9999 端口启动 Web 服务,如果我们已经将命令写在 Makefile 中了,只需要运行 make 即可;

接下来访问 localhost:9999,可以看到效果:

输入一个数字,点击Click,计算结果显示在输入框下方;

<BR/>

源代码见:https://github.com/JasonkayZK/go-assembly/tree/register-function

<BR/>

操作DOM

注册函数(Register Functions)例子中,仅仅是注册了全局函数 fibFunc,事件注册,调用,对 DOM 元素的操作都是在 HTML中通过原生的 JavaScript 函数实现的。

这些事情,能不能全部在 Go 语言中完成呢?答案可以。

首先修改 index.html,删除事件注册部分和 对 DOM 元素的操作部分。

<html>
...
<body>
    <input id="num" type="number" />
    <button id="btn">Click</button>
    <p id="ans">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,效果与之前是一致的!

<BR/>

源代码见:https://github.com/JasonkayZK/go-assembly/tree/operate-dom

<BR/>

回调函数(Callback Functions)

在 JavaScript 中,异步+回调是非常常见的,比如请求一个 Restful API,注册一个回调函数,待数据获取到,再执行回调函数的逻辑,这个期间程序可以继续做其他的事情;

Go 语言可以通过协程实现异步,进而简化编程;

在 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 id="num" type="number" />
    <button id="btn" onclick="fibFunc(num.value * 1, (v)=> ans.innerHTML=v)">Click</button>
    <p id="ans"></p>
</body>
</html>

接下来,重新编译 main.go,访问 localhost:9999:

随便输入一个数字,点击 Click。页面会先显示 Waiting 3s...,3s过后显示计算结果;

<BR/>

源代码见:https://github.com/JasonkayZK/go-assembly/tree/callback-function

<BR/>

进一步的尝试

6.1 工具框架

6.2 Demo/项目

6.3 相关文档

<BR/>

附录

源代码:

本文转自:

<br/>