跳转到主要内容

更新 - 本教程中的代码已更新以适用于 Go v1.12 中的重大更改

欢迎大家! 刚刚发布的 Go v1.11 包含了 WebAssembly 的实验性端口,我想看看我们如何编写自己的 Go 程序直接编译到 WebAssembly 会很棒!

因此,在本文中,我们将构建一个非常简单的计算器,让我们了解如何编写可以暴露给前端的函数、评估 DOM 元素并随后使用来自任何结果的结果更新任何 DOM 元素 我们调用的函数。

希望这将向您展示如何为您的前端应用程序编写和编译您自己的基于 Go 的程序。

注意 - 如果您还没有从开头猜到,则需要 Go v1.11 才能使本教程正常工作!

视频教程


如果您想支持我和我的努力,请查看本教程的视频版本并订阅我的频道!

https://youtu.be/4kBvvk2Bzis

介绍


那么这对 Go 和 Web 开发人员来说究竟意味着什么呢?好吧,它使我们能够使用 Go 语言编写我们的前端 Web 应用程序,以及随后的所有很酷的功能,例如它的类型安全、goroutines 等等。

现在,这不是我们第一次看到 Go 语言被用于前端目的。 GopherJS 已经存在了很长一段时间并且非常成熟,但是,不同之处在于它将 Go 代码编译为 JS 而不是 WebAssembly。

一个简单的例子


让我们从一个非常简单的示例开始,只要我们单击网页中的按钮,它就会在控制台中简单地输出 Hello World。我知道这听起来很令人兴奋,但我们可以很快将其构建成更实用、更酷的东西:

package main

func main() {
    println("Hello World")
}

现在,为了编译它,你必须设置 GOARCH=wasm 和 GOOS=js 并且你还必须使用 -o 标志指定文件的名称,如下所示:

$ GOARCH=wasm GOOS=js go build -o lib.wasm main.go


该命令应该将我们的代码编译成我们当前工作目录中的 lib.wasm 文件。我们将使用 WebAssembly.instantiateStreaming() 函数将其加载到我们的 index.html 中的页面中。注意 - 此代码是从官方 Go 语言仓库中窃取的:

<!DOCTYPE html>
<!--
Copyright 2018 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<html>
  <head>
    <meta charset="utf-8" />
    <title>Go wasm</title>
  </head>

  <body>
    <script src="wasm_exec.js"></script>

    <script>
      if (!WebAssembly.instantiateStreaming) {
        // polyfill
        WebAssembly.instantiateStreaming = async (resp, importObject) => {
          const source = await (await resp).arrayBuffer();
          return await WebAssembly.instantiate(source, importObject);
        };
      }

      const go = new Go();

      let mod, inst;

      WebAssembly.instantiateStreaming(fetch("lib.wasm"), go.importObject).then(
        result => {
          mod = result.module;
          inst = result.instance;
          document.getElementById("runButton").disabled = false;
        }
      );

      async function run() {
        await go.run(inst);
        inst = await WebAssembly.instantiate(mod, go.importObject); // reset instance
      }
    </script>

    <button onClick="run();" id="runButton" disabled>Run</button>
  </body>
</html>

我们还需要从 misc/wasm 复制 wasm_exec.js 文件。

$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .


而且,我们还有一个简单的基于 net/http 的文件服务器,同样是从这里偷来的,用于提供我们的 index.html 和其他各种 WebAssembly 文件:

package main

import (
    "flag"
    "log"
    "net/http"
)

var (
    listen = flag.String("listen", ":8080", "listen address")
    dir    = flag.String("dir", ".", "directory to serve")
)

func main() {
    flag.Parse()
    log.Printf("listening on %q...", *listen)
    log.Fatal(http.ListenAndServe(*listen, http.FileServer(http.Dir(*dir))))
}

当您启动此服务器后导航到 localhost:8080 时,您应该会看到 Run 按钮是可点击的,如果您在浏览器中打开控制台,您应该会看到每次单击时它都会打印出 Hello World按钮!

太棒了,我们成功地编译了一个非常简单的 Go -> WebAssembly 项目并让它在浏览器中运行。

一个更复杂的例子


现在好一点。比如说,我们想创建一个更复杂的示例,其中包含 DOM 操作、可以绑定到按钮单击的自定义 Go 函数等等。谢天谢地,这不是太难!

注册函数


我们将首先创建一些我们自己想要公开给前端的函数。我今天感觉很不新鲜,所以这些只是加减法。

这些函数接受一个 js.Value 类型的数组,并使用 js.Global().Set() 函数将输出设置为等于我们函数内完成的任何计算的结果。为了更好地衡量,我们还将结果打印到控制台:

func add(i []js.Value) {
    js.Global().Set("output", js.ValueOf(i[0].Int()+i[1].Int()))
    println(js.ValueOf(i[0].Int() + i[1].Int()).String())
}

func subtract(i []js.Value) {
    js.Global().Set("output", js.ValueOf(i[0].Int()-i[1].Int()))
    println(js.ValueOf(i[0].Int() - i[1].Int()).String())
}

func registerCallbacks() {
    js.Global().Set("add", js.NewCallback(add))
    js.Global().Set("subtract", js.NewCallback(subtract))
}

func main() {
    c := make(chan struct{}, 0)

    println("WASM Go Initialized")
    // register functions
    registerCallbacks()
    <-c
}

