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 新的回調注冊,在這里我花了非常多的時間。不過直到現在,這個方法工作的還不錯。
結論

通過對這些事件課程的學習,我認定 WebAssembly 就是 Web 開發的未來。它可以使用任何語言作為“前端語言”來進行 Web 開發,然后編譯為 wasm 就可以了。這給像我一樣并不想再學習 Javascript,而可以使用自己喜歡的語言來進行 Web 開發的人帶來了很多好處。