原始文章。
2017年2月,go Brad Fitzpatrick团队的成员
提议以该语言提供WebAssembly支持。 四个月后的2017年11月,
GopherJS的作者Richard Muziol开始实施这个想法。 最后,完整的实现在master中找到。 开发人员将在2018年8月左右收到wasm的
go版本
1.11 。 结果,如果您已经尝试在wasm中编译C,则标准库几乎承担了您熟悉的导入和导出函数的所有技术难题。 听起来很有希望。 让我们看看第一个版本可以做什么。
本文中的所有示例都可以从
作者存储库中的 docker容器中启动:
docker container run -dP nlepage/golang_wasm:examples
然后转到
localhost :32XXX /,然后从一个链接转到另一个链接。
嗨,好!
基本的“ hello world”的创建和概念已经有
充分的文档记录 (甚至
是俄语 ),因此让我们继续进行更微妙的事情。
最重要的是支持wasm的Go的新编译版本。 我不会
逐步描述安装 ,只是知道所需的已在主机中。
如果您不想为此担心,可以在
github上的golub-wasm存储库中找到
Dockerfile c go ,或者甚至可以更快地从
nlepage / golang_wasm拍摄图像。
现在,您可以编写传统的
helloworld.go
并使用以下命令进行编译:
GOOS=js GOARCH=wasm go build -o test.wasm helioworld.go
GOOS和GOARCH环境变量已经在
nlepage / golang_wasm映像中设置,因此您可以使用如下所示的
Dockerfile
文件进行编译:
FROM nlepage/golang_wasm COPY helloworld.go /go/src/hello/ RUN go build -o test.wasm hello
最后一步是使用位于
misc/wasm
中的go存储库中或
/usr/local/go/misc/wasm/
中的
test.wasm
映像
nlepage / golang_wasm中可用的
wasm_exec.html
和
wasm_exec.js
文件,在以下位置运行
test.wasm
浏览器(wasm_exec.js需要二进制文件
test.wasm
,因此我们使用此名称)。
例如,您只需要使用nginx提供3个静态文件,则wasm_exec.html将显示“运行”按钮(仅在
test.wasm
正确加载时才会
test.wasm
)。
值得注意的是,
test.wasm
必须与MIME类型
application/wasm
,否则浏览器将拒绝执行它。 (例如nginx需要
更新的mime.types文件 )。
您可以使用
nlepage / golang_wasm中的nginx图像,该图像已经在代码> / usr / share / nginx / html /目录中包含固定的MIME类型
wasm_exec.html
和
wasm_exec.js
。
现在,单击“运行”按钮,然后打开浏览器控制台,您将看到console.log问候语(“ Hello Wasm!”)。
这里有一个完整的例子。
从Go呼叫JS
现在,我们已经成功启动了从Go编译的第一个WebAssembly二进制文件,让我们仔细看一下所提供的功能。
新的syscall / js软件包已添加到标准库中,请考虑主文件
js.go
提供了一个新的
js.Value
类型,它表示一个JavaScript值。
它提供了一个用于管理JavaScript变量的简单API:
js.Value.Get()
和js.Value.Set()
返回并设置对象的字段值。js.Value.Index()
和js.Value.SetIndex()
通过读写索引访问对象。js.Value.Call()
将对象方法作为函数调用。js.Value.Invoke()
将对象本身作为函数调用。js.Value.New()
调用new运算符并将其自身的知识用作构造函数。- 还有一些获取对应Go类型的JavaScript值的方法,例如
js.Value.Int()
或js.Value.Bool()
。
以及其他有趣的方法:
js.Undefined()
将给js.Value相应的undefined
。js.Null()
将给js.Value
相应的null
。js.Global()
将返回js.Value
从而可以访问全局范围。js.ValueOf()
接受原始的Go类型并返回正确的js.Value
与其在os.StdOut中显示消息,不如使用
window.alert()
在通知窗口中显示消息。
由于我们在浏览器中,因此全局范围是一个窗口,因此首先需要从全局范围获取alert():
alert := js.Global().Get("alert")
现在我们有了一个
js.Value
形式的
alert
变量,它是对
window.alert
JS的引用,您可以使用该函数通过
js.Value.Invoke()
进行调用:
alert.Invoke("Hello wasm!")
如您所见,在将参数传递给Invoke之前不需要调用js.ValueOf(),它需要任意数量的
interface{}
并将值通过ValueOf本身传递。
现在,我们的新程序应如下所示:
package main import ( "syscall/js" ) func main() { alert := js.Global().Get("alert") alert.Invoke("Hello Wasm!") }
与第一个示例一样,您只需要创建一个名为
test.wasm
的文件,并
wasm_exec.html
和
wasm_exec.js
。
现在,当我们单击“运行”按钮时,将出现一个警报窗口,其中包含我们的消息。
一个有效的示例位于
examples/js-call
文件夹中。
从JS调用Go。
从Go调用JS非常简单,让我们仔细看一下
syscall/js
包,要查看的第二个文件是
callback.go
。
- Go函数的
js.Callback
包装器类型,用于JS。 js.NewCallback()
接受函数(接受js.Value
返回任何内容)的函数,并返回js.Callback
。- 一些管理活动回调和
js.Callback.Release()
,必须调用这些机制才能销毁回调。 js.NewEventCallback()
与js.NewEventCallback()
类似,但是包装函数仅接受1个参数-一个事件。
让我们尝试做一些简单的事情:从JS端运行Go
fmt.Println()
。
我们将对
wasm_exec.html
进行一些更改,以便能够从Go中获取回调以进行调用。
async function run() { console.clear(); await go.run(inst); inst = await WebAssembly.instantiate(mod, go.ImportObject);
这将启动wasm二进制文件并等待其完成,然后将其重新初始化以进行下一次运行。
让我们添加一个新函数,该函数将接收并保存Go回调并在完成时更改
Promise
的状态:
let printMessage
现在让我们修改
run()
函数以使用回调:
async function run() { console.clear()
这就是JS的一面!
现在,在Go部分中,您需要创建一个回调,将其发送到JS端并等待需要该函数。
var done = make(chan struct{})
然后,他们应该编写真正的函数
printMessage()
:
func printMessage(args []js.Value) { message := args[0].Strlng() fmt.Println(message) done <- struct{}{}
参数通过slice
[]js.Value
,因此您需要在第一个slice元素上调用
js.Value.String()
才能在Go行中获取消息。
现在我们可以将此函数包装在回调中:
callback := js.NewCallback(printMessage) defer callback.Release()
然后调用JS函数
setPrintMessage()
,就像调用
window.alert()
:
setPrintMessage := js.Global.Get("setPrintMessage") setPrintMessage.Invoke(callback)
最后要做的是等待在main中调用回调:
<-done
最后一部分很重要,因为回调是在专用goroutine中执行的,并且主goroutine必须等待调用回调,否则wasm二进制文件将过早停止。
生成的Go程序应如下所示:
package main import ( "fmt" "syscall/js" ) var done = make(chan struct{}) func main() { callback := js.NewCallback(prtntMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback) <-done } func printMessage(args []js.Value) { message := args[0].Strlng() fmt.PrintIn(message) done <- struct{}{} }
与前面的示例一样,创建一个名为
test.wasm
的文件。 我们
wasm_exec.html
要用我们的版本替换
wasm_exec.html
,并且可以重用
wasm_exec.js
。
现在,当您按“运行”按钮时(如我们的第一个示例),该消息将打印在浏览器控制台中,但这一次更好! (更难。)
examples/go-call
文件夹中提供了一个docker文件出价的工作示例。
漫长的工作
从JS调用Go比从Go调用JS更为麻烦,尤其是在JS方面。
这主要是由于您需要等待Go回调的结果传递到JS端。
让我们尝试其他方法:为什么不组织wasm二进制文件,它不会在回调调用后立即结束,而是会继续工作并接受其他调用。
这次,让我们从Go端开始,就像在前面的示例中一样,我们需要创建一个回调并将其发送到JS端。
添加一个调用计数器以跟踪该函数被调用了多少次。
我们的新函数
printMessage()
将打印收到的消息和计数器值:
var no int func printMessage(args []js.Value) { message := args[0].String() no++ fmt.Printf("Message no %d: %s\n", no, message) }
创建一个回调并将其发送到JS端与上一个示例相同:
callback := js.NewCallback(printMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback)
但是这一次我们没有
done
通道来通知我们主goroutine的终止。 一种方法是使用空的
select{}
永久锁定主goroutin:
select{}
这并不令人满意,我们的二进制wasm只会挂在内存中,直到关闭浏览器选项卡。
您可以在页面上监听
beforeunload
事件,您将需要第二个回调来接收该事件并通过通道通知主goroutine:
var beforeUnloadCh = make(chan struct{})
这次,新的
beforeUnload()
函数将仅接受事件,作为单个
js.Value
参数:
func beforeUnload(event js.Value) { beforeUnloadCh <- struct{}{} }
然后使用
js.NewEventCallback()
将其包装在回调中,并在JS端进行注册:
beforeUnloadCb := js.NewEventCallback(0, beforeUnload) defer beforeUnloadCb.Release() addEventLtstener := js.Global().Get("addEventListener") addEventListener.Invoke("beforeunload", beforeUnloadCb)
最后,将空阻塞
select
替换为从
beforeUnloadCh
通道读取:
<-beforeUnloadCh fmt.Prtntln("Bye Wasm!")
最终程序如下所示:
package main import ( "fmt" "syscall/js" ) var ( no int beforeUnloadCh = make(chan struct{}) ) func main() { callback := js.NewCallback(printMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback) beforeUnloadCb := js.NewEventCallback(0, beforeUnload) defer beforeUnloadCb.Release() addEventLtstener := js.Global().Get("addEventListener") addEventListener.Invoke("beforeunload", beforeUnloadCb) <-beforeUnloadCh fmt.Prtntln("Bye Wasm!") } func printMessage(args []js.Value) { message := args[0].String() no++ fmt.Prtntf("Message no %d: %s\n", no, message) } func beforeUnload(event js.Value) { beforeUnloadCh <- struct{}{} }
以前,在JS端,wasm二进制文件的下载看起来像这样:
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() { const go = new Go() const { instance } = await WebAssembly.instantiateStreaming( fetch("test.wasm"), go.importObject ) go.run(instance) })()
并用消息字段和调用
printMessage()
的按钮替换“运行”按钮:
<input id="messageInput" type="text" value="Hello Wasm!"> <button onClick="printMessage(document.querySelector('#messagelnput').value);" id="prtntMessageButton" disabled> Print message </button>
最后,接受并存储回调的
setPrintMessage()
函数应该更简单:
let printMessage; function setPrintMessage(callback) { printMessage = callback; document.querySelector('#printMessageButton').disabled = false; }
现在,当我们单击“打印消息”按钮时,您应该会在浏览器控制台中看到我们选择的消息和呼叫计数器。
如果我们选中浏览器控制台的Preserve日志框并刷新页面,我们将看到消息“ Bye Wasm!”。
源码可在github上的
examples/long-running
文件夹中找到。
然后呢
如您所见,学习到的
syscall/js
API可以完成其工作,并允许您使用一些代码来编写复杂的东西。 如果您知道更简单的方法,可以写信给
作者 。
当前无法直接从Go回调中将值返回给JS。
请记住,所有回调都在同一个goroutin中执行,因此,如果您在回调中执行了一些阻止操作,请不要忘记创建一个新的goroutin,否则将阻止所有其他回调的执行。
所有基本语言功能均已可用,包括并发功能。 目前,所有的goroutins都将在一个线程中运行,但是
将来会改变 。
在我们的示例中,我们仅使用了标准库中的fmt包,但是可以使用的所有内容都不会试图从沙盒中逃脱。
文件系统似乎通过Node.js得到支持。
最后,性能如何? 进行一些测试以查看Go wasm与等效的纯JS代码的比较会很有趣。
hajimehoshi有人测量了不同环境如何使用整数,但是这种技术不是很清楚。
不要忘记Go 1.11尚未正式发布。 我认为这对实验技术非常有利。 那些对性能测试感兴趣的人
可以折磨他们的浏览器 。
正如作者所指出的那样,主要的利基是现有go代码从服务器到客户端的传输。 但是使用新标准,您可以制作
完全脱机的应用程序 ,并且wasm代码以编译形式保存。 您可以将许多实用程序转移到Web上,同意,方便吗?