feat: 嵌入式 JS 脚本引擎核心

用 goja 承载业务回调,让业务逻辑变更不必重新编译发布 Go 程序。脚本用
ESM + TypeScript 写,Go 侧按名字把它们当普通对象实例化并调用方法。

主要组成:

  - Engine    编译脚本、管配置,公开 API 不暴露任何 goja 类型
  - Script    一份编译好的脚本 + 它的 VM 池,热更新时整体顶替
  - Instance  独占一个 VM 的实例,状态留在 JS 侧
  - Caller    自定义调用约定,把脚本函数适配成 Go 侧要的签名
  - Scope     让同一个 ctx 下的多个脚本共享 Go 侧对象
  - Extension 扩展接口:给脚本添全局对象,配套 TS 类型
  - Overlay   多层 Loader 叠加,后面的盖前面的

几个关键取舍:

  - 源码一律先过 esbuild 打包成 ESM,再改写成立即执行函数。goja 不认
    import/export,而业务脚本要能拆文件、用 TypeScript。
  - VM 池化复用,但每个 VM 单线程。goja 的 Runtime 不是 goroutine 安全的。
  - Go 侧函数返回的 error 在脚本里表现为抛异常,不占返回值位置。
  - 脚本能看见的全局只有白名单放行的那些,且注入是惰性的——没读到的
    全局根本不会被转换。
This commit is contained in:
2026-09-05 22:11:55 +08:00
commit 0627d49425
18 changed files with 2905 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
package jscriptx
// Extension 是一个作用域级扩展:给脚本添一个全局对象。
//
// 扩展实例由调用方自己创建,跟着 ctx 走:
//
// st := jscriptx.NewStore()
// tx := myTx(db)
// ctx = jscriptx.WithScope(ctx, jscriptx.ScopeExtensions(st, tx))
//
// obj, err := ctrl.New(ctx) // 脚本里能用 store 和 tx
// defer obj.Close()
//
// st.Get("count") // Go 侧拿的就是同一份,不用再取回
//
// 同一个 ctx 下 New 出来的所有实例共享同一份扩展对象——这就是多个 controller
// 共享数据的方式:共享的是 Go 侧对象,不是共用 Runtime。
//
// 业务写自己的扩展只要实现这三个方法:
//
// type tx struct{ conn *sql.Tx }
//
// func (t *tx) Name() string { return "tx" }
// func (t *tx) Bindings() map[string]any {
// return map[string]any{"commit": t.conn.Commit, "rollback": t.conn.Rollback}
// }
// func (t *tx) Module() string { return txTypings }
type Extension interface {
// Name 是它在脚本里的全局名,必须是合法的 JS 标识符。
// 跟白名单(WithGlobals)同名时以白名单为准。
Name() string
// Bindings 是暴露给脚本的方法集。方法名**大写开头**,跟脚本里能碰到的
// 其它东西保持一致——传进来的 Go 对象(m.GetCode()、res.GetDBTable()
// 用的都是 Go 的方法名,扩展再用小写的话,同一行代码里两种风格混着写。
Bindings() map[string]any
// Module 返回配套 TypeScript 模块的 import 路径和源码,让脚本能拿到类型:
//
// import store from "@jscriptx/store"
//
// 路径要加 scope 前缀,免得跟 node_modules 里的包撞名。用什么 scope 自己定,
// 框架层那套用的是 @jscriptx/,业务自己的可以用 @fsdpf/ 之类。
// 路径返回空字符串表示不提供模块,脚本只能用全局变量的写法。
//
// 模块本身只是个门面:把全局对象转发出来,附上类型声明。真正的实现在 Go 侧。
Module() (path, source string)
}