package jscriptx // Loader 负责按名字提供脚本源码。脚本存在文件、数据库表还是配置中心,由实现方决定; // 本库只定义这个接口,不预设来源。 // // 给出的源码默认会由 Engine 交给 esbuild 打包(ESM/TypeScript 都在那一步抹平), // 所以直接返回原始源码即可。已经自己打过包的实现(比如 jscriptx/esm 的 Loader) // 再实现 Prepared 接口,Engine 就会跳过这一步。 // // version 用来判断脚本有没有变:Engine 拿它跟缓存里的版本比对, // 一致就复用已编译的 Program 和 VM 池,不一致才重新编译。取值随实现方便, // 比如文件的 mtime+size、数据库行的 updated_at、源码哈希都行; // 返回空字符串表示"我不提供版本号",Engine 会退化成拿源码算哈希。 // // 打开 WithAutoReload 后每次取脚本都会调一次 Load,实现方要保证这个调用足够轻 // (能只查版本就别每次全量读源码,或者自己加一层短 TTL 缓存)。 // // 脚本不存在时,返回的错误要能被 errors.Is(err, ErrScriptNotFound) 匹配上, // 调用方才好区分"脚本没配"和"加载出故障"。 // // 实现必须并发安全。 type Loader interface { Load(name string) (source string, version string, err error) } // LoaderFunc 让普通函数直接当 Loader 用: // // loader := jscriptx.LoaderFunc(func(name string) (string, string, error) { // row, err := db.QueryScript(name) // if err != nil { // return "", "", err // } // return row.Source, row.UpdatedAt, nil // }) type LoaderFunc func(name string) (source string, version string, err error) func (f LoaderFunc) Load(name string) (string, string, error) { return f(name) } // PreparedFunc 跟 LoaderFunc 一样是把函数当 Loader 用,区别是它声明"源码已经打好包了" // (见 Prepared)。多层叠放时各层的 Prepared 必须一致,拿它就能让一个自定义来源 // 跟 jscriptx/esm 那样的层对齐: // // db := jscriptx.PreparedFunc(func(name string) (string, string, error) { // row, err := db.QueryScript(name) // if err != nil { // return "", "", err // } // bundled, err := jscriptx.Bundle(name, row.Source) // 自己打包 // if err != nil { // return "", "", err // } // return bundled, row.UpdatedAt, nil // }) // // e, _ := jscriptx.New(jscriptx.WithLoader(esmLoader, db)) // // 名副其实是你自己的责任:说了打好包,交出去的就必须是打包产物, // 否则脚本里的 import/export 会原样进 goja,直接编译失败。 type PreparedFunc func(name string) (source string, version string, err error) func (f PreparedFunc) Load(name string) (string, string, error) { return f(name) } // Prepared 恒为 true,见 PreparedFunc 的说明。 func (f PreparedFunc) Prepared() bool { return true } var _ Prepared = PreparedFunc(nil) // Prepared 由那些自己已经把源码打包好的 Loader 实现(比如 jscriptx/esm 的 Loader), // Engine 见到它就跳过打包这一步。 // // 这不是性能优化,是正确性要求:打包产物是 IIFE,里面已经没有 export 了, // 再打包一遍会被 esbuild 当死代码整段摇空,脚本变成什么都不剩。 // 所以自己打过包的 Loader 必须实现它。 // // Engine 靠类型断言识别,方法签名写错了不会有编译错误。自己实现时最好钉一行 // // var _ jscriptx.Prepared = (*MyLoader)(nil) type Prepared interface { // Prepared 返回 true 表示 Load 给出的源码已经可以直接交给引擎编译。 Prepared() bool } func isPrepared(l Loader) bool { p, ok := l.(Prepared) return ok && p.Prepared() } // loadLocked 走 Loader 拿源码;cached 非空且版本一致时直接复用缓存。调用方必须持有 e.mu。 func (e *Engine) loadLocked(name string, cached *Script) (*Script, error) { source, version, err := e.loader.Load(name) if err != nil { return nil, newError(KindLoad, name, "", err, "加载脚本源码失败") } if cached != nil && version != "" && cached.version == version { return cached, nil } if version == "" { version = hashVersion(source) if cached != nil && cached.version == version { return cached, nil } } return e.compileLocked(name, source, version, isPrepared(e.loader)) }