golang实现ECMAScript 5.1+解释器的JavaScript引擎插件库goja的使用

Golang实现ECMAScript 5.1+解释器的JavaScript引擎插件库goja的使用

简介

goja是一个用纯Go语言实现的ECMAScript 5.1(+)解释器,重点在于标准合规性和性能。这个项目很大程度上受到了otto的启发。

最低要求的Go版本是1.20。

特性

  • 完整的ECMAScript 5.1支持(包括正则表达式和严格模式)
  • 通过了几乎所有tc39测试(针对目前已实现的功能)
  • 能够运行Babel、Typescript编译器以及几乎所有用ES5编写的代码
  • 支持Sourcemaps
  • 大部分ES6功能(仍在开发中)

基本示例

运行JavaScript并获取结果值:

vm := goja.New()
v, err := vm.RunString("2 + 2")
if err != nil {
    panic(err)
}
if num := v.Export().(int64); num != 4 {
    panic(num)
}

传递值到JavaScript

任何Go值都可以使用Runtime.ToValue()方法传递给JS:

vm := goja.New()
vm.Set("myVar", 42)
res, _ := vm.RunString("myVar + 8")
fmt.Println(res.Export()) // 输出: 50

从JS导出值

JS值可以使用Value.Export()方法导出为其默认的Go表示形式,或者使用Runtime.ExportTo()方法导出到特定的Go变量。

从Go调用JS函数

有两种方法:

  1. 使用AssertFunction():
const SCRIPT = `
function sum(a, b) {
    return +a + b;
}
`

vm := goja.New()
_, err := vm.RunString(SCRIPT)
if err != nil {
    panic(err)
}
sum, ok := goja.AssertFunction(vm.Get("sum"))
if !ok {
    panic("Not a function")
}

res, err := sum(goja.Undefined(), vm.ToValue(40), vm.ToValue(2))
if err != nil {
    panic(err)
}
fmt.Println(res)
// 输出: 42
  1. 使用Runtime.ExportTo():
const SCRIPT = `
function sum(a, b) {
    return +a + b;
}
`

vm := goja.New()
_, err := vm.RunString(SCRIPT)
if err != nil {
    panic(err)
}

var sum func(int, int) int
err = vm.ExportTo(vm.Get("sum"), &sum)
if err != nil {
    panic(err)
}

fmt.Println(sum(40, 2)) // 注意:函数中的_this_值将是undefined
// 输出: 42

映射结构体字段和方法名

默认情况下,名称按原样传递(即大写)。要匹配标准的JavaScript命名约定,可以使用FieldNameMapper:

vm := goja.New()
vm.SetFieldNameMapper(TagFieldNameMapper("json", true))
type S struct {
    Field int `json:"field"`
}
vm.Set("s", S{Field: 42})
res, _ := vm.RunString(`s.field`) // 没有映射器的话会是s.Field
fmt.Println(res.Export())
// 输出: 42

异常处理

任何在JavaScript中抛出的异常都会作为*Exception类型的错误返回:

vm := goja.New()
_, err := vm.RunString(`

throw("Test");

`)

if jserr, ok := err.(*Exception); ok {
    if jserr.Value().Export() != "Test" {
        panic("wrong value")
    }
} else {
    panic("wrong type")
}

中断执行

func TestInterrupt(t *testing.T) {
    const SCRIPT = `
    var i = 0;
    for (;;) {
        i++;
    }
    `

    vm := goja.New()
    time.AfterFunc(200 * time.Millisecond, func() {
        vm.Interrupt("halt")
    })

    _, err := vm.RunString(SCRIPT)
    if err == nil {
        t.Fatal("Err is nil")
    }
    // err是*InterruptError类型,其Value()方法返回传递给vm.Interrupt()的任何内容
}

完整示例

下面是一个完整的示例,展示了如何初始化goja引擎、执行脚本并与Go代码交互:

package main

import (
	"fmt"
	"github.com/dop251/goja"
)

