WebAssembly和Go:對未來的觀望
我反對學(xué)習(xí) JavaScript 還有前端開發(fā)已經(jīng)不是秘密了。事實上,在 CSS 出現(xiàn)前我就學(xué)會了 HTML,不過 JavaScript 是我做 Web 開發(fā)好久后的事情了。當(dāng)看到現(xiàn)代 Web 的發(fā)展時,我感到不寒而栗。這個生態(tài)對于脫離已久的我來說是如此迷茫。Node, webpack, yarn, npm, frameworks, UMD, AMD,我的天啊!
目前我關(guān)注 WebAssembly 也已經(jīng)有段時間了,期望它能讓我在沒有典型 JavaScript 構(gòu)建的情況下編寫 Web 應(yīng)用程序。
當(dāng)聽到 WebAssembly(wasm) 最近支持 Go 語言時,我知道實驗的時機已經(jīng)成熟,并且迫切期待嘗試。在嘗試之前我讀了些好文章,而這篇文章將記錄我的一些體驗。
為了用 Go 來寫 wasm,你需要先下載 Go 源碼并編譯好。從 Go 1.11 開始,WebAssembly 將被原生支持,但現(xiàn)在還沒有 release。
你可以按照 這里 的步驟來編譯 Go。因為 Go 本身也是用 Go 語言實現(xiàn)的,所以在編譯之前你需要先有一個可以正常工作的 Go 二進制版本來自舉自己。最終,你系統(tǒng)里會有兩個不同的 Go 版本。 注意:如果你后面忘了你系統(tǒng)里安裝了兩個版本的 Go,那可能會給你造成一些困擾。 可以使用 direnv 來管理 Go 版本,這樣你就可以為不同的項目來配置不同的 Go 了。
安裝***的 Go 后,就可以體驗 WebAssembly 了。你需要一個 HTML 文件和一個 JavaScript 腳本來加載生成的 wasm 文件。這些都包含在 Go 安裝路徑下的 misc/wasm 目錄里。你可以復(fù)制它們到項目目錄,修改它們以加載你的 wasm 文件。
我的***個項目有點雄心勃勃,我打算用 Go 語言構(gòu)建一個看起來像 Web 組件 的東西,編譯成 WebAssembly。我并沒有把整件事做完,因為我被每件事要如何都做得好弄得心煩意亂。
首先,我將 GOROOT/misc/wasm 中的 HTML 和 JavaScript 文件復(fù)制到一個新目錄中,并添加了一個 main.go 文件。根據(jù)我預(yù)先想好的計劃,我把 HTML 放進 DOM 的一個現(xiàn)有節(jié)點,這個 DOM 要在 HTML 中聲明。所以我創(chuàng)建了一個帶有 thing 作為 ID 的 HTML section 標(biāo)簽。
- <section class="main" id="thing" >Please wait...</section>
我在 HTML 文件底部的腳本標(biāo)簽上面插入了這個。接下來,我知道我想程序化地替換這個節(jié)點,所以我查找了 Go 的 wasm 庫中與 DOM 交互的語法。為 Go 添加了一個 syscall/js 包,允許與 DOM 進行交互。我使用了這段 Go 代碼得到了一個 HTML 帶有 thing 作為 ID 的節(jié)點的引用:
- el := js.Global.Get("document").Call("getElementById", "thing")
現(xiàn)在我有一個空DOM節(jié)點的引用,我可以使用渲染的HTML來填充。因此下一步其實就是創(chuàng)建一些HTML并將其填充進去。
我將著名的TodoMVC應(yīng)用作為靈感。首先我創(chuàng)建兩個文件:todo.go和todolist.go。這些文件包含一些Go結(jié)構(gòu)來表示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類型中,并認(rèn)為我可以將它嵌入到我的自定義類型中,以便向它們提供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之間的區(qū)別
vdom包專為GopherJS而寫,而 GopherJS 是一個從Go到Javascript的轉(zhuǎn)譯器?;诒憬?,GopherJS使用js.Object類型。Go的新wasm庫syscall/js使用js.Value類型。它們精神上是相似的,但在實現(xiàn)上大為不同。這意味著我使用vdom渲染的想法是行不通的,除非我將vdom使用的js.Object移植到使用js.Value。盡管vdom的tree.HTML()函數(shù)在不用修改的情況下就可以運行,因此我可以將HTML節(jié)點的內(nèi)部HTML設(shè)置為vdom解析出的內(nèi)容。Render()函數(shù)解析Go結(jié)構(gòu)模板,將Go結(jié)構(gòu)的實例作為上下文來傳值。然后它用vdom庫創(chuàng)建一個解析dom樹,而且在函數(shù)的***一行渲染樹:
- todoList.Root.Set("innerHTML", string(newTree.HTML()))
此時,我已經(jīng)有了一個可以運行的Go/wasm原型,沒有連接任何事件。但是它確實可以渲染成dom并顯示在瀏覽器。這是巨大的一步;我當(dāng)時很興奮。
我創(chuàng)建了一個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
基于現(xiàn)在的Web Assembly狀態(tài),這個makefile也指出了一個至關(guān)重要的問題。新型瀏覽器會忽略WASM文件,除非給他們提供合適的MIME類型。 這篇文章 有一個簡單的HTTP文件服務(wù)器,它為web assembly文件設(shè)置了正確的MIME類型。我將其復(fù)制到我的項目,并將其用于應(yīng)用中。如果你的web服務(wù)器確實為.sasm文件配置好了,那么你就不需要自定義服務(wù)器。
提出挑戰(zhàn)
在這一點上,我意識到Web Assembly可以正常運行,而也許更重要的是:GopherJS的很多代碼很少甚至不用修改就可以在Web Assembly可以正常運行。我給自己提出挑戰(zhàn)( nerd sniped )。我嘗試的下一件事情是找一個 vecty 應(yīng)用并編譯它。由于vecty是專為GopherJS所寫,而且使用了js.Object類型而不是js.Vaule,因此要想失敗很困難。為了讓vecty在wasm中編譯,我 fork了vecty ,然后做了一些修改,一些處理,并注釋了很多代碼。
最終的結(jié)果就是放在在vecty/example目錄中的markdown編輯器可以在Web Assembly中***運行。本文有點冗長,因此我會讓你 在這 看源碼??偨Y(jié):它與GopherJS版本幾乎完全相同,但是在main()退出的時候web assembly也會退出,因此為了阻止退出并保持應(yīng)用運行,我在main()結(jié)尾添加了一個空的通道接收。
事件
Go 的 syscall/js 使用了一個非常不同的方法來進行事件注冊,我不得不修改 vecty 的事件 注冊代碼 才能使用 wasm 新的回調(diào)注冊,在這里我花了非常多的時間。不過直到現(xiàn)在,這個方法工作的還不錯。
結(jié)論

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