用 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 在脚本里表现为抛异常,不占返回值位置。
- 脚本能看见的全局只有白名单放行的那些,且注入是惰性的——没读到的
全局根本不会被转换。
42 lines
1.0 KiB
Go
42 lines
1.0 KiB
Go
package jscriptx
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
)
|
|
|
|
// newConsole 造一个绑定到指定脚本的 console 对象,脚本里的 console.log 等
|
|
// 直接落到结构化日志里,带上 script 字段,方便按脚本名检索。
|
|
func newConsole(logger *slog.Logger, script string) map[string]any {
|
|
at := func(level slog.Level) func(args ...any) {
|
|
return func(args ...any) {
|
|
if !logger.Enabled(context.Background(), level) {
|
|
return
|
|
}
|
|
logger.LogAttrs(context.Background(), level, joinArgs(args),
|
|
slog.String("script", script), slog.String("source", "console"))
|
|
}
|
|
}
|
|
return map[string]any{
|
|
"log": at(slog.LevelInfo),
|
|
"info": at(slog.LevelInfo),
|
|
"debug": at(slog.LevelDebug),
|
|
"warn": at(slog.LevelWarn),
|
|
"error": at(slog.LevelError),
|
|
}
|
|
}
|
|
|
|
// joinArgs 按 console 的习惯用空格拼接参数。
|
|
func joinArgs(args []any) string {
|
|
if len(args) == 0 {
|
|
return ""
|
|
}
|
|
parts := make([]string, 0, len(args))
|
|
for _, a := range args {
|
|
parts = append(parts, fmt.Sprintf("%v", a))
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|