func main() {
	// 创建新的VM实例
	vm := goja.New()

	// 定义一个JavaScript函数
	script := `
	function greet(name) {
		return "Hello, " + name + "!";
	}

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

	// 执行脚本
	_, err := vm.RunString(script)
	if err != nil {
		panic(err)
	}

	// 调用JavaScript中的greet函数
	greet, ok := goja.AssertFunction(vm.Get("greet"))
	if !ok {
		panic("greet is not a function")
	}

	res, err := greet(goja.Undefined(), vm.ToValue("World"))
	if err != nil {
		panic(err)
	}
	fmt.Println(res) // 输出: Hello, World!

	// 使用ExportTo调用add函数
	var addFunc func(int, int) int
	err = vm.ExportTo(vm.Get("add"), &addFunc)
	if err != nil {
		panic(err)
	}

	sum := addFunc(20, 22)
	fmt.Println(sum) // 输出: 42

	// 从Go传递对象到JavaScript
	type Person struct {
		Name string `json:"name"`
		Age  int    `json:"age"`
	}

	vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
	vm.Set("person", Person{Name: "Alice", Age: 30})
	
	val, err := vm.RunString(`
		"Name: " + person.name + ", Age: " + person.age
	`)
	if err != nil {
		panic(err)
	}
	fmt.Println(val) // 输出: Name: Alice, Age: 30
}

这个示例展示了goja的基本用法,包括执行脚本、调用JS函数、在Go和JS之间传递数据等核心功能。


更多关于golang实现ECMAScript 5.1+解释器的JavaScript引擎插件库goja的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang实现ECMAScript 5.1+解释器的JavaScript引擎插件库goja的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Goja - Golang实现的JavaScript引擎

Goja是一个用纯Go语言实现的ECMAScript 5.1+解释器。它允许你在Go应用程序中嵌入JavaScript执行环境,非常适合需要脚本扩展功能的Go程序。

基本使用

安装

go get github.com/dop251/goja

简单示例

package main

import (
	"fmt"
	"github.com/dop251/goja"
)

func main() {
	// 创建新的运行时环境
	vm := goja.New()

	// 执行简单JS代码
	v, err := vm.RunString("2 + 2")
	if err != nil {
		panic(err)
	}
	fmt.Println(v) // 输出: 4

	// 执行更复杂的JS代码
	_, err = vm.RunString(`
		function factorial(n) {
			if (n <= 1) return 1;
			return n * factorial(n - 1);
		}
	`)
	if err != nil {
		panic(err)
	}

	// 调用JS函数
	fact, ok := goja.AssertFunction(vm.Get("factorial"))
	if !ok {
		panic("Not a function")
	}

	res, err := fact(goja.Undefined(), vm.ToValue(5))
	if err != nil {
		panic(err)
	}
	fmt.Println(res) // 输出: 120
}

高级功能

Go与JS交互

package main

import (
	"fmt"
	"github.com/dop251/goja"
)

func main() {
	vm := goja.New()

	// 向JS环境注入Go函数
	err := vm.Set("greet", func(call goja.FunctionCall) goja.Value {
		name := call.Argument(0).String()
		return vm.ToValue("Hello, " + name + "!")
	})
	if err != nil {
		panic(err)
	}

	// 调用Go注入的函数
	v, err := vm.RunString(`greet("World")`)
	if err != nil {
		panic(err)
	}
	fmt.Println(v) // 输出: Hello, World!
}

从Go访问JS对象

package main

import (
	"fmt"
	"github.com/dop251/goja"
)

func main() {
	vm := goja.New()

	// 执行JS代码创建对象
	_, err := vm.RunString(`
		var person = {
			name: "Alice",
			age: 30,
			sayHello: function() {
				return "Hi, I'm " + this.name;
			}
		}
	`)
	if err != nil {
		panic(err)
	}

	// 获取JS对象
	person := vm.Get("person")
	obj := person.ToObject(vm)

	// 访问属性
	name := obj.Get("name").String()
	age := obj.Get("age").ToInteger()
	fmt.Printf("%s is %d years old\n", name, age) // Alice is 30 years old

	// 调用方法
	sayHello, ok := goja.AssertFunction(obj.Get("sayHello"))
	if ok {
		res, err := sayHello(person, nil)
		if err != nil {
			panic(err)
		}
		fmt.Println(res.String()) // Hi, I'm Alice
	}
}

错误处理

package main

import (
	"fmt"
	"github.com/dop251/goja"
)

func main() {
	vm := goja.New()

	// 执行可能出错的代码
	_, err := vm.RunString(`
		function divide(a, b) {
			if (b === 0) {
				throw new Error("Division by zero");
			}
			return a / b;
		}
	`)
	if err != nil {
		panic(err)
	}

	divide, _ := goja.AssertFunction(vm.Get("divide"))

	// 捕获JS抛出的异常
	_, err = divide(goja.Undefined(), vm.ToValue(10), vm.ToValue(0))
	if jsErr, ok := err.(*goja.Exception); ok {
		fmt.Println("JS Error:", jsErr.String())
	} else if err != nil {
		panic(err)
	}
}

性能考虑

  1. Goja不是性能最高的JavaScript引擎,但对于大多数嵌入场景已经足够
  2. 避免频繁在Go和JS之间传递数据
  3. 对于性能敏感的部分,尽量用Go实现

限制

  1. 不支持完整的ES6+特性(但支持部分)
  2. 没有JIT编译,纯解释执行
  3. 某些JavaScript特性可能实现不完全

Goja是一个轻量级的解决方案,特别适合需要在Go应用中添加脚本功能的场景,如配置脚本、规则引擎、插件系统等。

回到顶部