用 Go 來開發 WebAssembly 入門(1)

原文:https://golangbot.com/webassembly-using-go/

歡迎來到 WebAssembly 教程系列的第一篇。

WebAssembly 是什麼?

JavaScript 已成爲瀏覽器可以理解的唯一語言。它經歷了時間的考驗,可以滿足大多數 web 應用的性能需求。但是,當遇到 3D 遊戲、VR、AR 以及圖像編輯等應用的時候,JavaScript 就不那麼好用了,其原因是它是一種解釋性的語言。雖然像 Gecko 和 V8 這樣的 JavaScript 引擎已具備 JIT 特性,但 JavaScript 還是不能完全滿足現代 web 應用所需的高性能。

WebAssembly(又稱 wasm)的目標就是解決這個問題。它是一種專爲瀏覽器設計的虛擬彙編語言。所謂虛擬,意思就是它不能直接運行於底層的硬件之上。因爲瀏覽器可能運行在任意體系的硬件上,所以瀏覽器不可能讓 WebAssembly 直接運行於底層硬件之上。但是,WebAssembly 採用了高度優化的虛擬彙編格式,它在瀏覽器中運行時要比普通的 JavaScript 快得多,這是由於它是編譯型的而且比 JavaScript 更靠近硬件體系。下圖顯示了 WebAssembly 與 JavaScript 在棧中的位置。它比 JavaScript 更靠近硬件一些。

現有的 JavaScript 引擎基本上都可以支持 WebAssembly 虛擬彙編代碼的運行。

**WebAssembly 的目標並不是替代 JavaScript。它的目標是與 JavaScript 一起配合,以實現 web 應用中性能敏感的部分。**可以從 JavaScript 調用 WebAssembly,反之亦然。

WebAssembly 通常並不需要手工編寫彙編代碼,而是從其它高級語言編譯得到。例如,可以從 Go、C、C++ 或 Rust 等代碼編譯得到 WebAssembly。因此,在其它語言中已實現的模塊也可以被編譯成 WebAssembly,從而在瀏覽器中直接使用。

如何開發?

在本教程中,我們將把一個 Go 程序編譯爲 WebAssembly 並在瀏覽器中運行它。

我們將創建一個對 JSON 進行格式化的簡單程序。如果輸入的是一個未格式化的 JSON 串,我們把它格式化後再打印出來。

例如,輸入的 JSON 如下: 

{"website":"golangbot.com", "tutorials": {"string":"https://golangbot.com/strings/", "maps":"https://golangbot.com/maps/", "goroutine":"https://golangbot.com/goroutines/", "channels":"https://golangbot.com/channels/"}}

它會被格式化並在瀏覽器中顯示以下內容:

{
  "tutorials": {
    "channels": "https://golangbot.com/channels/",
    "goroutine": "https://golangbot.com/goroutines/",
    "maps": "https://golangbot.com/maps/",
    "string": "https://golangbot.com/strings/"
  },
  "website": "golangbot.com"
}

 我們還會爲這個應用創建一個 UI,並在 Go 語言中操縱瀏覽器的 DOM,不過這個要留到下一個教程。

本教程代碼在 Go 1.13 以上版本中測試通過。

從 Go 編譯 WebAssembly 的 Hello World 程序

我們從編寫一個 Go 的最簡單的 hello world 程序開始,把它編譯成 WebAssembly 並在瀏覽器上運行。然後我們再修改這個程序,把它變成我們的 JSON 格式化應用。

我們先來創建以下目錄結構,比如在 Documents 目錄之下:

Documents/  
└── webassembly
    ├── assets
    └── cmd
        ├── server
        └── wasm

後面將逐步明晰各個文件夾的用途。

在~/Documents/webassembly/cmd/wasm 目錄下創建一個 main.go 文件,文件內容如下:

package main
 
import (  
    "fmt"
)
 
func main() {  
    fmt.Println("Go Web Assembly")
}

我們來把它編譯成 WebAssembly。以下命令將對這個 Go 程序進行編譯並將輸入的二進制文件存放在 assets 文件夾中:

cd ~/Documents/webassembly/cmd/wasm/  
GOOS=js GOARCH=wasm go build -o  ../../assets/json.wasm

