如何在Golang中从JavaScript调用函数

如何在Golang中从JavaScript调用函数 我正在尝试理解 Go 语言中的 WebAssembly,因此编写了以下代码,旨在实现:

  1. 操作 DOM
  2. 调用 JavaScript 函数
  3. 定义一个可供 JavaScript 调用的函数

前两步运行正常,但最后一步未能按预期工作,我遇到了 JavaScript 错误 function undefined。我的代码如下,问题出在函数 sub 上。

package main

import (
	"syscall/js"
)

// func sub(a, b float64) float64

func sub(this js.Value, inputs []js.Value) interface{} {
	return inputs[0].Float() - inputs[1].Float()
}

func main() {
	c := make(chan int) // 通道用于保持 wasm 运行,它不像 rust/c/c++ 中那样是一个库,所以我们需要保持二进制程序运行
	js.Global().Set("sub", js.FuncOf(sub))
	alert := js.Global().Get("alert")
	alert.Invoke("Hi")
	println("Hello wasm")

	num := js.Global().Call("add", 3, 4)
	println(num.Int())

	document := js.Global().Get("document")
	h1 := document.Call("createElement", "h1")
	h1.Set("innerText", "This is H1")
	document.Get("body").Call("appendChild", h1)

	<-c // 暂停执行,以便我们为 JS 创建的资源保持可用
}

将其编译为 wasm 的命令如下:

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

wasm_exec.js 文件复制到相同的工作目录:

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

我的 HTML 文件是:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>WASM</title>
    <script src="http://localhost:8080/www/lib.js"></script>
    <!-- WASM -->
    <script src="http://localhost:8080/www/wasm_exec.js"></script>
    <script src="http://localhost:8080/www/loadWasm.js"></script>
</head>
<body>
</body>
<script>
   console.log(sub(5,3));
</script>
</html>

lib.js 的内容是:

function add(a, b){
    return a + b;
}

loadWasm.js 的内容是:

async function init(){
    const go = new Go();
    const result = await WebAssembly.instantiateStreaming(
        fetch("http://localhost:8080/www/main.wasm"),
        go.importObject
    );
    go.run(result.instance);
}
init();

服务器端代码如下:

package main

import (
	"fmt"
	"html/template"
	"net/http"
)

func wasmHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		tmpl := template.Must(template.ParseFiles("www/home.html"))

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		w.Header().Set("Access-Control-Allow-Origin", "*")
		err := tmpl.Execute(w, nil)
		if err != nil {
			fmt.Println(err)
		}
	})
}

func main() {
	fs := http.StripPrefix("/www/", http.FileServer(http.Dir("./www")))
	http.Handle("/www/", fs)

	http.Handle("/home", wasmHandler())
	http.ListenAndServe(":8080", nil)

}

我得到的输出是:

enter image description here

更新

我尝试使用下面的 TinyGO 示例,但遇到了几乎相同的问题:

//wasm.go

package main

// This calls a JS function from Go.
func main() {
	println("adding two numbers:", add(2, 3)) // expecting 5
}

// module from JavaScript.
func add(x, y int) int

//export multiply
func multiply(x, y int) int {
	return x * y
}

编译命令如下:

tinygo build -o main2.wasm -target wasm -no-debug
cp "$(tinygo env TINYGOROOT)/targets/wasm_exec.js" .

server.go 代码如下:

package main

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

const dir = "./www"

func main() {
	fs := http.FileServer(http.Dir(dir))
	log.Print("Serving " + dir + " on http://localhost:8080")
	http.ListenAndServe(":8080", http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
		resp.Header().Add("Cache-Control", "no-cache")
		if strings.HasSuffix(req.URL.Path, ".wasm") {
			resp.Header().Set("content-type", "application/wasm")
		}
		fs.ServeHTTP(resp, req)
	}))
}

JavaScript 代码如下:

const go = new Go(); // Defined in wasm_exec.js

go.importObject.env = {
    'main.add': function(x, y) {
        return x + y
    }
    // ... other functions
}


const WASM_URL = 'main.wasm';

var wasm;

if ('instantiateStreaming' in WebAssembly) {
	WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject).then(function (obj) {
		wasm = obj.instance;
		go.run(wasm);
	})
} else {
	fetch(WASM_URL).then(resp =>
		resp.arrayBuffer()
	).then(bytes =>
		WebAssembly.instantiate(bytes, go.importObject).then(function (obj) {
			wasm = obj.instance;
			go.run(wasm);
		})
	)
}

