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:
+41
@@ -0,0 +1,41 @@
|
||||
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, " ")
|
||||
}
|
||||
Reference in New Issue
Block a user