借助WebAssembly,您可以在Go上编写Frontend

原始文章。

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 # Find out which host port is used docker container ls 

然后转到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.htmlwasm_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.htmlwasm_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.htmlwasm_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 // Our reference to the Go callback let printMessageReceived // Our promise let resolvePrintMessageReceived // Our promise resolver function setPrintMessage(callback) { printMessage = callback resolvePrintMessageReceived() } 

现在让我们修改run()函数以使用回调:

 async function run() { console.clear() // Create the Promise and store its resolve function printMessageReceived = new Promise(resolve => { resolvePrintMessageReceived = resolve }) const run = go.run(inst) // Start the wasm binary await printMessageReceived // Wait for the callback reception printMessage('Hello Wasm!') // Invoke the callback await run // Wait for the binary to terminate inst = await WebAssembly.instantiate(mod, go.importObject) // reset instance } 

这就是JS的一面!

现在,在Go部分中,您需要创建一个回调,将其发送到JS端并等待需要该函数。

  var done = make(chan struct{}) 

然后,他们应该编写真正的函数printMessage()

 func printMessage(args []js.Value) { message := args[0].Strlng() fmt.Println(message) done <- struct{}{} // Notify printMessage has been called } 

参数通过slice []js.Value ,因此您需要在第一个slice元素上调用js.Value.String()才能在Go行中获取消息。
现在我们可以将此函数包装在回调中:

 callback := js.NewCallback(printMessage) defer callback.Release() // to defer the callback releasing is a good practice 

然后调用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上,同意,方便吗?

Source: https://habr.com/ru/post/zh-CN417563/


All Articles