search.png
关于我
menu.png
goja 实用 go js 执行引擎 探路

Goja

https://github.com/dop251/goja

goja 是 k6 的 js 执行引擎实现库。拥有很高的性能,精简好用的语法。

支持的语法只有到 es5,不过 在 k6 中通过导入 babel 实现了 支持 es6 + 的语法。

goja 实现 es 5 的所有自带库,实现了 JSON,Date,Regex 等基础的。

像 console.log 需要自己来实现。性能上不如 v8。

https://github.com/dop251/goja/issues/2#issuecomment-426429140

If anyone's still wondering, I ran a quick test just out of curiosity about how "go" javascript engine implementations compare.

I ran the following code a couple times:

function factorial(n) {
    return n === 1 ? n : n * factorial(--n);
}

var i = 0;

while (i++ < 1e6) {
    factorial(10);
}

The execution times roughly were:
otto: 33.195s
goja: 3.937s
duktape: 1.545s
v8 (go binding): 0.309s
v8 native (d8): 0.187s

准确的说 goja 不是 js 的完美运行时,它只是提供了一个桥梁,js 提供动态脚本,go 实现底层函数,从动态到静态的桥梁。相比 v8 nodejs 的完整,它的这种实现方式精简而有效。

作为仅动态脚本,不应该用来执行耗时的算法操作。耗时的算法操作应该由 go 来实现,然后 js 仅复制调用。

和 jenkins 类似,jenkins 执行的是 groovy 脚本,goja 执行 js 脚本。

和 nGrinder 类似, nGrinder 可以利用 jvm 执行 jython 和 groovy,用来做压测。k6 也是类似。

执行 js 字符串

func Test1(t *testing.T) {
    vm := goja.New()
    // 执行js字符串
    v, err := vm.RunString(`"helloworld"`)
    if err != nil {
        panic(err)
    }
    // export 获取执行结果
    fmt.Println(v.Export())
}

vm 不是 线程安全的,需要注意。

执行 js 函数

func Test2(t *testing.T) {
    vm := goja.New()
    // 执行 js 函数
    jsfun := `
    function hello(name) {
        return "hello! " + name
    }
    `
    _, _ = vm.RunString(jsfun)
    // 校验函数并返回
    hellofun, ok := goja.AssertFunction(vm.Get("hello"))
    if !ok {
        panic("not function")
    }
    // 函数的第一个参数是 this,其它参数要用 vm.ToValue 做转换
    result, err := hellofun(goja.Undefined(), vm.ToValue("hengyumo"))
    if err != nil {
        panic(err)
    }
    fmt.Println(result)
}

除了 AssertFunction 还有一种方式:

func Test3(t *testing.T) {
    vm := goja.New()
    // 执行 js 函数
    jsfun := `
    function hello(name) {
        return "hello! " + name
    }
    `
    _, _ = vm.RunString(jsfun)

    var fn func(string) string
    err := vm.ExportTo(vm.Get("hello"), &fn)
    if err != nil {
        panic(err)
    }

    fmt.Println(fn("hengyumo")) // note, _this_ value in the function will be undefined.
    // Output: hello! hengyumo
}

go 结构体转义

func Test4(t *testing.T) {
    vm := goja.New()
    vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
    type S struct {
        Field int `json:"field"` // 控制 go 结构体在 js 调用时的名称
    }
    vm.Set("s", S{Field: 42})
    res, _ := vm.RunString(`s.field`) // without the mapper it would have been s.Field
    fmt.Println(res.Export())
    // Output: 42
}

js 调用 go 的函数


type Console struct{}


func (*Console) Log(msg ...interface{}) {
    fmt.Println(msg...)
}


func Test5(t *testing.T) {
    vm := goja.New()

    vm.Set("console", &Console{})
    _, err := vm.RunString("console.Log('hello world')")
    if err != nil {
        panic(err)
    }
}

这里相当于设置了一个结构体为 console,然后 在 js 中去调用了。

还可以 NewObject 来构造一个 js 的对象:


type Console struct{}

func (*Console) Log(msg ...interface{}) {
    fmt.Println(msg...)
}

func newConsole(vm *goja.Runtime) *goja.Object {
    c := &Console{}
    obj := vm.NewObject()
    obj.Set("log", c.Log)
    return obj
}

