欢迎来到我们的 WebAssembly 系列教程的第 2 篇。
系列索引
Introduction to WebAssembly Using Go
Accessing DOM from Go using Javascript
在本系列教程的第一篇教程中,我们创建并暴露了一个 Go 的函数,并使用 JavaScript 调用它。如果你还没有阅读第一部分,我强烈建议你先看一下 https://golangbot.com/webassembly-using-go/。
在本教程中,我们将为我们的应用程序开发一个 UI,处理错误,还将从 Go 中操作浏览器的 DOM。
创建 UI 并调用 wasm 函数
让我们使用 HTML 创建一个非常简单的 UI。它将包含一个获取输入 JSON 的文本区域,一个用于格式化输入JSON 的提交按钮,以及另一个用于显示输出的文本区域。
让我们修改 assets
文件夹中现有的 ~/Documents/webassembly/assets/index.html
来包含 UI。
<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>
<textarea id="jsoninput" name="jsoninput" cols="80" rows="20"></textarea>
<input id="button" type="submit" name="button" value="pretty json" onclick="json(jsoninput.value)"/>
<textarea id="jsonoutput" name="jsonoutput" cols="80" rows="20"></textarea>
</body>
<script>
var json = function(input) {
jsonoutput.value = formatJSON(input)
}
</script>
</html>
在上面的 HTML 第 13 行,我们创建了一个 id 为 jsoninput 的文本区域。这将是我们的文本区域,我们在这里输入要格式化的 JSON。
接下来,我们创建一个提交按钮,当按钮被点击时,第 18 行的 json JavaScript 函数将被调用。这个函数将输入的 JSON 作为参数,调用我们在前面的教程中创建的 formatJSON wasm 函数,并将输出设置为第 15 行中定义的 jsonoutput 文本区域。
让我们来编译并运行这个程序,看看它是否有效。
cd ~/Documents/webassembly/cmd/wasm/
GOOS=js GOARCH=wasm go build -o ../../assets/json.wasm
cd ~/Documents/webassembly/cmd/server/
go run main.go
进入浏览器,输入 localhost:9090。你可以看到有两个文本区域和一个按钮的用户界面。
在第一个文本区域输入以下文字。
{"website":"golangbot.com", "tutorials": {"string":"https://golangbot.com/strings/", "maps":"https://golangbot.com/maps/", "goroutine":"https://golangbot.com/goroutines/", "channels":"https://golangbot.com/channels/"}}
现在点开 pretty json 按钮。你可以看到 JSON 被格式化并打印在输出文本区域。
你可以在浏览器中看到上面的输出。我们已经成功地调用了 wasm 函数,并对 JSON 进行了格式化。
使用 JavaScript 从 Go 访问 DOM
在上一节中,我们调用了 wasm 函数,得到了格式化的 JSON 字符串输出,并使用 JavaScript 设置了格式化JSON的输出文本区域。
还有一种方法可以实现同样的输出。不把格式化的 JSON 字符串传给 javascript,而是可以从 Go 中访问浏览器的DOM,将格式化的 JSON 字符串设置到输出文本区域。
让我们来看看是如何实现的。
我们需要修改 ~/Documents/webassembly/cmd/wasm/main.go 中的 jsonWrapper 函数来实现。
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"
}
jsDoc := js.Global().Get("document")
if !jsDoc.Truthy() {
return "Unable to get document object"
}
jsonOuputTextArea := jsDoc.Call("getElementById", "jsonoutput")
if !jsonOuputTextArea.Truthy() {
return "Unable to get output text area"
}
inputJSON := args[0].String()
fmt.Printf("input %s\n", inputJSON)
pretty, err := prettyJson(inputJSON)
if err != nil {
errStr := fmt.Sprintf("unable to parse JSON. Error %s occurred\n", err)
return errStr
}
jsonOuputTextArea.Set("value", pretty)
return nil
})
return jsonfunc
}
在第 6 行,我们尝试从 global 范围内获取 JavaScript 的 document 属性。这个属性是访问输出 JSON 文本区域所需要的。第 7 行的 Truthy 函数是 JavaScript 测试 nil 的方法。如果 truthy 返回 false,则意味着该属性不存在。因此,相应的错误字符串会返回给 JavaScript。我们没有明确地返回 Go 错误类型。原因以及如何处理错误将在下一节介绍。
在第 10 行,我们使用 call 方法调用 jsDoc JavaScript 对象的 getElementById 函数,并将 jsonoutput 参数传递给它。在 JavaScript 中,这行代码对应的是
jsDoc.getElementById("jsonoutput")
如果你还记得,jsonoutput 是 index.html 中输出文本区域的 id。
这将返回对 jsonoutput 文本区的引用。正如我们之前所做的那样,我们检查 truthy。
现在我们可以访问 jsonoutput 文本区了。在第 21 行,我们使用 set 方法将 jsonoutput 文本区域的 value 属性设置为格式化的 JSON 字符串。这样就会在输出文本区显示格式化的 JSON。
Go 方面的程序修改就完成了。
在 ~/Documents/webassembly/assets/index.html 中需要做一个小小的改动。由于 JSON 是通过操作浏览器的DOM 而不是 JavaScript 直接从 Go 中设置的,所以我们可以删除下面的一段代码。
将第 19 行
jsonoutput.value = formatJSON(input)
改成
var result = formatJSON(input)
console.log("Value returned from Go", result)
我们已经删除了 JavaScript 中设置 jsonoutput 值的代码,因为这是从 Go 端完成的。我们只是将结果记录到控制台。如果 JSON 输入有错误,从 jsonfunc 返回的错误字符串将被记录到控制台。
请注意,如果出现错误,输出文本区域不会被清除。它仍然会继续显示其现有的内容。这将在下一节中得到解决。
尝试使用以下命令再次运行程序,然后在浏览器中打开 localhost:9090。
cd ~/Documents/webassembly/cmd/wasm/
GOOS=js GOARCH=wasm go build -o ../../assets/json.wasm
cd ~/Documents/webassembly/cmd/server/
go run main.go
输出将是一样的。如果传递的是一个有效的 JSON,它将被格式化并打印出来。现在是通过操作 DOM 从 Go 代码中完成,而不是从 JavaScript 中完成。如果你传递了一个无效的 JSON,相应的错误将被记录到控制台。
错误处理
在上一节中,当 JSON 格式化过程中发生错误时,我们只是从 jsonfunc 函数中返回一个字符串。
在 Go 中处理错误的惯用方法是返回错误。让我们修改 ~/Documents/webassembly/cmd/wasm/main.go 中的jsonWrapper 函数来返回一个错误,看看会发生什么。
func jsonWrapper() js.Func {
jsonfunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 1 {
return errors.New("Invalid no of arguments passed")
}
jsDoc := js.Global().Get("document")
if !jsDoc.Truthy() {
return errors.New("Unable to get document object")
}
jsonOuputTextArea := jsDoc.Call("getElementById", "jsonoutput")
if !jsonOuputTextArea.Truthy() {
return errors.New("Unable to get output text area")
}
inputJSON := args[0].String()
fmt.Printf("input %s\n", inputJSON)
pretty, err := prettyJson(inputJSON)
if err != nil {
errStr := fmt.Sprintf("unable to parse JSON. Error %s occurred\n", err)
return errors.New(errStr)
}
jsonOuputTextArea.Set("value", pretty)
return nil
})
return jsonfunc
}
第 4 行改为返回一个 error 而不是一个 string。在其他需要返回错误的地方也做了类似的改动。
编译并运行这段代码,尝试输入一个不正确的 JSON,看看会发生什么。我已经提供了无效的 JSON 字符串dfs333{“website 为输入。
程序已经崩溃,堆栈跟踪如下。
input dfs333{"website wasm_exec.js:47:14
panic: ValueOf: invalid value wasm_exec.js:47:14
<empty string> wasm_exec.js:47:14
goroutine 6 [running]: wasm_exec.js:47:14
syscall/js.ValueOf(0x1db00, 0x40e390, 0x6, 0x7ff8000100000017) wasm_exec.js:47:14
/usr/local/go/src/syscall/js/js.go:219 +0x13f wasm_exec.js:47:14
syscall/js.Value.Set(0x7ff8000100000012, 0x41a0d0, 0x3b31e, 0x6, 0x1db00, 0x40e390) wasm_exec.js:47:14
/usr/local/go/src/syscall/js/js.go:314 +0x7 wasm_exec.js:47:14
syscall/js.handleEvent() wasm_exec.js:47:14
/usr/local/go/src/syscall/js/func.go:91 +0x25 wasm_exec.js:47:14
exit code: 2 wasm_exec.js:138:14
Value returned from Go undefined
正如我们在上一篇教程中已经讨论过的,jsonfunc 返回的任何值都会通过 ValueOf 函数自动映射到相应的JavaScript 值。如果你快速看一下这个函数的文档,你可以看到 Go 的 error 类型没有映射到相应的 JavaScript 类型。这就是程序崩溃的原因:panic: ValueOf: invalid value 。当 error 类型从 Go 返回时,目前还没有办法将 Go 的错误传递给 Javascript。这个功能可以在未来添加,但目前还没有。我们必须在返回错误时考虑其他选项。
一种方法是在 Go 和 JavaScript 之间建立一个约定。例如,我们可以从 Go 返回一个 map 给 JavaScript。如果该字典包含一个 error 键,就可以被 JavaScript 认为是一个错误,并进行适当的处理。
让我们修改 jsonWrapper 函数来实现这个目的。
func jsonWrapper() js.Func {
jsonfunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 1 {
result := map[string]interface{}{
"error": "Invalid no of arguments passed",
}
return result
}
jsDoc := js.Global().Get("document")
if !jsDoc.Truthy() {
result := map[string]interface{}{
"error": "Unable to get document object",
}
return result
}
jsonOuputTextArea := jsDoc.Call("getElementById", "jsonoutput")
if !jsonOuputTextArea.Truthy() {
result := map[string]interface{}{
"error": "Unable to get output text area",
}
return result
}
inputJSON := args[0].String()
fmt.Printf("input %s\n", inputJSON)
pretty, err := prettyJson(inputJSON)
if err != nil {
errStr := fmt.Sprintf("unable to parse JSON. Error %s occurred\n", err)
result := map[string]interface{}{
"error": errStr,
}
return result
}
jsonOuputTextArea.Set("value", pretty)
return nil
})
return jsonfunc
}
在上面的代码第 4 行中,创建了一个名为 result 的字典,该字典带有一个错误键,并以相应的错误返回。其他地方也做了类似的改动。现在 JavaScript 端可以检查这个键是否存在。如果这个键存在,说明发生了错误,可以进行适当的处理。
下面提供修改后的 index.html 文件。只对第 17 行开始的 JavaScript 部分进行了修改。
...
<script>
var json = function(input) {
var result = formatJSON(input)
if (( result != null) && ('error' in result)) {
console.log("Go return value", result)
jsonoutput.value = ""
alert(result.error)
}
}
</script>
</html>
首先验证 Go 的返回值是否为空,然后检查是否存在 error 键。如果存在错误键,说明在处理 JSON 时发生了一些错误。首先清除输出文本区域,然后向用户弹出提示,并显示错误信息。
再次编译并运行程序。尝试传递一个无效的 JSON。你可以看到一个带有错误信息的警报。输出文本区域也会被清除。
本教程到此结束。
源代码可以在 https://github.com/golangbot/webassembly/tree/tutorial2。
请留下你的意见和反馈。
如果你想在这个网站上做广告,雇佣我,或者你有任何其他的开发需求,请发邮件到naveen[at]golangbot[dot]com。