概述
上篇介绍了如何成功执行了Go编译的第一个WebAssembly(以下简称wasm)二进制文件,接着进一步测试下Go的wasm的能实现的功能。
从Go调用JS
Go的标准库有一个新的包syscall/js,先看下js.go文件。里面定义了个新的类型js.Value,它表示一个JavaScript值。它提供了一个简单的API来操纵任何类型的JavaScript值并与之交互:
js.Value.Get()和js.Value.Set()检索并设置Object值的属性js.Value.Index()和js.Value.SetIndex()检索并设置Array值中的值js.Value.Call()在一个Object值上调用一个方法js.Value.Invoke()调用一个函数值js.Value.New()在代表JS类型的引用上调用new运算符- 在相应的
Go类型中检索JavaScript值的其他方法(如js.Value.Int()或js.Value.Bool())
一个js.ValueOf()函数,它接受任何Go基本类型并返回相应的js.Value。
最后是一些有趣的变量:
js.Undefined与js的undefined对应的js.Valuejs.Null与js的null对应的js.Valuejs.Global允许访问js全局范围的js.Value
尝试调用下js的window.alert()将消息其显示在对话框中,而不是发送到console。
由于在浏览器中,global就是window,从global中检索alert(),于是有了一个alert类型的js.Value变量,它是对js的window.alert的引用,在其上使用js.Value.Invoke()。可以发现在将参数传递给Invoke之前不需要调用js.ValueOf(),它接受interface{}参数,并通过调用ValueOf去运行。
package main |
现在,当点击按钮时,会弹出一条包含Hello wasm!消息的对话框。
从JS调用Go
如上从Go调用js非常简单,接着看callback.go文件。里面定义了一个新的js.Callback类型,它代表一个Go的func包装以便用作js回调。一个js.NewCallback()函数,它接受一个js.Value切片(并且不返回任何内容)并返回一个js.Callback。并提供一些机制来管理活动回调,以及一个js.Callback.Close()函数,当不再使用回调时必须调用它来释放相应资源。另外还有一个js.NewEventCallback()函数来接受js事件。
先试着做一些简单的事情,从js端触发Go的fmt.Println。
当前执行wasm二进制文件的run()函数如下所示,需要在wasm_exec.html中进行一些调整,让它能够从Go接收回调并调用它。
async function run() { |
它启动wasm二进制文件并等待它终止,然后重新实例化它以便下次运行。添加一个新的函数,它将接收并存储Go回调,并在完成后立即解析Promise:
let printMessage |
现在调整run()函数以使用回调:
async function run() { |
现在Go部分需要创建回调,将其发送给js端并等待它被调用。需要一个channel来通知回调被调用了,然后编写实际的printMessage()``func:
var done = make(chan struct{}) |
正如所看到的,参数是在js.Value的切片中接收到的,在第一个元素上调用js.Value.String()转化为Go的string来获取message。现在可以在回调中包装这个func,然后调用js的setPrintMessage()函数,就像调用window.alert()时一样,最后就是等待回调被调用,这个很重要,因为回调是在goroutine中执行的,因此主goroutine必须等待回调被调用,否则wasm二进制会提前终止。
callback := js.NewCallback(printMessage) |
完整的Go程序应如下所示:
import ( |
编辑wasm_exec.html,继续重用wasm_exec.js。现在,当点击按钮时,和之前的hello world类似Hello Wasm!消息被输出在console中。
持续运行
从js调用Go比从Go调用js更麻烦一些,特别是在js部分。这主要是因为需要等待Go回调传递给js,而且执行完就终止了,如何让wasm不会在调用回调之后终止,却继续运行并接收其他调用?
这一次从Go开始,同样需要创建一个回调并将它发送给js端。并添加一个调用计数器,以便跟踪回调被调用的次数。新的printMessage()函数将打印接收到的消息和调用计数器的值:
var no int |
创建回调并将其发送给js端与我们前面的示例中完全相同,但是这一次没有完成的channel来通知什么时候终止主goroutine。一种方法是使用空select无限制地阻塞主goroutine。这不是很优雅,wasm二进制文件永远不会完全关闭,并且可能会在浏览器关闭wasm_exec.html时被kill。另一种方法就是监听页面事件来终止主goroutine。
创建回调来接收页面的beforeunload事件并通过一个channel通知主goroutine。这次新的beforeUnload()函数将只接受一个js.Value参数用来接受事件:
var beforeUnloadCh = make(chan struct{}) |
然后可以使用js.NewEventCallback()将它包装在一个回调中,并将其注册到js端:
beforeUnloadCb := js.NewEventCallback(0, beforeUnload) |
最后用beforeUnloadCh通道上的接收替换空select:
<-beforeUnloadCh |
最终Go程序如下所示:
package main |
现在在js部分,这是wasm二进制文件的加载:
const go = new Go(); |
修改让它在加载后直接启动wasm二进制文件:
let run |
通过输入框和按钮来替换我们的Run按钮来触发printMessage():
<input id="messageInput" type="text" value="Hello Wasm!"> |
接收和存储回调的setPrintMessage()函数变得简单了:
let printMessage |
现在,当点击Print message按钮时,应该看到输入的信息和计数器输出在console中。然后,如果勾选浏览器控制台的Preserve log选项并刷新页面,则应该在console中看到Bye Wasm !。
最后
上面用简单的例子和较少的代码测试了syscall/jsAPI,Go与js之间更容易的相互调用了。如果感兴趣的可以做一些基准测试比较下Go的wasm与等效的纯js代码的性能。