我反对学习 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.
你可以按照 这里 https://golang.org/doc/install/source 的步骤来编译 Go. 因为 Go 本身也是用 Go 语言实现的, 所以在编译之前你需要先有一个可以正常工作的 Go 二进制版本来自举自己. 最终, 你系统里会有两个不同的 Go 版本. 注意: 如果你后面忘了你系统里安装了两个版本的 Go, 那可能会给你造成一些困扰. 可以使用 http://direnv.net/ 来管理 Go 版本, 这样你就可以为不同的项目来配置不同的 Go 了.
安装最新的 Go 后, 就可以体验 WebAssembly 了. 你需要一个 HTML 文件和一个 JavaScript 脚本来加载生成的 wasm 文件. 这些都包含在 Go 安装路径下的 misc/wasm 目录里. 你可以复制它们到项目目录, 修改它们以加载你的 wasm 文件.
我的第一个项目有点雄心勃勃, 我打算用 Go 语言构建一个看起来像 Web 组件 https://www.webcomponents.org/ 的东西, 编译成 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
- }
我的想法是用我找到的 https://github.com/albrow/vdom 包来做这些渲染, 这样的话渲染的效率会更高一些. 这就是我遇到的第一个问题.
GopherJS 和 Go/wasm 之间的区别
vdom 包专为 GopherJS 而写, 而 GopherJS https://gopherjs.org/ 是一个从 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 类型. 这篇文章 https://blog.owulveryck.info/2018/06/08/some-notes-about-the-upcoming-webassembly-support-in-go.html 有一个简单的 HTTP 文件服务器, 它为 web assembly 文件设置了正确的 MIME 类型. 我将其复制到我的项目, 并将其用于应用中. 如果你的 web 服务器确实为. sasm 文件配置好了, 那么你就不需要自定义服务器.
提出挑战
在这一点上, 我意识到 Web Assembly 可以正常运行, 而也许更重要的是: GopherJS 的很多代码很少甚至不用修改就可以在 Web Assembly 可以正常运行. 我给自己提出挑战 ( nerd sniped https://xkcd.com/356/ ). 我尝试的下一件事情是找一个 https://github.com/gopherjs/vecty 应用并编译它. 由于 vecty 是专为 GopherJS 所写, 而且使用了 js.Object 类型而不是 js.Vaule, 因此要想失败很困难. 为了让 vecty 在 wasm 中编译, 我 fork 了 vecty https://github.com/gowasm/vecty , 然后做了一些修改, 一些处理, 并注释了很多代码.
最终的结果就是放在在 vecty/example 目录中的 markdown 编辑器可以在 Web Assembly 中完美运行. 本文有点冗长, 因此我会让你 在这 https://github.com/bketelsen/wasmplay/tree/master/markdownvecty 看源码. 总结: 它与 GopherJS 版本几乎完全相同, 但是在 main() 退出的时候 web assembly 也会退出, 因此为了阻止退出并保持应用运行, 我在 main() 结尾添加了一个空的通道接收.
事件
Go 的 syscall/js 使用了一个非常不同的方法来进行事件注册, 我不得不修改 vecty 的事件 注册代码 https://github.com/gowasm/vecty/blob/wasm-wip/dom.go#L231 才能使用 wasm 新的回调注册, 在这里我花了非常多的时间. 不过直到现在, 这个方法工作的还不错.
结论
通过对这些事件课程的学习, 我认定 WebAssembly 就是 Web 开发的未来. 它可以使用任何语言作为 "前端语言" 来进行 Web 开发, 然后编译为 wasm 就可以了. 这给像我一样并不想再学习 Javascript, 而可以使用自己喜欢的语言来进行 Web 开发的人带来了很多好处.
你可以从 这里 https://github.com/bketelsen/wasmplay 下载源代码, 不过记住: 风险自担.
来源: http://www.mzh.ren/webassembly-and-go-a-look-to-the-future.html