以上命令使用 js 作爲 GOOS,wasm 作爲 GOARCH,wasm 是 WebAssembly 的縮寫。執行這一命令將在 assets 目錄下創建一個名爲 json.wasm 的 WebAssembly 模塊。恭喜!我們已經成功將第一個 Go 程序編譯成 WebAssembly 了。

有一個重要的提示就是,只能將 main 包編譯成 WebAssembly。所以我們必須把所有代碼都寫在 main 包中。

如果你試圖在終端上運行這個二進制文件,像這樣:

$]~/Documents/webassembly/assets/json.wasm 
 
-bash: json.wasm: cannot execute binary file: Exec format error

你將收到一個錯誤提示。這是因爲這個二進制文件是一個 wasm 二進制文件,只能在瀏覽器沙盒中運行。Linux 或 Mac 操作系統無法理解這種格式,所以提示錯誤。

JavaScript 膠水

前面已提到過,WebAssembly 是需要和 JavaScript 一起配合運行的。因些我們需要一些 JavaScript 膠水代碼來引入我們剛纔得到的 WebAssembly 模塊,以便在瀏覽器中運行。這些膠水代碼已存在於 Go 的安裝路徑下。我們只需要把它複製到我們的 assets 目錄下即可:

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ~/Documents/webassembly/assets/

以上命令將 wasm_exec.js 複製到 assets 目錄下,這個 js 文件中包含了用於運行 WebAssembly 的膠水代碼。

現在你應該已知道,assets 文件夾的用途就是用於存放我們的 web 服務器所要使用的所有 HTML、JavaScript 和 wasm 代碼。

Index.html

現在我們已經準備好 wasm 二進制文件以及膠水代碼了。下一步就是創建一個 index.html 文件用於導入我們的 wasm 文件。

我們創建一下 index.html 文件,還是在 assets 目錄下,文件內容如下。文件中包含了運行 WebAssembly 模塊所需的樣板代碼,參看 WebAssembly Wiki

<html>  
    <head>
        <meta charset="utf-8"/>
        <script src="wasm_exec.js"></script>
        <script>
            const go = new Go();
            WebAssembly.instantiateStreaming(fetch("json.wasm"), go.importObject).then((result) => {
                go.run(result.instance);
            });
        </script>
    </head>
    <body></body>
</html>

創建 index.html 之後的目錄結構如下:

Documents/  
└── webassembly
    ├── assets
    │   ├── index.html
    │   ├── json.wasm
    │   └── wasm_exec.js
    └── cmd
        ├── server
        └── wasm
            └── main.go

雖然 index.html 只是一個標準的樣板,但是稍微瞭解一下也無妨。我們來嘗試瞭解一下 index.html 中的代碼。instantiateStreaming 函數是用來初始化我們的 json.wasm 模塊。這個函數返回一個 WebAssembly 實例,實例中包含一個可以被 JavaScript 調用的 WebAssembly 函數列表。要從 JavaScript 中調用我們的 wasm 函數,這一步是必須的。隨着教程的繼續,它的用法會越來越清晰。

Web 服務器

現在我們已經準備好了 JavaScript 膠水、index.html 以及我們的 wasm 二進制文件。最後還缺少的一塊就是,我們需要一個 web 服務器來提供 assets 文件夾中的這些內容。開幹吧!

在 server 目錄下創建一個 main.go 文件,目錄結構將變成:

Documents/  
└── webassembly
    ├── assets
    │   ├── index.html
    │   ├── json.wasm
    │   └── wasm_exec.js
    └── cmd
        ├── server
        |   └── main.go
        └── wasm
            └── main.go

將以下代碼輸入~/Documents/webassembly/cmd/server/main.go:

package main
 
import (  
    "fmt"
    "net/http"
)
 
func main() {  
    err := http.ListenAndServe(":9090", http.FileServer(http.Dir("../../assets")))
    if err != nil {
        fmt.Println("Failed to start server", err)
        return
    }
}

以上程序創建一個文件服務器,在 9090 端口監聽,以我們的 assets 文件夾作爲根目錄。這就是我們想要的。我們來運行這個服務器,看看我們第一個 WebAssembly 程序開始運行。

cd ~/Documents/webassembly/cmd/server/  
go run main.go

服務器監聽於 9090 端口。啓動你喜歡的瀏覽器,敲入 http://localhost:9090。你會看到頁面是空的。不要緊張,後面我們會創建 UI 的。

