WebAssembly 和 Go语言:对未来的观望

我反对学习 JavaScript 还有前端开发已经不是秘密了。事实上,在 CSS 出现前我就学会了 HTML,不过 JavaScript 是我做 Web 开发好久后的事情了。当看到现代 Web 的发展时,我感到不寒而栗。这个生态对于脱离已久的我来说是如此迷茫。Node, webpack, yarn, npm, frameworks, UMD, AMD,我的天啊!

目前我关注 WebAssembly 也已经有段时间了,期望它能让我在没有典型 JavaScript 构建的情况下编写 Web 应用程序。

当听到 WebAssembly(wasm) 最近支持 Go 语言时,我知道实验的时机已经成熟,并且迫切期待尝试。在尝试之前我读了些好文章,而这篇文章将记录我的一些体验。

为了用 Go 来写 wasm,你需要先下载 Go 源码并编译好。从 Go 1.11 开始,WebAssembly 将被原生支持,但现在还没有 release。

你可以按照 这里 的步骤来编译 Go。因为 Go 本身也是用 Go 语言实现的,所以在编译之前你需要先有一个可以正常工作的 Go 二进制版本来自举自己。最终,你系统里会有两个不同的 Go 版本。 注意:如果你后面忘了你系统里安装了两个版本的 Go,那可能会给你造成一些困扰。 可以使用  direnv 来管理 Go 版本,这样你就可以为不同的项目来配置不同的 Go 了。

安装最新的 Go 后,就可以体验 WebAssembly 了。你需要一个 HTML 文件和一个 JavaScript 脚本来加载生成的 wasm 文件。这些都包含在 Go 安装路径下的 misc/wasm 目录里。你可以复制它们到项目目录,修改它们以加载你的 wasm 文件。

我的第一个项目有点雄心勃勃,我打算用 Go 语言构建一个看起来像 Web 组件 的东西,编译成 WebAssembly。我并没有把整件事做完,因为我被每件事要如何都做得好弄得心烦意乱。

首先,我将 GOROOT/misc/wasm 中的 HTML 和 JavaScript 文件复制到一个新目录中,并添加了一个 main.go 文件。根据我预先想好的计划,我把 HTML 放进 DOM 的一个现有节点,这个 DOM 要在 HTML 中声明。所以我创建了一个带有 thing 作为 ID 的 HTML section 标签。

<section class="main" id="thing" >Please wait...</section>

我在 HTML 文件底部的脚本标签上面插入了这个。接下来,我知道我想程序化地替换这个节点,所以我查找了 Go 的 wasm 库中与 DOM 交互的语法。为 Go 添加了一个 syscall/js 包,允许与 DOM 进行交互。我使用了这段 Go 代码得到了一个 HTML 带有 thing 作为 ID 的节点的引用:

el := js.Global.Get("document").Call("getElementById", "thing")

现在我有一个空DOM节点的引用,我可以使用渲染的HTML来填充。因此下一步其实就是创建一些HTML并将其填充进去。

我将著名的TodoMVC应用作为灵感。首先我创建两个文件:todo.go和todolist.go。这些文件包含一些Go结构来表示Todo事项,和Todo事项列表。

type Todo struct {
	Title     string
	Completed bool
	//Root      js.Value
	tree *vdom.Tree
}

type TodoList struct {
	Todos []Todo
	Component
}

type Component struct {
	Name     string
	Root     js.Value
	Tree     *vdom.Tree
	Template string
}

我也有点自大,开始将东西提取到Component类型中,并认为我可以将它嵌入到我的自定义类型中,以便向它们提供Web 组件功能。 我没有完成这个想法。。。在后文你会看到原因。

这些自定义Go类型每一个都有一个Render()方法和模板:

var todolisttemplate = `<ul>
{{range $i, $x := $.Todos}} 
	{{$x.Render}} 
{{end}}
</ul>`
func (todoList *TodoList) Render() error {

	tmpl, err := template.New("todolist").Parse(todoList.Template)
	if err != nil {
		return err
	}
	// Execute the template with the given todo and write to a buffer
	buf := bytes.NewBuffer([]byte{})
	if err := tmpl.Execute(buf, todoList); err != nil {
		return err
	}
	// Parse the resulting html into a virtual tree
	newTree, err := vdom.Parse(buf.Bytes())
	if err != nil {
		return err
	}

	if todoList.Tree != nil {
		// Calculate the diff between this render and the last render
		//	patches, err := vdom.Diff(todo.tree, newTree)
}		//	if err != nil {
		//		return err
		//	}

		// Effeciently apply changes to the actual DOM
		//		if err := patches.Patch(todo.Root); err != nil {
		//			return err
		//		}
	} else {

		todoList.Tree = newTree
	}
	// Remember the virtual DOM state for the next render to diff against
	todoList.Tree = newTree

	todoList.Root.Set("innerHTML", string(newTree.HTML()))
	return nil
}

我的想法是用我找到的 vdom 包来做这些渲染,这样的话渲染的效率会更高一些。这就是我遇到的第一个问题。

GopherJS和Go/wasm之间的区别

vdom包专为GopherJS而写,而 GopherJS 是一个从Go到Javascript的转译器。基于便捷,GopherJS使用js.Object类型。Go的新wasm库syscall/js使用js.Value类型。它们精神上是相似的,但在实现上大为不同。这意味着我使用vdom渲染的想法是行不通的,除非我将vdom使用的js.Object移植到使用js.Value。尽管vdom的tree.HTML()函数在不用修改的情况下就可以运行,因此我可以将HTML节点的内部HTML设置为vdom解析出的内容。Render()函数解析Go结构模板,将Go结构的实例作为上下文来传值。然后它用vdom库创建一个解析dom树,而且在函数的最后一行渲染树:

todoList.Root.Set("innerHTML", string(newTree.HTML()))

此时,我已经有了一个可以运行的Go/wasm原型,没有连接任何事件。但是它确实可以渲染成dom并显示在浏览器。这是巨大的一步;我当时很兴奋。

我创建了一个Makefile,这样我就不用一次又一次的输入冗长的编译命令:

wasm2:
	GOROOT=~/gowasm GOARCH=wasm GOOS=js ~/gowasm/bin/go build -o example.wasm markdown.go

wasm:
	GOROOT=~/gowasm GOARCH=wasm GOOS=js ~/gowasm/bin/go build -o example.wasm .

build-server:
	go build -o server-app server/server.go

run: build-server wasm
	./server-app

基于现在的Web Assembly状态,这个makefile也指出了一个至关重要的问题。新型浏览器会忽略WASM文件,除非给他们提供合适的MIME类型。 这篇文章 有一个简单的HTTP文件服务器,它为web assembly文件设置了正确的MIME类型。我将其复制到我的项目,并将其用于应用中。如果你的web服务器确实为.sasm文件配置好了,那么你就不需要自定义服务器。

提出挑战

在这一点上,我意识到Web Assembly可以正常运行,而也许更重要的是:GopherJS的很多代码很少甚至不用修改就可以在Web Assembly可以正常运行。我给自己提出挑战( nerd sniped )。我尝试的下一件事情是找一个 vecty 应用并编译它。由于vecty是专为GopherJS所写,而且使用了js.Object类型而不是js.Vaule,因此要想失败很困难。为了让vecty在wasm中编译,我 fork了vecty ,然后做了一些修改,一些处理,并注释了很多代码。

最终的结果就是放在在vecty/example目录中的markdown编辑器可以在Web Assembly中完美运行。本文有点冗长,因此我会让你 在这 看源码。总结:它与GopherJS版本几乎完全相同,但是在main()退出的时候web assembly也会退出,因此为了阻止退出并保持应用运行,我在main()结尾添加了一个空的通道接收。

事件

Go 的 syscall/js 使用了一个非常不同的方法来进行事件注册,我不得不修改 vecty 的事件 注册代码 才能使用 wasm 新的回调注册,在这里我花了非常多的时间。不过直到现在,这个方法工作的还不错。

结论

图1:WebAssembly 和 Go语言:对未来的观望

通过对这些事件课程的学习,我认定 WebAssembly 就是 Web 开发的未来。它可以使用任何语言作为“前端语言”来进行 Web 开发,然后编译为 wasm 就可以了。这给像我一样并不想再学习 Javascript,而可以使用自己喜欢的语言来进行 Web 开发的人带来了很多好处。

你可以从 这里 下载源代码,不过记住:风险自担。

本文文字及图片出自 OSchina

余下全文(1/3)
分享这篇文章:

请关注我们:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注