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()
}
版权声明
本文章由作者“衡于墨”创作,转载请注明出处,未经允许禁止用于商业用途
评论区#
weiqi 评论道:
始终坚持做一件事情是极其困难的,我在整理书签的时候看到博主的主页,访问进来,很高兴了解到博主能一直坚持维护自己的博客。虽然没有读博主
2022年12月28日 12:40 #
衡与墨 评论道:
hh 谢谢wq
引用weiqi的发言
始终坚持做一件事情是极其困难的,我在整理书签的时候看到博主的主页,访问进来,很高兴了解到博主能一直坚持维护自己的博客。虽然没有读博主
2023年04月02日 17:39 #
引用发言