Rust技巧-WebAssembly和Vugu

122 阅读4分钟

我利用空闲时间学习RustWebAssembly已经有几个月了,说实话,没有什么真正疯狂的东西,只是阅读一些文档、文章和博客,看一些视频和参加一些现场研讨会来了解更多。Rust和WebAssembly密切相关,实际上我应该先学习Rust,但事实上,我的大部分时间都用于更深入地探索WebAssembly,特别是学习Go中已经取得的进展,以及使用不同的包和其他东西与WebAssembly互动的一些方法。

有很多好的文章和视频,涵盖了关于Go和WebAssembly如何工作的真正有趣的技术内容。

有趣的是,在GopherCon 2020期间,有一个名为Go不只是在你的服务器上,它还在你的浏览器中的演讲。Vugu(Go+WebAssembly UI库)的故事和介绍,作者Brad Peabody,内容包括 Vugu一个用于Go+WebAssembly的现代UI库,真正引起了我的兴趣。

有趣的是(再次!)我一直在工作,就在最近,在一个内部工具上,碰巧需要一些前端代码,这是开始真正使用Vugu的完美借口。

我从两周前开始,已经发布了这个内部工具的工作版本,恰好与我们的后端API交互,说实话,一切工作都很顺利,最酷的是一切都用Go编写。

然而,由于缺乏使用WebAssembly和Vugu的经验,我最初遇到了一些问题,接下来让我详细介绍一下我是如何解决这些问题的。顺便说一下,所有的代码都可以在Gitlab上找到,欢迎探索并在本地运行它。

渲染Vugu页面的自定义模板

官方文档中提到了对全HTML模式的支持,但我还没能弄明白,最后我的解决方案是通过提供所有资产的服务器来渲染这个模板。

templ := fmt.Sprintf(/* here define the HTML template to be used */)

h := simplehttp.New(wd, false)
h.PageHandler = &simplehttp.PageHandler{
	Template:         template.Must(template.New("_page_").Parse(templ)),
	TemplateDataFunc: simplehttp.DefaultTemplateDataFunc,
}

请参考最后的例子,特别是模板的定义,以及如何使用这个模板。

wasmmain arguments via Javascript.

与上面的问题有一点关系。如果我们需要向最终的wasm工件传递信息,我们应该使用类似的东西。

WebAssembly.instantiateStreaming(fetch("/main.wasm"), go.importObject).then((result) => {
    go.argv = ["main.wasm", "-parameter", "%s", "-parameter2", "something"];
    go.run(result.instance);
});

例如,在我们需要表明配置细节的情况下,恰好只能通过环境变量通过原始服务器获得。

/health 使用simplehttp检查

如果你使用simplehttp (通过github.com/vugu/vugu/simplehttp )来构建交付资产的服务器,那么你可能需要一种方法来定义一个health-like 端点,以允许你的自动缩放配置工作,这可以通过实现一个PageHandler 的包装器来轻松解决,类似于。

health := func(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/health" {
			// do some actual health checks
			w.WriteHeader(http.StatusOK)
			return
		}

		h.ServeHTTP(w, r)
	})
}

h := simplehttp.New(wd, false)

h.PageHandler = health(&simplehttp.PageHandler{/* actual configuration */})

渲染更小的wasm文件

到目前为止,最终编译的wasm文件有点大,为了减少它们的文件大小,你需要两个方面的结合。

  1. 要使用 brotli来压缩最终的wasm文件和。
  2. 要使用 gzipped.FileServer(通过github.com/lpar/gzipped/v2)将压缩后的资产与相应的头文件(如果客户端支持的话)一起交付。

像下面这样的东西应该可以。

h.StaticHandler = gzipped.FileServer(gzipped.Dir(wd))

不要忘了http.StripPrefix ,以防你要求的路径与本地路径不同。

为了说明情况,最终的文件看起来像这样。

4.6M Dec 20 14:35 main.wasm
980K Dec 20 14:35 main.wasm.br

下载一个文件

最后,这个很有趣,因为它涉及到通过标准库从Go向Javascript传递数据。在链接的例子中,这是在点击"下载文件!"按钮时触发的。

func (r *Root) HandleDownloadFile(e vugu.DOMEvent) {
	var output bytes.Buffer

	// literal copy/paste from the encoding/csv godoc

	records := [][]string{
		{"first_name", "last_name", "username"},
		{"Rob", "Pike", "rob"},
		{"Ken", "Thompson", "ken"},
		{"Robert", "Griesemer", "gri"},
	}

	w := csv.NewWriter(&output)

	for _, record := range records {
		if err := w.Write(record); err != nil {
			log.Fatalln("error writing record to csv:", err)
		}
	}

	w.Flush()

	if err := w.Error(); err != nil {
		log.Fatal(err)
	}

	// important part

	dst := js.Global().Get("Uint8Array").New(len(output.Bytes()))
	_ = js.CopyBytesToJS(dst, output.Bytes())
	js.Global().Call("downloadFile", dst, "text/csv", "usernames.csv")
}

真正重要的部分是方法的最后3行

dst := js.Global().Get("Uint8Array").New(len(output.Bytes()))
_ = js.CopyBytesToJS(dst, output.Bytes())
js.Global().Call("downloadFile", dst, "text/csv", "usernames.csv")
  • Uint8Array之所以使用,是因为它代表一个8位无符号整数的数组,并且记得byteuint8 的别名,因此使它们等同。
  • downloadFile() 被调用来将数据从围棋传递到原始的Javascript函数。

这样就可以把数据从Go中传到Javascript中,最后在前台触发下载。

总结

我真的很喜欢Vugu(和WebAssembly),我理解众所周知的局限性,比如用户需要一个现代的浏览器,但不知为何,如果你能控制你的用户是谁,那么使用Vugu来构建Web应用程序就是一个很好的借口。