你会注意到我们通过调用 make 并创建了一个新通道来稍微修改了我们的 main 函数。这有效地将我们以前短暂的程序变成了一个长期运行的程序。我们还调用了另一个函数 registerCallbacks(),它的作用几乎就像一个路由器,而是创建新的回调,将我们新创建的函数有效地绑定到我们的前端。

为了让它工作,我们必须稍微修改 index.html 中的 JavaScript 代码,以便在获取程序实例后立即运行它:

const go = new Go();
let mod, inst;
WebAssembly.instantiateStreaming(fetch("lib.wasm"), go.importObject).then(
  async result => {
    mod = result.module;
    inst = result.instance;
    await go.run(inst);
  }
);


再次将其加载到浏览器中,您应该会看到,无需按下任何按钮,WASM Go Initialized 就会在控制台中打印出来。这意味着一切正常。

然后,我们可以开始从 <button> 元素之类的元素中调用这些函数,如下所示:

<button onClick="add(2,3);" id="addButton">Add</button>
<button onClick="subtract(10,3);" id="subtractButton">Subtract</button>


删除现有的运行按钮并将这两个新按钮添加到您的 index.html。当您在浏览器中重新加载页面并打开控制台时,您应该能够看到此函数的输出打印出来。

我们正在缓慢但肯定地开始在这方面取得进展!

评估 DOM 元素


所以,我猜下一阶段是开始评估 DOM 元素并使用它们的值而不是硬编码的值。

让我们修改 add() 函数,以便我可以传入 <input/> 元素的 2 个 id,然后像这样添加这些元素的值:

func add(i []js.Value) {
    value1 := js.Global().Get("document").Call("getElementById", i[0].String()).Get("value").String()
    value2 := js.Global().Get("document").Call("getElementById", i[1].String()).Get("value").String()
    js.Global().Set("output", value1+value2)
    println(value1 + value2)
}


然后我们可以更新我们的 index.html 以获得以下代码:

<input type="text" id="value1" />
<input type="text" id="value2" />

<button onClick="add('value1', 'value2');" id="addButton">Add</button>

如果您在我们的两个输入中输入了一些数值,然后单击添加按钮,您应该希望在控制台中看到两个值的串联打印输出。

我们忘记了什么?我们需要将这些字符串值解析为 int 值:

func add(i []js.Value) {
    value1 := js.Global().Get("document").Call("getElementById", i[0].String()).Get("value").String()
    value2 := js.Global().Get("document").Call("getElementById", i[1].String()).Get("value").String()

    int1, _ := strconv.Atoi(value1)
    int2, _ := strconv.Atoi(value2)

    js.Global().Set("output", int1+int2)
    println(int1 + int2)
}

你可能会注意到我在这里没有处理错误,因为我觉得很懒,这只是为了展示。

现在尝试重新编译此代码并重新加载浏览器,您应该注意到,如果我们在两个输入中输入值 22 和 3,它会在控制台中成功输出 25。

操作 DOM 元素


如果我们的计算器实际上没有在我们的页面中报告结果,我们的计算器就不会很好,所以现在让我们通过获取第三个 id 来解决这个问题,我们会将结果输出到:

func add(i []js.Value) {
    value1 := js.Global().Get("document").Call("getElementById", i[0].String()).Get("value").String()
    value2 := js.Global().Get("document").Call("getElementById", i[1].String()).Get("value").String()

    int1, _ := strconv.Atoi(value1)
    int2, _ := strconv.Atoi(value2)

    js.Global().Get("document").Call("getElementById", i[2].String()).Set("value", int1+int2)
}

更新我们的减法函数:


最后,让我们更新我们的减法:

func subtract(i []js.Value) {
    value1 := js.Global().Get("document").Call("getElementById", i[0].String()).Get("value").String()
    value2 := js.Global().Get("document").Call("getElementById", i[1].String()).Get("value").String()

    int1, _ := strconv.Atoi(value1)
    int2, _ := strconv.Atoi(value2)

    js.Global().Get("document").Call("getElementById", i[2].String()).Set("value", int1-int2)
}

我们完成的 index.html 应该如下所示:

<!DOCTYPE html>
<!--
Copyright 2018 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<html>
  <head>
    <meta charset="utf-8" />
    <title>Go wasm</title>
  </head>

  <body>
    <script src="wasm_exec.js"></script>

    <script>
      if (!WebAssembly.instantiateStreaming) {
        // polyfill
        WebAssembly.instantiateStreaming = async (resp, importObject) => {
          const source = await (await resp).arrayBuffer();
          return await WebAssembly.instantiate(source, importObject);
        };
      }

      const go = new Go();
      let mod, inst;
      WebAssembly.instantiateStreaming(fetch("lib.wasm"), go.importObject).then(
        async result => {
          mod = result.module;
          inst = result.instance;
          await go.run(inst);
        }
      );
    </script>

    <input type="text" id="value1" />
    <input type="text" id="value2" />

    <button onClick="add('value1', 'value2', 'result');" id="addButton">
      Add
    </button>
    <button
      onClick="subtract('value1', 'value2', 'result');"
      id="subtractButton"
    >
      Subtract
    </button>

    <input type="text" id="result" />
  </body>
</html>

结论


因此,在本教程中,我们设法学习了如何使用 Go 语言的新 v1.11 将 Go 程序编译成 WebAssembly。 我们创建了一个非常简单的计算器,它将 Go 代码中的函数暴露给我们的前端,并且还进行了一些 DOM 解析和操作以启动。

希望您发现这篇文章有用/有趣! 如果你这样做了,那么我很乐意在下面的评论部分收到你的来信。 

文章链接