// Calling the multiply function:
console.log('multiplied two numbers:', exports.multiply(5, 3));

我得到的输出是: enter image description here


更多关于如何在Golang中从JavaScript调用函数的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

我找到了解决方案,我需要某种方法来检测并确认 wasm 已经加载完毕并准备好进行处理,类似于在 JS 中检查文档是否就绪的方法:

if (document.readyState === 'complete') {
  // 页面已完全加载
}

// 或者

document.onreadystatechange = () => {
  if (document.readyState === 'complete') {
    // 文档已就绪
  }
};

因此,由于我代码中的 wasm 初始化函数是 async 的,我在 JS 中使用了以下方法:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>WASM</title>
    <!-- WASM -->
    <script src="http://localhost:8080/www/wasm_exec.js"></script>
    <script src="http://localhost:8080/www/loadWasm.js"></script>
</head>
<body>
</body>
<script>
    (async () => {
        try {
            await init();
            alert("Wasm had been loaded")
            console.log(multiply(5, 3));
        } catch (e) {
            console.log(e);
        } 
    })(); 
/***** OR ****/
    (async () => {
        await init();
        alert("Wasm had been loaded")
        console.log(multiply(5, 3));
    })().catch(e => {
        console.log(e);
    });
/*************/
</script>
</html>

这帮助我确保文档已准备好处理并调用 wasm 函数。

wasm 加载函数就简单地变成了:

async function init(){
    const go = new Go();
    const result = await WebAssembly.instantiateStreaming(
        fetch("http://localhost:8080/www/main.wasm"),
        go.importObject
    );
    go.run(result.instance); 
}

更多关于如何在Golang中从JavaScript调用函数的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


问题在于JavaScript执行时机。在HTML的<script>标签中直接调用sub(5,3)时,WebAssembly模块尚未初始化完成。以下是修正方案:

修改HTML文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>WASM</title>
    <script src="http://localhost:8080/www/lib.js"></script>
    <script src="http://localhost:8080/www/wasm_exec.js"></script>
    <script src="http://localhost:8080/www/loadWasm.js"></script>
</head>
<body>
</body>
<script>
    // 等待WASM初始化完成后再调用
    async function callWasmFunction() {
        await init(); // 确保init()返回Promise
        console.log(sub(5, 3));
    }
    callWasmFunction();
</script>
</html>

修改loadWasm.js:

async function init() {
    const go = new Go();
    const result = await WebAssembly.instantiateStreaming(
        fetch("http://localhost:8080/www/main.wasm"),
        go.importObject
    );
    go.run(result.instance);
    return result.instance; // 返回实例以便确认加载完成
}

或者更简单的方案,在WASM完全加载后调用:

<script>
    // 使用事件监听确保WASM就绪
    document.addEventListener('wasm-ready', function() {
        console.log(sub(5, 3));
    });
</script>

在loadWasm.js中添加事件触发:

async function init() {
    const go = new Go();
    const result = await WebAssembly.instantiateStreaming(
        fetch("http://localhost:8080/www/main.wasm"),
        go.importObject
    );
    go.run(result.instance);
    // 触发自定义事件通知WASM已就绪
    document.dispatchEvent(new Event('wasm-ready'));
}

对于TinyGO示例的问题,需要正确导出函数:

// 修改JavaScript部分
WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject).then(function (obj) {
    wasm = obj.instance;
    go.run(wasm);
    
    // 从wasm实例的exports中获取函数
    const multiply = wasm.exports.multiply;
    console.log('multiplied two numbers:', multiply(5, 3));
})

Go代码保持原样即可:

package main

import (
	"syscall/js"
)

func sub(this js.Value, inputs []js.Value) interface{} {
	return inputs[0].Float() - inputs[1].Float()
}

func main() {
	c := make(chan int)
	js.Global().Set("sub", js.FuncOf(sub))
	
	// 测试其他功能
	alert := js.Global().Get("alert")
	alert.Invoke("WASM Loaded Successfully")
	
	<-c
}

关键点是确保JavaScript在WebAssembly模块完全初始化完成后再调用导出的函数。由于WASM加载是异步过程,直接同步调用会导致函数未定义的错误。

回到顶部