現在我們要關注的是 JavaScript 控制檯。在瀏覽器中右擊鼠標並選擇 "檢查"。 

開發者控制檯將被打開。選擇 "控制檯" 頁。

 你將看到控制檯中有一段 "Go Web Assembly" 文字。漂亮!我們已經成功運行了我們的第一個用 Go 編寫的 WebAssembly 程序。從 Go 編譯生成的 WebAssembly 模塊已被我們的服務器提供給到瀏覽器,並被瀏覽器的 JavaScript 引擎正確執行。

下面我們再進一步,開始編寫我們的 JSON 格式化器。

編寫 JSON 格式化器

我們的 JSON 格式化器以未格式化的 JSON 作爲輸入,對其進行格式化,並返回格式化後的 JSON 字符串作爲輸出。我們可以用 MarshalIndent 函數來實現。 

把以下函數加入到~/Documents/webassembly/cmd/wasm/main.go:

func prettyJson(input string) (string, error) {  
    var raw interface{}
    if err := json.Unmarshal([]byte(input), &raw); err != nil {
        return "", err
    }
    pretty, err := json.MarshalIndent(raw, "", "  ")
    if err != nil {
        return "", err
    }
    return string(pretty), nil
}

MarshalIndent 函數有 3 個輸入參數。第一個是未格式化的 JSON 串,第二個是要加到每行 JSON 前的前綴。這裏我們不需要加前綴。第三個參數是每行 JSON 的縮入要增加的字符串。這裏我們給定兩個空格。這樣,格式化時每次 JSON 行要縮入,就會多增加兩個空格。 

如果把字符串 {"website":"golangbot.com", "tutorials": {"string":"https://golangbot.com/strings/"}} 作爲輸入傳給以上函數,它將返回以下格式化 JSON 字符串:

{
  "tutorials": {
    "string": "https://golangbot.com/strings/"
  },
  "website": "golangbot.com"
}

從 Go 暴露函數給 JavaScript

我們的函數已經準備好了,但是我們還沒有把這個函數暴露給 JavaScript 以使得該函數可以從前端調用。

Go 提供了 syscall/js 包,可以幫助我們從 Go 把函數暴露給 JavaScript。

暴露函數給 JavaScript 的第一步是創建一個 Func 類型。Func 是一個被包裹的 Go 函數,它可以被 JavaScript 調用。可以用 FuncOf 函數來創建 Func 類型。

把以下函數加入到~/Documents/webassembly/cmd/wasm/main.go:

func jsonWrapper() js.Func {  
        jsonFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
                if len(args) != 1 {
                        return "Invalid no of arguments passed"
                }
                inputJSON := args[0].String()
                fmt.Printf("input %s\n", inputJSON)
                pretty, err := prettyJson(inputJSON)
                if err != nil {
                        fmt.Printf("unable to convert to json %s\n", err)
                        return err.Error()
                }
                return pretty
        })
        return jsonFunc
}

FuncOf 函數接受一個函數類型的參數,該類型的函數應帶有兩個參數以及一個 interface{} 返回類型。傳遞給 FuncOf 的函數將會被 JavaScript 以同步方式調用。這個函數的第一個參數是 JavaScript 的 this 關鍵字。this 指向 JavaScript 的 global 對象。第二個參數是一個 []js.Value 切片,表示被傳入到這個 JavaScript 函數調用的所有參數。在這個例子中,應該只傳入一個參數,即未格式化的 JSON 字符串。如果你覺得不太好理解也不要緊。程序完成的時候你會搞清楚的:)。

我們首先檢查傳入的參數數量是否爲 1(第 3 行)。這個檢查是需要的,因爲我們希望只有一個 JSON 串參數。如果不是這樣,我們就返回一個字符串信息 Invalid no of arguments passwd。我們不能從 Go 直接返回一個 error 類型給 JavaScript。下一節教程會討論如何進行錯誤處理。

我們用 args[0].String() 來獲取輸入的 JSON。這表示從 JavaScript 傳入的第一個參數。獲取輸入的 JSON 後,我們就調用 prettyJson 函數(第 8 行),將將結果返回。

從 Go 返回一個值給 JavaScript 時,編譯器將自動使用 ValueOf 函數將 Go 值轉換爲 JavaScript 值。在這個例子中,我們從 Go 返回的是一個 string,它會被編譯器用 js.ValueOf() 函數轉換爲相應的 JavaScript 字符串類型。

