Files
jscriptx/static.go
T
what 0627d49425 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 在脚本里表现为抛异常,不占返回值位置。
  - 脚本能看见的全局只有白名单放行的那些,且注入是惰性的——没读到的
    全局根本不会被转换。
2026-09-05 22:11:55 +08:00

87 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package jscriptx
import "context"
// CallStatic 调用脚本导出的 class 上的**静态方法**。
//
// export default class PkgImportController {
// static Startup() { store.Set("pkg.registry", "https://…") }
// Execute(g) { … }
// }
//
// _, err := script.CallStatic(ctx, "Startup")
//
// 它跟 Call 有两点不同,都是"静态"这个语义要求的:
//
// - **不构造实例**constructor 不会跑。构造函数做的是"这一次调用的准备",
// 跟脚本级的初始化无关,跑它只会白费一遍还可能有副作用。
// - **VM 用完就丢**,不回池。静态方法一般只在脚本生命周期里跑一两次,
// 为它留一个 VM 不划算;而且它做的事通常是往 Go 侧扩展里写东西,
// 那些副作用留在 Go 那边,VM 本身没有保留的价值。
//
// 脚本没写这个静态方法时返回的错误可以被 errors.Is(err, ErrFuncNotFound) 匹配上
// ——生命周期钩子多半是可选的,用它来区分"没写"和"写了但炸了"
//
// if _, err := script.CallStatic(ctx, "Startup"); err != nil &&
// !errors.Is(err, ErrFuncNotFound) {
// return err // 真的出错了
// }
//
// 静态方法里能用 ctx 带进来的作用域扩展(WithScope),这正是它的用武之地:
// 把配置写进 store、往 Go 侧注册东西。但**它建的 JS 对象活不下来**——
// VM 一丢就没了,别指望后续调用能看到。
func (s *Script) CallStatic(ctx context.Context, fn string, args ...any) (any, error) {
if fn == DefaultFunc {
return nil, newError(KindNotFound, s.name, fn, ErrFuncNotFound,
"CallStatic 要给一个静态方法名")
}
return callAny(ctx, staticTarget{s}, fn, args)
}
// HasStatic 判断脚本导出的 class 上有没有这个静态方法。
//
// 它要建一个 VM 才能回答,所以别在热路径上反复调;只想"有就调"的话,
// 直接 CallStatic 然后判 ErrFuncNotFound 更省。
func (s *Script) HasStatic(fn string) bool {
if fn == DefaultFunc {
return false
}
vm, err := s.borrowStatic(context.Background())
if err != nil {
return false
}
_, _, _, ok := vm.lookup(fn)
return ok && vm.ctor != nil
}
// staticTarget 让静态调用复用 invoke 那一整套(超时中断、panic 恢复、错误分类),
// 只是换一种取 VM 的方式:不构造实例,用完丢弃。
type staticTarget struct{ s *Script }
func (t staticTarget) owner() *Script { return t.s }
func (t staticTarget) acquire(ctx context.Context) (*vmHandle, error) {
return t.s.borrowStatic(ctx)
}
// finish 直接丢弃,不回池——见 CallStatic 的说明。
func (t staticTarget) finish(*vmHandle, bool) { t.s.dropped.Add(1) }
// borrowStatic 造一个只装了 class、没有实例的 VM。
func (s *Script) borrowStatic(ctx context.Context) (*vmHandle, error) {
if s.closed.Load() {
return nil, newError(KindClosed, s.name, "", ErrClosed, "脚本已关闭")
}
var extra map[string]any
if sc, ok := scopeOf(ctx); ok {
extra = sc.vmGlobals()
}
rt, err := s.engine.newRuntime(s.name, extra)
if err != nil {
return nil, err
}
return s.load(ctx, rt, nil, true)
}