跳到主要内容

JS 引擎

js 包内嵌了由 goja 驱动的沙箱化 JavaScript 运行时。它围绕 Engine / Runtime / Lib 三元 架构构建:不可变的 Engine 持有一组经过校验的库并批量产出一次性的 Runtime;具备副作用的能力(HTTP、SQL、缓存、事件……)都是按运行时安装的 Lib,脚本只能触及被安装的内容。

框架用该引擎运行集成适配器脚本、签名与验证脚本等脚本执行接缝;应用也可以 直接使用它满足自己的脚本化需求。

快速开始

import "github.com/coldsmirk/vef-framework-go/js"

engine, err := js.NewEngine()
if err != nil {
return err
}

// 一个引擎服务整个应用;每次执行创建一个运行时。
rt, err := engine.NewRuntime(js.WithRunTimeout(5 * time.Second))
if err != nil {
return err
}

value, err := rt.RunString(ctx, `1 + 2`)
fmt.Println(value.Export()) // 3

Engine

js.NewEngine(opts ...EngineOption) 构建引擎并急切校验库集合:nil 库、 空名称、名称冲突(跨标准库、常驻库与目录库)都会使构建失败。

选项行为
js.WithBaseLibs(libs...)注册常驻库:安装进每个运行时,无需 opt-in。仅用于安全、普适的工具
js.WithLibs(libs...)注册目录(catalog)库:每个运行时经 js.EnableLibs 按需激活
js.WithoutStdLibs()构建裸引擎,其运行时不带标准库 bundle

错误:js.ErrInvalidLib(nil 库 / 空名)、js.ErrDuplicateLib(名称 冲突)、js.ErrLibNotFound(激活未注册的名称)。

引擎可并发使用。框架提供共享的 DI 引擎,内置能力库按两档预先接线:

  • 常驻(安装进每个运行时):consolecryptocache(内存存储, 键前缀 js:);
  • 目录 opt-in(运行时经 EnableLibs 按需激活):eventshttp (默认 30s 超时)、sql(主数据库,只读)。

应用用 vef.ProvideJSLib 贡献或替换库:与内置同名的库在其所在档位内 替换,新名称加入 opt-in 目录。

vef.ProvideJSLib(func(db orm.DB) js.Lib {
return jssql.New(db, config.Postgres, jssql.WithExecute())
})

Runtime

Engine.NewRuntime(opts ...RuntimeOption) 创建携带引擎基线加已激活目录库 的全新运行时。运行时可并发使用,同一时间只允许一个 Run 调用在途, 用完即弃——每个 goroutine、每次执行创建一个。

选项行为
js.EnableLibs(names...)按名称激活目录库;安装顺序即参数顺序;重复激活为 no-op;未知名称使 NewRuntime 返回 ErrLibNotFound
js.WithRunTimeout(d)为每次 Run 调用设置上限;与调用方 context 组合——更早的期限生效
js.WithMaxCallStackSize(n)约束 JS 调用栈深度,防失控递归
方法契约
RunProgram(ctx, program) / RunString(ctx, source)ctx 下执行:取消会中断运行中的脚本,并经 Context() 传导到宿主库的在途 IO;此时返回的错误即 context 的错误
Set(name, value)绑定全局变量
Context()在途 Run 调用的 context(空闲时为 context.Background());宿主库必须经它发起 IO
AsFunction(value)把值转换为可调用的 js.Func 句柄;调用必须发生在当前驱动运行时的 goroutine 上
VM()底层 *goja.Runtime,用于高级库开发

每个运行时都配置了 JSON 字段名映射 (goja.TagFieldNameMapper("json", true)),经 Set 传入的 Go 结构体在 脚本中按 json 标签读取。

标准库 Bundle

除非引擎以 WithoutStdLibs 构建,每个运行时都自带一个 vendored esbuild bundle,按各库生态原生的全局名安装:

全局名用途
BigNumberbignumber.js任意精度十进制
dayjsDay.js日期时间解析、格式化、运算
fxpfast-xml-parserXML 解析与构建(fxp.XMLParserfxp.XMLBuilder
radashiRadashi函数式工具集
zZodSchema 校验,内置 enzh-CN 语言包(默认 zh-CN
URL / URLSearchParamscore-js polyfillWHATWG URL 处理

内置能力库

具备副作用的能力是 js/* 下的独立包,接入 DI 引擎的目录。每个库按包名 安装一个全局对象;运行时只能看到为它激活的库。所有宿主 IO 都经 Runtime.Context(),取消因此能到达阻塞中的 Go 调用。

jssql —— 全局 sql

sql.queryList('SELECT name FROM users WHERE age > ?', 18) // → [{...}, ...];无匹配行返回 []
sql.queryOne('SELECT ... WHERE id = ?', id) // → {...} | null
sql.execute('UPDATE ...', args) // → { rowsAffected }
  • jssql.New(db, kind, opts...) 在选定的数据源上构建库;由调用方决定脚本 能触达什么。选项类型为 jssql.Option(例如 jssql.WithExecute()jssql.WithMaxRows(n))。
  • 只提供占位符绑定——刻意不提供字符串拼接辅助。
  • 默认只读,由 AST 守卫(sqlguard)fail-closed 强制:写 CTE、堆叠语句、 方言特有的副作用函数被拒绝;无法解析的 SQL 一律拒绝 (ErrQueryNotReadOnly)。
  • 未以 jssql.WithExecute() 构建时,sql.execute 抛出 ErrExecuteDisabled
  • 结果集有上限(jssql.WithMaxRows,默认 jssql.DefaultMaxRows = 1000; ErrTooManyRows 提示脚本加 LIMIT)。

jshttp —— 全局 http

fetch 标准的同步化版本:

http.fetch(url, { method, headers, query, body, redirect, timeout })
http.get(url, options?) // fetch 语法糖,put/patch 同理
http.post(url, body, options?)
http.delete(url, options?)
  • 每次调用返回 { status, statusText, ok, url, redirected, headers, body, text(), json(), arrayBuffer() };失败以可捕获异常抛出。响应头名小写化, 多值以 ", " 连接。
  • 超出 fetch 的部分:query 追加 URL 参数、timeout(毫秒)取代 AbortSignal、redirect 支持 follow / error / manual
  • 默认无任何限制——超时、响应体上限、主机白名单、私网守卫都通过选项 (jshttp.New(opts...))按需开启,选项类型为 jshttp.Optionjshttp.WithClient(client) 提供自定义 http.Clientjshttp.WithTimeout(d) 限制每次请求的超时; jshttp.WithMaxBodySize(n) 限制响应体字节数; jshttp.WithAllowedHosts(hosts...) 将目标限制在给定主机名白名单内; jshttp.WithPublicNetworkOnly() 阻止回环、私有、链路本地、组播以及未指定地址。
  • 错误:jshttp.ErrInvalidRequest(调用格式错误)、jshttp.ErrSchemeNotAllowed(非 HTTP/HTTPS 协议)、jshttp.ErrHostNotAllowed(主机不在白名单内)、jshttp.ErrAddressNotAllowed(解析到禁止地址)、jshttp.ErrBodyTooLarge(响应体超过上限)、jshttp.ErrRedirectBlockedredirect"error")、jshttp.ErrTooManyRedirects(重定向链过长)。

jscache —— 全局 cache

脚本跨执行保存状态的唯一通道(运行时每次运行后即被丢弃):

cache.set('counter', { n: 1 }) // 默认 TTL
cache.set('token', value, 60000) // TTL 毫秒
cache.get('counter') // → 值 | null
cache.has('counter') // → 布尔
cache.delete('counter')

jscache.New(store, opts...) 接受任意 cache.Cache[any] —— 单节点状态用 内存、共享状态用 Redis —— 外加 WithKeyPrefix 做键命名空间。选项类型为 jscache.Option(例如 jscache.WithKeyPrefix(prefix))。

jsevents —— 全局 events

events.publish('report.generated', { reportId: id })

负载 JSON 编码后以 event.RawPayload 发布,Go 订阅方照常用 event.SubscribeTyped 解码。publish 刻意是唯一动词——订阅是长生命周期的, 属于宿主而不属于按执行创建的运行时。jsevents.New(bus, opts...) 的选项 类型为 jsevents.Optionjsevents.WithAllowedTypes(patterns...) 可约束可发布的类型命名空间。

  • 错误:jsevents.ErrEmptyEventType(事件类型为空)、jsevents.ErrEventTypeNotAllowed(事件类型不在白名单内)。

jscrypto —— 全局 crypto

crypto.md5(data) // 同理 sha1 / sha256 / sha512 / sm3
crypto.hmac('sha256', key, data) // hex 摘要
crypto.base64Encode(data) / crypto.base64Decode(encoded)
crypto.hexEncode(data) / crypto.hexDecode(encoded)
crypto.uuid()

摘要为小写 hex;输入为 UTF-8 字符串。弱摘要(md5、sha1)只为兼容遗留 API 签名——密码存储属于安全模块的编码器。

  • 错误:jscrypto.ErrUnsupportedAlgorithmcrypto.hmac 使用了不支持的算法)。

jsconsole —— 全局 console

console.info('processed', count, payload) // 亦有 warn / error

参数以空格连接;字符串原样、错误取其消息、其余 JSON 编码。由 logx.Logger 支撑(传入命名 logger 可区分脚本输出)。

编写自己的 Lib

type Lib interface {
Name() string // Engine 内唯一键;按惯例即其安装的全局名
Install(rt *js.Runtime) error
}
  • 纯 JS 库:js.SourceLib(name, source)(急切编译)或 js.ProgramLib(name, program)
  • 宿主能力:以 rt.Set(...) 实现 Install,只持有共享且 goroutine 安全的 依赖——绝不持有按运行时状态——并经 rt.Context() 发起 IO,让取消得以 传导。
lib, err := js.SourceLib("fmtx", `var fmtx = { pad: (s, n) => String(s).padStart(n, '0') };`)
engine, err := js.NewEngine(js.WithLibs(lib))
rt, err := engine.NewRuntime(js.EnableLibs("fmtx"))

编译辅助

js 包暴露了 goja pass-through surface:类型别名(RuntimeValueObjectProgramAstProgram)和函数别名(CompileMustCompileIs*)镜像了上游 github.com/dop251/goja API。每个精确符号和签名都列在 public API index 中。

API契约
js.Compile(name, source, strict) / js.MustCompile(...)为重复执行预编译(goja 别名)
js.Parse(name, source)返回 *js.AstProgram(禁用 source map)
js.Runtimejs.Valuejs.Objectjs.Programjs.AstProgramgoja 类型别名
js.IsNaNjs.IsStringjs.IsBigIntjs.IsNumberjs.IsInfinityjs.IsUndefinedjs.IsNullgoja 辅助函数别名

框架在哪些地方运行脚本

接缝可见库
集成适配器脚本基线 + errorscodes、按系统的作用域 http / sql
集成签名/验证脚本仅基线(零 IO)+ requestparams 绑定
表达式引擎自有求值器(非本引擎)

线程安全

警告Engine 可并发使用;Runtime 不可。每个 goroutine、每次执行 创建一个运行时,用完即弃。