欢迎来到我们的 WebAssembly 系列教程的第1篇。我们的 WebAssembly 系列教程的第 1 篇。
系列索引
Introduction to WebAssembly Using Go
DOM Access and Error Handling
什么是 WebAssembly?
JavaScript 一直是唯一能跑在浏览器上的编程语言。JavaScript 经受住了时间的考验,它已经能够提供大多数网络应用所需的性能。但当涉及到 3D 游戏、VR、AR 和图像编辑应用时,JavaScript 就不太符合要求了,因为它是解释型语言。虽然 Gecko 和 V8 等JavaScript 引擎具有 Just in Time(JIT) 编译功能,但 JavaScript 无法提供现代 Web 应用所需的高性能。
WebAssembly(也称作 wasm)就是为了解决这个问题。WebAssembly 是一种针对浏览器的虚拟汇编语言。当我们说虚拟时,意味着它不能在底层硬件上原生运行。由于浏览器可以运行在任何架构上,所以浏览器不可能直接在底层硬件上运行 WebAssembly。但是这种高度优化的虚拟汇编格式由于经过编译,比 JavaScript 更接近硬件架构,所以比现代浏览器处理普通的 JavaScript 的速度快很多。下图展示了 WebAssembly 与 Javascript 相比,在堆栈中的位置。它比 JavaScript 更接近硬件。
![](https://cdn.nlark.com/yuque/0/2020/png/137440/1594537422710-83e8a4e0-4968-401f-8e5c-0ea5889162fe.png#align=left&display=inline&height=181&margin=%5Bobject%20Object%5D&originHeight=181&originWidth=121&size=0&status=done&style=none&width=121)
现有的 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,并使用 Javascript 从 Go 中操作浏览器的 DOM,但这是在下一个教程中。
本教程使用 Go 版本 >= 1.13 进行了测试。
Go 交叉编译的 Hello World WebAssembly 程序
我们先用 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")
}
让我们把上面的 Go 程序交叉编译成 WebAssembly。下面的命令将交叉编译这个 Go 程序,并将输出的二进制文件放在 assets 文件夹中。
cd ~/Documents/webassembly/cmd/wasm/
GOOS=js GOARCH=wasm go build -o ../../assets/json.wasm
上面的命令使用 js 作为 GOOS,使用 wasm (WebAssembly 的缩写形式)作为架构。运行上面的命令会在 assets 目录下创建 WebAssembly 模块 json.wasm。恭喜你,我们已经成功地将第一个Go程序交叉编译到了WebAssembly中😀。
有一个重要的事实是,只有 main package 可以交叉编译到 WebAssembly。因此,我们在 main package 中写了我们的代码。
如果你尝试在终端运行这个编译后的二进制文件。
$]~/Documents/webassembly/assets/json.wasm
-bash: json.wasm: cannot execute binary file: Exec format error
你会得到 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/
上面的命令将包含运行 WebAssembly 胶水代码的 wasm_exec.js 复制到 assets 目录下。
现在你已经猜到了,assets 文件夹将包含所有的 HTML、JavaScript 和 wasm 代码,这些代码将在以后使用 Web 服务器提供服务。
Index.html
现在我们已经准备好了 wasm 二进制文件,也有了胶水代码。下一步是创建 index.html 文件并导入 wasm 二进制文件。
让我们在 assets 目录下创建一个名为 index.html 的文件,内容如下。这个文件包含了运行 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 模块。这个函数返回一个WebAssembly 实例,其中包含了可以从 JavaScript 中调用的 WebAssembly 函数列表。这是需要从 JavaScript中调用我们的 wasm 函数的。随着教程的深入,这个用法会更加清晰。
WebServer
现在我们已经准备好了 JavaScript 胶水、index.html 和 wasm 二进制。唯一缺少的是我们需要创建一个 webserver 来提供 assets 文件夹的内容。让我们现在就来做。
在 server 目录下创建一个名为 main.go 的文件。创建 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/。你可以看到页面是空的。不要担心,我们将在接下来的章节中创建用户界面。
我们现在感兴趣的是查看J avaScript 控制台。右键点击并选择浏览器中的 inspect element。
这将打开开发者控制台。点击名为 “console “的标签。
你可以看到控制台中打印出了 Go Web Assembly 的文本。太棒了,我们已经成功运行了第一个用 Go 编写的 Web 汇编程序。我们从 Go 交叉编译的 Web 汇编模块已经由我们的服务器传送到了浏览器,并且已经被浏览器的 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 的每一个新的缩进,都会添加两个空格,因此 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 中。
将函数从 Go 暴露到 JavaScript 的第一步是创建一个 Func 类型。Func 是一个可以被 JavaScript 调用的封装的Go 函数。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 输入字符串。如果这没有意义,不要担心。一旦程序完成,你将能够更好地理解:)。
我们在第三行首先检查 Javascript 是否只传递了一个参数,这个检查是需要的,因为我们希望只有一个JSON字符串参数。如果没有,我们将返回一个字符串消息,说明传递的参数数量无效。我们没有明确地从 Go 返回任何错误类型给 Javascript。错误处理将在下一个教程中处理。
我们使用 args[0].String() 来获取 JSON 输入。这代表了从 JavaScript 传递的第一个参数。随着教程的深入,这一点将更加清晰。在得到输入的 JSON 后,我们调用第 8 行的 prettyJson 函数,并返回输出。
当从 Go 向 Javascript 返回一个值时,编译器会自动使用 ValueOf 函数将 Go 的值转换为 JavaScript 的值。在本例中,我们从 Go 中返回的是一个 string,因此它将被编译器使用 js.ValueOf() 转换为相应的 JavaScript 的字符串类型。
我们将 FuncOf 的返回值分配给 jsonFunc。现在 jsonFunc 包含了将从 Javascript 中调用的函数。我们在第 15 行返回 jsonFunc。
现在我们已经准备好了可以从 Javascript 中调用的函数。我们还差一步。
我们需要暴露我们刚刚创建的函数,以便它可以从 Javascript 中调用。我们向 Javascript 暴露 Go 函数的方法是将 JavaScript 全局对象的 formatJSON 字符串属性设置为 jsonWrapper() 返回的 js.Func。
执行此操作的代码行是
js.Global().Set("formatJSON", jsonWrapper())
将其添加到 main() 函数的末尾。在上面的代码中,我们已经将 Javascript 的 Global 对象的 formatJSON 属性设置为 jsonWrapper() 函数的返回值。现在可以使用函数 formatJSON 调用格式化 JSON 的 jsonFunc。
下面是完整的程序。
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 控制台。
在 Javascript 控制台中输入以下命令。
formatJSON('{"website":"golangbot.com", "tutorials": {"string":"https://golangbot.com/strings/"}}')
上面的命令调用了我们从 Go 中导出的 formatJSON JavaScript 函数,并将其作为 JSON 字符串作为参数传递。点击回车。成功了吗?
对不起:) 失败鸟。你会得到错误 Error: Go program has already exited
正如错误中提到的原因是我们的围棋程序在被 Javascript 调用时已经退出了。我们该如何解决这个🤔?嗯,很简单。我们必须确保当 JavaScript 调用 Go 程序的时候,Go 程序是在运行的。在 Go 中,简单的方法就是在一个 channel 上继续等待。
func main() {
fmt.Println("Go Web Assembly")
js.Global().Set("formatJSON", jsonWrapper())
<-make(chan bool)
}
在上面的代码段中,我们正在等待一个通道,请将上面代码段的最后一行添加到~/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 编写的函数。我们已经成功地从 JavaScript 中调用了一个用 Go 编写的函数。
本教程的源代码可以在 https://github.com/golangbot/webassembly/tree/tutorial1/ 找到。
在接下来的教程中,我们将为我们的应用程序创建一个 UI,处理错误,还将从 Go 中修改浏览器的 DOM。
如果你想在这个网站上做广告,雇佣我,或者你有其他开发需求,请发邮件到 naveen[at]golangbot[dot]com。
谢谢你的阅读。请留下你的意见和反馈。
Next tutorial - DOM Access and Error Handling
**