我們將 FuncOf 的返回值賦值給 jsonFunc。這樣 jsonFunc 就是一個可以從 JavaScript 調用的函數了。最後我們返回 jsonFunc(第 15 行)。

現在我們有了一個可以從 JavaScript 調用的函數了。最後還差一步。

我們需要把剛剛創建的這個函數暴露出去,以使得可以從 JavaScript 調用。方法是,把 JavaScript 的 global 對象的 formatJSON 屬性設置爲 jsonWrapper() 返回的 js.Func。

以下這行代碼就是幹這個的:

js.Global().Set("formatJSON", jsonWrapper())

把這行代碼加入到 main() 函數的最後。在這行代碼中,我們將 JavaScript 的 global 對象的 formatJSON 屬性設置爲 jsonWrapper() 函數的返回值。現在負責對 JSON 串進行格式化的 jsonFunc 函數可以從 JavaScript 以函數名 formatJSON 來調用了。

完整的程序代碼如下:

package main
 
import (  
    "fmt"
    "encoding/json"
    "syscall/js"
)
 
func prettyJson(input string) (string, error) {  
        var raw interface{}
        if err := json.Unmarshal([]byte(input), &raw); err != nil {
                return "", err
        }
        pretty, err := json.MarshalIndent(raw, "", "  ")
        if err != nil {
                return "", err
        }
        return string(pretty), nil
}
 
func jsonWrapper() js.Func {  
        jsonFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
                if len(args) != 1 {
                        return "Invalid no of arguments passed"
                }
                inputJSON := args[0].String()
                fmt.Printf("input %s\n", inputJSON)
                pretty, err := prettyJson(inputJSON)
                if err != nil {
                        fmt.Printf("unable to convert to json %s\n", err)
                        return err.Error()
                }
                return pretty
        })
        return jsonFunc
}
 
func main() {  
    fmt.Println("Go Web Assembly")
    js.Global().Set("formatJSON", jsonWrapper())
}

我們來編譯並測試一下這個程序。

cd ~/Documents/webassembly/cmd/wasm/  
GOOS=js GOARCH=wasm go build -o  ../../assets/json.wasm  
cd ~/Documents/webassembly/cmd/server/  
go run main.go

以上命令將編譯 wasm 二進制並啓動我們的 web 服務器。

從 JavaScript 調用 Go 函數

我們已經成功地將 Go 函數暴露給了 JavaScript。我們來檢查一下它能不能用。

再次從瀏覽器打開 http://localhost:9090,並打開 JavaScript 控制檯。

在控制檯中輸入以下命令:

formatJSON('{"website":"golangbot.com", "tutorials": {"string":"https://golangbot.com/strings/"}}')

以上命令調用了我們從 Go 暴露出去的 formatJSON 函數,傳入一個 JSON 參數。敲回車,能否成功?

對不起:),你得到的是一個錯誤提示:Error: Go program has already exited

原因是,當從 JavaScript 調用的時候,我們的 Go 程序已經退出了。怎麼辦?很簡單,我們必須確保在 JavaScript 調用的時候,我們的 Go 程序還在運行。最簡單的辦法就是在 Go 程序中持續等待一個 channel。

func main() {  
        fmt.Println("Go Web Assembly")
        js.Global().Set("formatJSON", jsonWrapper())
        <-make(chan bool)
}

在這段代碼中,我們在一個 channel 上持續等待輸入。把最後一行代碼加入到~/Documents/webassembly/cmd/wasm/main.go 中,編譯並重新運行。然後在瀏覽器中再次嘗試以下命令:

formatJSON('{"website":"golangbot.com", "tutorials": {"string":"https://golangbot.com/strings/"}}')

這一次可以正確打印出格式化的 JSON 了:

如果我們不傳入參數:

formatJSON()

則會得到以下提示:

"Invalid no of arguments passed"

漂亮!我們已經成功地從 JavaScript 調用了用 Go 編寫的函數。

本教程的源代碼可以從 https://github.com/golangbot/webassembly/tree/tutorial1/ 處獲取。

在下一節教程中,我們將爲這個應用創建 UI,進行錯誤處理,並從 Go 修改瀏覽器的 DOM。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://blog.csdn.net/alai04/article/details/112259160