func Test6(t *testing.T) {
    vm := goja.New()
    vm.Set("console", newConsole(vm))
    _, err := vm.RunString(`
        console.log('hello world')
    `)
    if err != nil {
        panic(err)
    }
}

实现一个模块

我们通过自定义一个 require 函数来实现模块:

这个例子会稍微更复杂,首先实现一个 http 模块

// 实现一个 http 模块

type httpModule struct{}

func NewHttpModule(vm *goja.Runtime) *goja.Object {
    m := &httpModule{}
    obj := vm.NewObject()
    obj.Set("get", func(url string) *goja.Object {
        ok, res := m.Get(url)
        resObj := vm.NewObject()
        resObj.Set("ok", ok)
        resObj.Set("data", res)
        return resObj
    })
    return obj
}

// 实现一个 http 模块的 Get 方法
func (h *httpModule) Get(url string) (ok bool, resStr string) {
    res, err := http.Get(url)
    if err != nil {
        resStr = err.Error()
        return
    }
    defer res.Body.Close()
    all, err := ioutil.ReadAll(res.Body)
    if err != nil {
        resStr = err.Error()
        return
    }
    return true, string(all)
}

为了简单,只实现了 Get 方法。

然后是 module 模块实现:


// 实现模块

type modules struct {
    m map[string]*goja.Object
}

func Modules(vm *goja.Runtime) *modules {
    m := &modules{m: make(map[string]*goja.Object)}
    vm.Set("require", m.Require())
    return m
}

// RegisterModule 注册模块
func (m *modules) RegisterModule(name string, obj *goja.Object) {
    m.m[name] = obj
}

// 实现 Require
func (m *modules) Require() goja.Callable {
    return func(this goja.Value, args ...goja.Value) (goja.Value, error) {
        name := this.String() // 当作为函数被调用时, this 不是预想的 undefined 而会是第一个参数
        module := m.m[name]
        return module, nil
    }
}


这个模块使用 map 来保持 object,当调用 require 时才返回对应的 object。

让我们测试一下效果:

这个实例尝试在 js 脚本中调用 http 模块的 get 方法,get 这个方法返回了调用是否成功和具体的返回值。

func Test7(t *testing.T) {
    vm := goja.New()
    m := Modules(vm)
    m.RegisterModule("http", NewHttpModule(vm))
    vm.Set("console", newConsole(vm))

    _, err := vm.RunString(`
        var url = "https://github.com/dop251/goja"
        var http = require('http')
        var res = http.get(url)
        console.log(res.ok, res.data)
    `)
    if err != nil {
        panic(err)
    }
}

异常

Any exception thrown in JavaScript is returned as an error of type *Exception. It is possible to extract the value thrown by using the Value() method:


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

throw("Test");

`)

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

js 的异常会包装成 go Run 返回的异常,通过类型 goja.Exception 可以取出其中的内容。

If a native Go function panics with a Value, it is thrown as a Javascript exception (and therefore can be caught):


var vm *Runtime

func Test() {
    panic(vm.ToValue("Error"))
}

vm = New()
vm.Set("Test", Test)
_, err := vm.RunString(`

try {
    Test();
} catch(e) {
    if (e !== "Error") {
        throw e;
    }
}

`)

if err != nil {
    panic(err)
}

go 中 panic 的异常可以在 js 中捕获,而不会让 go 直接崩溃

中断 js

这是个很有效的功能,尤其是你需要控制 js 执行的时间不至于过长的时候。

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

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

    _, err := vm.RunString(SCRIPT)
    if err == nil {
        t.Fatal("Err is nil")
    }
    // err is of type *InterruptError and its Value() method 
    // returns whatever has been passed to vm.Interrupt()
}

版权声明

知识共享许可协议 本文章由作者“衡于墨”创作,转载请注明出处,未经允许禁止用于商业用途

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
发布时间:2022年10月03日 09:44:37

评论区#

weiqi 评论道:

始终坚持做一件事情是极其困难的,我在整理书签的时候看到博主的主页,访问进来,很高兴了解到博主能一直坚持维护自己的博客。虽然没有读博主

2022年12月28日 12:40 #

衡与墨 评论道:

hh 谢谢wq

引用weiqi的发言

始终坚持做一件事情是极其困难的,我在整理书签的时候看到博主的主页,访问进来,很高兴了解到博主能一直坚持维护自己的博客。虽然没有读博主

2023年04月02日 17:39 #

关闭特效