原文:https://golangbot.com/go-webassembly-dom-access/
欢迎来到WebAssembly教程系列的第二篇。
在本教程系列的第一篇中,我们用Go创建并暴露了一个函数给JavaScript来调用。如果你还没有看过第一篇,强烈建议先看一下再回来。
在本篇中,我们将为我们的应用开发一个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函数将被调用。该函数以输入的JSON作为参数,调用我们在前一篇中创建的formatJSON函数,然后将输出设置为第15行所定义的jsonoutput文本区的value属性。
我们来编译并运行这段程序,看看它是否工作。
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。你可以看到由两个文本区和一个按钮组成的UI。
在第一个文本区输入以下文本:
{"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。
从Go使用JavaScript访问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属性。要访问输出文本区就必须先获取该属性。第7行的Truthy函数是JavaScript测试nil的方法。如果Truthy返回false,意思是该属性不存在。因此要返回一个相应的错误信息给JavaScript。我们不能直接返回一个Go error类型。下一节将讨论为什么会这样以及如何处理错误。
在第10行,我们用Call方法来调用jsDoc对象上的getElementById函数,并传入jsonoutput参数。在JavaScript中,这行代码对应于:
jsDoc.getElementById("jsonoutput")
回想一下,jsonoutput是index.html中的输出文本区的id。
这将返回jsonoutput文本区的引用。和之前一样,我们用Truthy来验证一下。
现在我们已可以访问jsonoutput文本区了。在第21行,我们用Set方法将jsonoutput文本区的value属性设置为格式化后的JSON串。这样格式化的JSON串将被显示在输出文本区。
Go这一边的程序修改就这么多了。
我们还需要对~/Documents/webassemblu/assets/index.html做少许改动。由于JSON串已由Go直接操纵浏览器的DOM来完成显示了,就不需要在JavaScript中来做同样的工作了,我们可以把相应代码段删掉。
把index.html中的第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,则会被格式化并打印出来。不过现在这个工作是由Go通过操纵DOM来完成的,而不是由JavaScript完成。如果你传入一个非法JSON,则相应的错误会记录在控制台中。
错误处理
在前一节中,在JSON格式化过程中,如果发生错误,我们只是从jsonfunc函数返回一个字符串。
在Go中处理错误的通常做法是返回一个error。我们来试一下,把~/Documents/webassembly/cmd/wasm/main.go中的jsonWrapper函数改为返回一个error,看看会发生什么。
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。其它几个要返回error的地方也做了同样的修改。
编译,运行,然后输入一个错误的JSON,看看会发生什么。下例中我输入了一个非法的JSON串。
程序崩溃了,输出以下栈跟踪信息。
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类型可以映射。这正是以上程序在Go返回一个error类型时发生了"panic: ValueOf: invalid value"错误并崩溃的原因。目前来说,没有办法从Go返回一个error给JavaScript。以后可能会增加这一特性,但至少目前是不行的。我们必须看看有没有其它方式来返回error。
一种方法是,在Go和JavaScript间建立一个协议。例如,我们可以从Go返回一个map给JavaScript。如果这个map带有一个error的键,则可以被JavaScript视为一个error并进行相应的处理。
我们来修改一下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的map并返回,它带有一个error的键。在其它几个地方也做了类似的改动。JavaScript一侧现在可以检查error键是否存在。如果存在,这个返回值就表示发生了某种错误,可以进行相应的处理。
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返回的值首先检查是否为null,然后检查是否存在error键。如果存在,则表示在处理JSON时发生了某种错误。输出文本区要被清空,然后弹出一个带有错误信息的警告给用户。
本教程到此为止。
教程中的所有源代码可以访问https://github.com/golangbot/webassembly/tree/tutorial2。