Gox
用 Go 从零实现的 JavaScript 运行时:词法分析 → 语法分析 → 字节码编译 → 栈式虚拟机执行。
单二进制、零 cgo、零外部运行时依赖,自带软件光栅化 GUI 与脚本打包器。

📖 官网与使用教程 | GUI 开发指南 | npm 包
English version: README.en.md。两份 README 同步维护,描述同一个构建。
目录
概览
Gox 是一个用纯 Go 实现的 JavaScript(ES6+ 子集)运行时。整条编译管线 —— 词法分析、语法分析、
字节码生成、栈式虚拟机 —— 都从零实现,不依赖 V8/QuickJS 等任何现成引擎,也不依赖 cgo。
在此之上,Gox 补齐了脚本语言通常缺失的那一层:一套自研的桌面 GUI 渲染层(纯 Go 软件光栅化,
flex 布局 + JSX + 信号驱动更新,win32 / X11 / cocoa 窗口后端)、一组宿主能力模块(文件、HTTP、剪贴板、
原生对话框、持久化存储),以及把脚本打包成独立可执行文件的工具链。
它的定位是"小而完整":一个 go build 得到一个可执行文件,可以把 JS 当脚本层嵌进 Go 程序,
也可以直接用它写带界面的小工具并打包分发。
设计取向
| 取向 |
做法 |
代价 |
| 单二进制分发 |
静态编译、无动态库依赖;跨平台只需换 GOOS/GOARCH,无需目标机工具链 |
二进制体积较大 |
| 零 cgo |
后端直连系统 API(Windows 走 syscall、Linux 走 X 协议、macOS 走 cocoa/purego) |
不支持 -race 检测 |
| GUI 不是外挂 |
渲染、布局、命中测试、脏矩形重绘全在运行时内部,脚本侧只有 JSX 与信号 |
渲染能力受软件光栅化上限约束 |
| 确定性单线程 |
VM 单线程执行,网络 I/O 在 goroutine 中完成后投递回主线程,回调无需加锁 |
无真正的并行执行 |
| 缺能力明说缺 |
宿主能力走 canIUse() + 8 个统一错误码,不返回假数据、不假装在播 |
某些场景下比"尽力而为"更早暴露问题 |
Gox 不适合
把边界写在前面,比让读者踩坑再回头更省事:
- 不适合做 Web 后端或替代 Node.js —— 不支持 CommonJS(
require)与 npm 生态的大多数包,见 兼容性矩阵。
- 不适合 CPU 密集的计算任务 —— 纯解释器,无 JIT,
fib28 比 goja 慢约 3 倍、比 V8 慢一个数量级,见 性能。
- 不适合需要完整 ES 规范的生产系统 —— test262
built-ins 合规率 62%,Temporal / RegExp 等子集仍有成块缺口。
- 不适合"一次写完到处跑"的幻想 —— 三端形态(折叠屏、安全区、权限)差异真实存在,需要按平台适配。
适用场景:用 JS 写跨平台桌面小工具;把 JS 作为配置与插件层嵌入 Go 应用;学习编译器与虚拟机实现。
项目状态
Gox 处于 0.x 活跃开发期 —— 公共 API 可能在 minor 版本间发生破坏性变更(0.x 下 minor 即破坏位)。
test262 合规率
官方 ECMAScript 一致性套件,由 CI 每周定时跑批并自动回写徽章(docs/test262-*.json):
| 套件 |
合规率 |
口径 |
language |
97.0% |
语言语义(语句 / 表达式 / 模块 / 函数 / class 等) |
built-ins |
62.1% |
内置对象(Array / String / Temporal / TypedArray …),14804 / 23823 |
两个套件分母不同、构成完全不同,所以各守一条独立的下限基线;intl402(需 i18n)Gox 未实现,不在 runner 的套件内。
合规率是趋势观测而非验收门禁 —— 失败面按「目录族 × 失败形态」聚类成 502 个簇,排期按簇开单。
完整口径与跑法见 官网 · 性能与合规。
平台支持矩阵
| 平台 |
运行时 |
GUI 窗口后端 |
CI 覆盖 |
| Windows x64 |
✅ |
win32(纯 syscall) |
ci.yml 构建 + 测试 |
| Linux x64 / arm64 |
✅ |
X11(纯协议) |
ci.yml 构建 + 测试 |
| macOS x64 / arm64 |
✅ |
cocoa(purego) |
ci.yml 构建 + 测试 |
| Android arm64 |
✅ 预编译库 |
壳工程(Kotlin 宿主) |
mobile-smoke.yml + 折叠屏模拟器回归 |
| iOS arm64 |
✅ 预编译库 |
壳工程(Swift 宿主) |
mobile-smoke.yml 模拟器三机型;真机验收待做 |
| 鸿蒙 arm64 |
⚠️ 交叉编译通过 |
壳工程(ArkTS),六个能力模块仍是桩 |
构建通过;HAP 签名是硬阻塞,模拟器验收待做 |
已知缺口
- 富文本(行内混排样式)尚未封装;虚拟化长列表已由
<scroll vlist> 落地
- 原生能力层中依赖真机的部分(相机 / 定位 / 相册 / 权限)桌面端诚实报
unsupported
- CommonJS /
require 明确不做;npm 生态仅 uuid 等极少数库部分可用
- 逐项见 docs/gui-guide.md 与官网 限制与常见误区
快速开始
环境要求
| 场景 |
要求 |
| 从源码构建 |
Go 1.26.2 或更高(零 cgo,无需 C 工具链) |
| 仅运行脚本 |
无 —— npm 包已内置各平台预编译二进制,只需 Node.js ≥ 14 |
| GUI 应用 |
Windows(win32)、Linux(X11)或 macOS(cocoa) |
三种装入方式
# 1) npm —— 最省事,五平台预编译二进制随包分发
npm i -g @goxjs/goxjs # 安装后可直接使用 goxjs 命令
npx goxjs app.js # 或不安装,直接运行
# 2) 源码构建 —— 需要一个 Go 工具链
git clone https://github.com/14752222/Gox.git
cd Gox
go build ./cmd/gox # 生成可执行文件;Windows 下为 gox.exe
# 3) go install —— 直接装进 GOBIN(需先发布过 tag)
go install github.com/14752222/Gox/cmd/gox@latest
npm/ · website/ · gox-logo-concepts/ 是三个独立仓库(gox-npm ·
gox-website ·
gox-logo-concepts),2026-10-12 起不再以
git submodule 挂载,克隆主仓不需要 --recurse-submodules。CI 构建时会自动 checkout
npm/ 与 website/;本地要跑 scripts/check-registries.py 的话,先
git clone https://github.com/14752222/gox-npm npm(否则读不到 npm/package.json)。
交互式 REPL
./gox # Unix;Windows 下为 gox.exe
Gox REPL (ES6 subset)
Type :exit to quit, :help for help
> let x = 10
> let y = 20
> x + y
30
> [1, 2, 3].map(v => v * 2)
[2, 4, 6]
REPL 命令::help 查看帮助、:clear 重置环境、:exit 退出。
运行脚本
./gox example.js # npm 安装的 goxjs 命令用法相同
脚本执行完毕后会回显最后一个顶层表达式的值(undefined 除外),并等待定时器与异步回调
全部执行完再退出。
首屏示例:单双栏自适应列表页
窗口宽度 < 600dp 时单栏,拖宽到 ≥ 600dp 自动变双栏(左侧列表 + 右侧详情)。
拖拽窗口边缘即触发,不用监听 resize,也不用轮询尺寸。完整可跑文件:
testdata/adaptive_list_demo.js(gox testdata/adaptive_list_demo.js)。
import { h, render } from "gx/gfx";
import { createSignal } from "gx/solid";
import { useBreakpoint, matchBreakpoint } from "gx/viewport";
import { Switch, Match } from "gx/view";
const bp = useBreakpoint(); // () => "sm" | "md" | "lg" | "xl"(订阅型)
const [selected, setSelected] = createSignal(0);
const ITEMS = [
{ id: "a", title: "编译管线", desc: "词法 → 语法 → 字节码 → 栈式 VM,160 个操作码。" },
{ id: "b", title: "软件光栅化 GUI", desc: "flex 布局 + JSX + 信号驱动,三后端。" },
{ id: "c", title: "脚本打包器", desc: "把 .js 打成单个可执行文件。" },
];
// 布局分支由断点名声明(一张 sm/md/lg/xl 的表),
// 而不是把同一个宽度判断散落在 N 处 if 里 —— 加一档只改表,不改判断。
const layout = () => {
bp(); // ← 无条件读一次 = 订阅登记。删掉这行,整页不再自适应(无警告)
return matchBreakpoint({
sm: { cols: 1, side: false }, // <600dp 单栏
md: { cols: 2, side: true }, // >=600dp 双栏
lg: { cols: 2, side: true },
xl: { cols: 2, side: true },
});
};
const List = () => (
<column gap={6}>
<view each={ITEMS} key="id">
{(it, i) => (
<row gap={6} padding={8} radius={6}
background={selected() === i ? "#dce8ff" : "#f2f5fa"}
onClick={() => setSelected(i)}>
<text font={13} color="#334">{it.title}</text>
</row>
)}
</view>
</column>
);
const Detail = () => (
<column gap={6} padding={12} radius={8} background="#ffffff">
<text font={14} color="#223">{ITEMS[selected()].title}</text>
<text font={12} color="#667">{ITEMS[selected()].desc}</text>
</column>
);
render(
<window title="自适应列表页" width={520} height={420}>
<column gap={10} padding={14}>
<text font={15}>{() => `breakpoint = ${bp()} → ${layout().side ? "双栏" : "单栏"}`}</text>
<Switch>
<Match when={() => layout().side}>
<row gap={10}>
<column width={210} gap={6}><List /></column>
<column flex={1}><Detail /></column>
</row>
</Match>
<Match when={() => true}>
<column gap={8}>
<List />
<Detail />
</column>
</Match>
</Switch>
</column>
</window>
);
两个最容易踩的坑(示例注释里同样写明):
- 纯读数会短路掉订阅 ——
breakpoint() / posture() / hasFold() 是纯读数、不订阅;只有
useBreakpoint() 这类两段式返回的取值函数才订阅。把纯读数写在三元的第一操作数或 && 左侧,
被短路的那支一次都没读 ⇒ 零依赖 ⇒ 之后宽度变了这一支永不重跑,没有任何警告。
gfx/adaptive_list_test.go 把这条钉成了断言:纯读数快照拖宽后仍停在首帧的值。
- 坐标单位是两套 ——
reservedRegions() 的 x/y/width/height 是设备像素(折痕、挖孔),
safeAreaStyle() 与脚本显式写的尺寸属性是 dp(内核每帧乘 Display.Scale)。
折痕避让时不要把前者直接喂给布局属性,先除 pixelRatio。
能力总览
| 能力域 |
内容 |
文档 |
| 语言运行时 |
160 个操作码、定长 3 字节指令;ES6+ 语言子集;完整内置对象族 |
语言与内置对象 |
| GUI 渲染层 |
63 个内置元素、flex 布局、JSX、脏矩形局部重绘、win32 / X11 / cocoa 三后端 |
docs/gui-guide.md |
| 宿主能力 |
全局注入 fs / path / http / fetch / process / stats / crypto / hash;gx/* 内置模块 23 个 |
宿主能力模块 |
| 移动端与形态 |
Android / iOS / 鸿蒙壳工程 + gx/screen 折叠屏与安全区语义 |
docs/foldable-adaptation.md |
| 工具链 |
REPL、gox create 脚手架、gox dev 热更新、gox cert / gox build / gox sync / gox icon、jsbuild 打包 |
打包与工具链 |
语言与内置对象
语法子集
let/const/var、函数与箭头函数、闭包、class(含私有字段 #name)、async/await、
解构赋值、剩余/默认参数、展开、模板字符串、for...of、for...in、try/catch/throw、
可选链 ?.、空值合并 ??、ES 模块 import/export,以及 JSX 语法
(编译期降级为 h(tag, props, ...children) 调用)。
let x = 10
let y = 20
console.log(x + y) // 30
console.log([1, 2, 3].map(v => v * 2)) // [2, 4, 6]
console.log([1, 2, 3].filter(v => v > 1).reduce((a, b) => a + b)) // 5
console.log(Math.hypot(3, 4)) // 5
let name = "Gox"
console.log(`Hello, ${name}!`) // Hello, Gox!
let [a, b] = [1, 2] // 解构赋值
console.log(a + b) // 3
class Point {
constructor(x, y) { this.x = x; this.y = y }
norm() { return Math.hypot(this.x, this.y) }
}
console.log(new Point(3, 4).norm()) // 5
async function main() {
let v = await delay(10, "timer done") // delay(ms, value) 返回 Promise
console.log(v)
}
main()
5
Promise { <pending> } ← 顶层回显:main() 调用的返回值
timer done ← 定时器到期后,await 继续执行
语法提示:异步函数写 async function 或 async 箭头 async () => {} 都行;顶层不能 await。
ES 模块 —— lib.js:
export const PI = 3.14
export function double(x) { return x * 2 }
main.js:
import { PI, double } from "./lib.js"
setTimeout(() => console.log("tick"), 10)
console.log(double(PI))
6.28
tick
内置对象
Array / String / Number / Object / Boolean / Math / JSON /
Map / Set / WeakMap / WeakSet / Symbol / BigInt / RegExp / Proxy / Reflect /
Iterator / Promise / ArrayBuffer / DataView(TypedArray 家族)/ WeakRef /
FinalizationRegistry / 完整错误类型族 / Temporal(现代日期时间 API,本运行时不含 Date)。
响应式编程
Dart GetX 风格的 obs / computed / ever / once:
let count = obs(0)
ever(count, v => console.log("count =", v)) // 订阅时立即以当前值回调一次
count.value = 1
count.value = 2
count.value = 2 // 值未变化,不触发通知
count = 0
count = 1
count = 2
2 ← 顶层回显:最后一条赋值表达式的值
以及 SolidJS 风格的 gx/solid 信号(createSignal / createEffect / createMemo /
createResource / onMount / onCleanup)。
GUI 桌面应用
gx/gfx + gx/solid 提供 JSX 声明式 UI 与信号驱动的响应式更新,渲染器为纯 Go 软件光栅化
(无 cgo、无动态库依赖)。
用脚手架起工程
gox create my-app # 同为 goxjs create / npx @goxjs/goxjs create
gox create my-app --ts # TypeScript/TSX 版: .tsx 直接跑, 无 node 端构建
cd my-app && npm install && npm run dev
生成 package.json、入口 src/main.js、根组件 src/app.js、共享状态 src/store.js、
设计令牌 src/theme.js 与 src/components/ 下三个示例组件(计数器 / 待办列表 / 多状态),
把信号、元素级指令(each / show)、model 双向绑定与 Switch / Match 各演示一遍。
--ts 生成 TypeScript 版:入口改 src/main.tsx、附 tsconfig.json 与 src/gox.d.ts
(IDE 类型提示),源码在加载时自动完成类型剥离(内嵌 esbuild 转译,JSX 原样保留)——
没有 node 端构建步骤,类型检查交给 IDE,运行时零开销。
模板是 scaffold/template/ 下的真实文件,由 go:embed 嵌进二进制
(改模板要重新编译才生效;scaffold/scaffold_test.go 会保证生成出来的 JS/TS 仍能过
转译 + lexer → parser → compiler,gfx/scaffold_project_test.go 会真的把它挂载起来点一遍)。
最小手写版
import { h, render, createSignal } from "gox" // 聚合入口:所有 gx/* 导出的并集
// 等价的细分写法:import { h, render } from "gx/gfx"; import { createSignal } from "gx/solid"
const [count, setCount] = createSignal(0)
render(
<window title="Counter" width={400} height={300}>
<column gap={8} padding={16}>
<text font={20}>计数:{ count()}</text>
<button onClick={() => setCount(c => c + 1)}>加一</button>
</column>
</window>
)
./gox counter.js # 直接运行,弹出 400x300 窗口
点击按钮 → 信号更新 → 依赖它的属性/文本节点标脏 → 脏矩形合并后只重绘受影响区域。render()
返回窗口句柄,可调用多次开多窗口(各有独立元素树与焦点,全关才退出进程)。
GUI 能力清单
- 63 个内置元素 —— 布局(
column / row / grid / scroll / view)、表单(input /
textarea / select / slider / checkbox / radio / switch)、数据展示(table / tree /
list-item / tabs / pagination)、反馈(alert / tag / badge / avatar / empty /
icon / spinner / skeleton / progress / tooltip)、弹层(dialog / drawer / toast)、
媒体(<video> / <audio>)、自绘画布、自绘菜单栏与右键菜单
- 声明式界面是元素级指令 ——
model 双向绑定、each 列表(keyed 复用)、show 条件保活显隐,
多分支用 gx/view 的 Switch + Match
- 路由与屏幕 ——
gx/router(路由表 / 参数匹配 / 三级守卫 / 懒加载 / 历史栈 / 多窗口作用域 /
折叠屏双栏)与 gx/screen(多显示器 / 窗口显示信息)
- 媒体组件真的能播 —— 内核只定义宿主契约(
nativeVideoHost / nativeAudioHost):
能挂载、能收事件、playing / muted / loop / volume / fit 全受控,后端没这块能力就明说缺
(封面占位 + 一次 onError({code:"unsupported"}) + canIUse("video"/"audio") 照实回答 false)。
Linux 默认构建挂 gfx/softmedia 软件后端:视频 GIF(含 disposal 合成)与 MJPEG,音频 WAV
(PCM 8/16/24/32 位 + IEEE float),音频经 PulseAudio purego dlopen 输出(零 cgo)。
h264 / aac / mp3 留给平台媒体层,边界见 docs/media-decision.md
完整参考 —— 事件模型、内置元素属性表、弹性布局与装饰绘制、受控组件与输入法、过渡动画、
原生对话框 / 剪贴板 / 持久化存储 / 菜单栏、多窗口语义与平台差异 ——
见 docs/gui-guide.md,可直接运行的示例见
testdata/(counter_demo.js、form_demo.js、multiwindow_demo.js、
menu_demo.js、view_demo.js、router_demo.js、router_window_demo.js 等 108 个)。
路由与屏幕适配另有一份专门手册:docs/gui-router.md。
宿主能力模块
全局对象(无需 import)
fs / path / http / fetch / process / stats / crypto / hash:
const text = fs.readFileSync("a.txt") // 同步:失败抛 JS 异常
const data = await fs.readFile("a.txt") // 异步:无回调时返回 Promise
fs.mkdirSync("a/b", { recursive: true })
const server = http.createServer(function (req, res) {
res.writeHead(200, { "Content-Type": "text/plain" })
res.end("hello")
})
server.listen(0, function () { console.log(server.port) })
const res = await fetch("https://example.com")
console.log(res.status, await res.text())
异步 I/O 的实现模型是跨 goroutine I/O + 投递回 VM 单线程:网络读写不触碰 VM 状态,结果经
零延时定时器回到事件循环执行 JS 回调,因此回调里访问 VM 无需加锁。
一份完整的 HTTP 示例(起服务于端口 0、路由/查询参数/JSON 请求体/404-405-500 各状态码、
回调式 http.get · http.request 与 Promise 式 fetch 对照、异步响应与 server.close 收尾):
testdata/http_demo.js —— gox testdata/http_demo.js 即可运行。
摘要与随机数 —— crypto 与 hash 底层走 Go 标准库 crypto/*,零 cgo:
hash("gox"); // 同步 → 小写 hex (缺省 SHA-256)
hash("gox", "SHA-512"); // 算法名大小写/连字符不敏感
const ab = await crypto.subtle.digest("SHA-256", "gox"); // Promise → ArrayBuffer (32 字节)
crypto.randomUUID(); // RFC 4122 v4
crypto.getRandomValues(new Uint8Array(16)); // 原地填充, 上限 64KB
| 项 |
说明 |
| 算法 |
hash() 支持 MD5 / SHA-1 / SHA-256 / SHA-512;crypto.subtle.digest() 只认 SHA-1 / SHA-256 / SHA-512 |
| 参数序 |
hash(data, algo) 而 crypto.subtle.digest(algo, data) |
| 不做的事 |
加解密 / 签名 / KDF 不在本模块内 |
| 错误 |
不支持的算法或入参一律 TypeError,不会静默回退 |
gx/* 内置模块(需要 import)
注册表共 23 个模块:gox(聚合入口)、gx/solid、gx/gfx、gx/view、gx/router、
gx/screen、gx/viewport、gx/dialog、gx/storage、gx/update、gx/crypto、gx/theme、
gx/dev、gx/a11y、gx/device、gx/app、gx/geo、gx/media、gx/permission,
以及全局侧同样可 import 的 fs / http / path / process。
原生能力层六个模块共用一个宿主契约(NativeHost)。三种调用形态要在写代码时区分开:
import { deviceInfo, battery, isOnline, canIUse } from "gx/device"; // 拉取型 + 上报型
import { getLocation, watchLocation } from "gx/geo";
import { takePhoto } from "gx/media"; // 动作型
const info = deviceInfo(); // 同步可得
console.log(info.platform, info.model, battery().level + "%", isOnline());
if (canIUse("camera")) { // 事前判断能力,不要靠 catch 兜底
const photo = await takePhoto({ count: 1 }); // 失败会 reject,带 errCode
}
const stop = watchLocation((loc) => console.log(loc.latitude, loc.longitude));
try {
await getLocation({ highAccuracy: true });
} catch (e) {
if (e.errCode === "permission-denied") console.warn(e.message);
}
桌面后端直接实现其中能实现的部分(电量 / 网络 / 亮度 / 屏幕常亮 / 打开系统设置页),
给不出的一律诚实报 unsupported。移动端同样实现这套 NativeHost 契约,不必改内核 ——
Android 壳(Kotlin)与 iOS 壳(Swift)的六个模块基本齐备:Android 已在模拟器上实测通过
(上屏 / 触摸 / IME / 安全区 / 折叠屏),iOS 壳工程与构建脚本(含 TestFlight 打包)齐备、真机验收待做;
鸿蒙壳(ArkTS)目前只通了安全区与折叠上报两条纯上报通道,六个能力模块仍是桩。
逐方法(模块 × 平台)状态表、壳构建说明与首帧自检清单见 app/NATIVE-HOST.md;
三种形态的完整演示:testdata/native_demo.js。
三端验证范围(CI 矩阵口径)—— 能力矩阵按实际验证面写,不夸:
| 平台 |
构建冒烟(每次 push) |
设备回归 |
折叠语义 |
| Android |
mobile-smoke.yml 的 android job:libgox.so → assembleDebug APK |
android-fold-emulator job:pixel_fold 模拟器(API 34+,真能切铰链)上跑 CLOSED → HALF_FOLDED → OPENED 矩阵 + 截屏存档;真机侧 scripts/mobile-device-accept.sh --rounds 20 |
✅ 模拟器已覆盖折叠态切换 |
| iOS |
mobile-smoke.yml 的 ios job:xcodebuild 模拟器包 |
ios-device-matrix job:iPhone 16 / 16 Pro Max / iPad 三机型 × 竖横屏,simctl 截屏存档 |
⚠️ 模拟器没有折叠屏:reservedRegions(iOS 27.1+)在模拟器只报 flat;真机验收待做 |
| 鸿蒙 |
交叉编译 + 宿主契约测试 + assembleHap 构建通过 |
— |
❌ 模拟器验收待做(HAP 签名是硬阻塞);不写「已支持鸿蒙」 |
打包与工具链
jsbuild:脚本 → 独立可执行文件
jsbuild(packager/)把入口脚本及其相对 import 的模块嵌入一个生成的 Go 工程,编译成单文件
可执行程序,自带完整运行时:
go run ./packager app.js -o app.exe # CLI 应用
go run ./packager counter.js --gui -o counter.exe # GUI 应用
go run ./packager app.js --gui --target linux/amd64 # 纯 Go 交叉编译,无需目标机工具链
各平台的分发注意事项(Windows 图标与签名、Linux 打包格式、macOS .app bundle)见
docs/desktop-distribution.md。
命令行参数:打包产物与 gox <file> <args> 保持同一口径 —— process.argv 恒为
[可执行文件, 脚本路径, ...用户参数],用户参数始终从索引 2 开始。
于是同一份脚本源码跑和打包跑都能用 process.argv[2] 取到第一个用户参数。
多平台配置(gox.json / 权限 / 图标 / 证书)
gox create 生成的工程自带 gox.json(应用名、appId、版本、权限、各平台子配置)
与完整的 Android / iOS / 桌面资源骨架 + 默认图标:
| 命令 |
作用 |
gox sync |
把 gox.json 里的 permissions 幂等注入 AndroidManifest 与 iOS Info.plist |
gox icon |
以 assets/icon.png(1024×1024)为单源生成 Android mipmap、iOS AppIconSet、Windows .ico、macOS .icns、favicon |
gox cert |
一键生成平台签名证书(纯 Go 实现,无需 JDK/openssl):android / windows / harmony / ios |
gox build <android|ios|windows|macos> |
统一构建入口(sync → icon → 平台打包);--release 走签名发布包 |
字段说明、权限清单与图标规范见 docs/platform-config.md。
脚本运行时的权限模型(--allow-fs-write 等,默认最小权限)见 docs/permissions.md。
npm 包信息
| 项 |
值 |
| 包名 / 命令 |
@goxjs/goxjs → goxjs |
| 内含平台 |
macOS x64 / arm64、Linux x64 / arm64、Windows x64 |
| Node 版本 |
>=14(仅用于分发二进制,运行期不依赖 Node) |
| 版本真源 |
gox-npm 的 package.json 的 version 字段 |
| 移动端子包 |
@goxjs/goxjs-mobile-<platform>-<abi>(4 个,按需安装,不在主包依赖里) |
技术架构
编译与执行管线
.js 源码
│ lexer 词法分析
▼
Token 流
│ parser 语法分析(含 JSX 降级)
▼
AST (ast/)
│ compiler 符号表解析 + 字节码生成 (常量池)
▼
字节码 (bytecode/) 定长 3 字节: [opcode 1B][operand 2B 大端],160 个操作码
│ vm 栈式调用帧执行 + 定时器事件循环 + 模块加载
▼
结果
关键设计决策
- 定长指令编码 —— 无需变长解码状态机,直取操作数;常量池承担大对象引用。
- 单线程 VM + 跨 goroutine 调度 ——
http.createServer 等网络回调跨 goroutine 捕获后调度回
VM 单线程执行,无需锁。挂起任务保活事件循环,计数归零才允许进程退出。
- 可选能力走可选接口 —— GUI 后端通过
Surface + WindowFactory 抽象,换后端零改动脚本;
平台独有能力(IME、剪贴板、原生对话框、视频 / 音频)按后端探测,缺失时诚实降级而非报错。
- 固定分片保证合规率可复现 —— test262 runner 的分片数钉死 128 且与
-jobs 解耦:
引擎有包级可变状态,分片数若跟着并发度变,每个用例的"同进程邻居"就变,通过数会随 -jobs 翻转。
目录结构
| 目录 |
职责 |
lexer/ |
词法分析器(含 JSX 词法支持) |
parser/ |
语法分析器,生成 AST(含 JSX 语法降级) |
ast/ |
AST 节点定义 |
compiler/ |
AST → 字节码编译器(含符号表) |
bytecode/ |
操作码与字节码格式定义 |
vm/ |
栈式字节码虚拟机(调用帧、模块加载、定时器调度) |
object/ |
运行时对象系统(Number / Array / Map / Promise / Observable…) |
runtime/ |
全局环境 Environment |
stdlib/ |
标准库与宿主 API 实现(含 gx/solid 响应式信号) |
gfx/ |
自研 GUI 渲染层(软件光栅化、布局、命中测试、win32 / X11 / cocoa 后端) |
packager/ |
jsbuild 打包器(GUI 应用、交叉编译) |
scaffold/ |
gox create 的项目脚手架:template/ 是真实文件(go:embed 进二进制) |
tools/ |
开发工具:tstransform/(TS/TSX → JS)、icongen/(图标)、certgen/(证书)、update/(自更新)、perf/(基准) |
test/ |
测试相关:bench/ 性能剖析基准 |
testdata/ |
可直接运行的示例脚本(108 个) |
app/ |
移动端壳工程与 NATIVE-HOST.md 契约文档 |
docs/ |
对外文档 |
scripts/ |
构建与检查脚本:build-npm.sh、check-registries.py、check-imports.py、test262-cluster.py 等 |
.github/workflows/ |
CI:ci.yml 常规闸门、release.yml 发版、pages.yml 官网、test262.yml 合规率、mobile-smoke.yml 三端矩阵 |
性能
数字来自 CI runner 上的公开基准(bench-results/,AMD EPYC 7763)与官网实测页(Apple M2)。
含未达标项 —— 口径与阈值全部公开。
| 指标 |
gox |
goja(纯解释器对照) |
node / V8(JIT 对照) |
| 启动耗时 |
4 ms |
3 ms |
25 ms |
fib28 纯计算 |
631 ms |
200 ms |
34 ms |
| 1 万定时器 |
168 ms |
116 ms |
139 ms |
| 常驻内存 RSS |
25 MB |
10 MB |
42 MB |
结论:启动与内存与同类解释器同级;纯计算算力未达 goja 同级(fib28 慢约 3 倍),
这是纯解释器 + 无 JIT 的直接代价,不作为优化目标。UI 无 GC 毛刺达标
(重压聚合 p95 帧间隔 5.95ms ≤ 16.7ms;单次 GC 停顿 max 1.382ms ≤ 5ms)。
虚拟化长列表(<scroll vlist>)与行数脱钩:十万行首帧 89ms vs 全量 2187ms(约 24 倍),
滚动帧恒在 0.11~0.17ms vs 全量 999ms。
完整数字、跑法与判定阈值见 官网 · 性能与合规。
文档地图
下表只列 docs/ 里已定稿、对外发布的文档。
开发与测试
go test ./... # 运行全部测试(308 个测试文件 / 2419 个测试函数)
go run ./test/bench # 生成 cpu.prof 性能剖析
由于零 cgo,go test -race 在本项目不可用(Go 会要求 cgo 支持)。
深入参与开发(新增标准库 API、理解回调桥与内存管理)请阅读
docs/js-runtime-api-tutorial.md。
发版流程
@goxjs/goxjs 由 GitHub Actions 自动发布(.github/workflows/release.yml),
认证走 npm Trusted Publishing (OIDC),仓库里不需要任何 secret / token:
# 1. 改版本号 —— npm/package.json 的 version 是唯一真源,npm/ 是独立仓库:
$EDITOR npm/package.json
git -C npm commit -am "chore(release): 0.15.0" && git -C npm push
# 2. 回主仓打 tag 并推送 —— tag 必须与 gox-npm 默认分支的版本一致
git tag v0.15.0 && git push Gox v0.15.0
# 3. 剩下交给 CI:交叉编译五平台二进制 → 打包校验 → OIDC 发布 → 自动打 tag + 建 Release
- 触发:push 到
main、推送 v* 标签、发布 Release,以及手动 workflow_dispatch
- 幂等:CI 先查 registry,该版本已存在则跳过并留一条 notice —— 重复推送不会误发
- 版本号:日常提交不会触发真发布;
cmd/gox/main.go 的 const version 与 npm/package.json
须同号(scripts/check-registries.py 卡死)
- 完整手册:见 docs/npm-release.md
贡献指南
欢迎提交 Issue 与 Pull Request —— 完整的参与方式、本地闸门、与本仓库特有的
「改一处、同步多处」纪律,见 CONTRIBUTING.md;
行为准则见 CODE_OF_CONDUCT.md。首次贡献可从带
good first issue 标签的 issue 入手。
开发环境:Go 1.26.2+,零 cgo(无需 C 工具链)。提交前请确保:
gofmt -l . # 应无输出
go build ./...
go vet ./...
go test ./...
python3 scripts/check-registries.py # 注册表一致性(CI 同款闸门)
python3 scripts/check-imports.py # 文档/示例 import 与导出表一致(CI 同款闸门)
提交规范:沿用仓库现有的 Conventional Commits 风格,scope 用受影响的模块或主题,中文描述:
feat(gfx): 新增 <slider> 的键盘调节支持
fix(vm): 修正 SET_INDEX 对非对象类型静默丢弃的问题
docs(gui-guide): 补充多窗口事件泵的轮询语义
ci(release): 打包闸改回 tar 校验,不再解析 npm 的输出
新增内置 GUI 组件时须同步四处(漏改会静默失效,不出编译错误):
| 位置 |
作用 |
gfx/node.go 的 knownTags |
注册标签名,否则 h() 会打印"未知标签"警告 |
gfx/layout.go 的 intrinsicSize |
声明固有尺寸 |
gfx/layout.go 的布局分派 |
声明子节点如何参与布局 |
gfx/raster.go 的 drawNode |
实现绘制;被 case 截走的标签需自行补画 background / border |
其他约定:
- 事件回调统一经
callHandler 分发;涉及窗口的操作从节点出发经 appOfNode(n) 取所属窗口(支持多窗口)
gx/* 的 JSX 属性与子节点在调用当场求值一次 —— 需要响应式就必须传函数(value: () => sig()),
子节点写 {() => sig()} 而不是 {sig()}),传值只是一张快照 —— 而且子节点写成快照
是全静默的(属性误用有警告,子节点没有),见 docs/gui-guide.md §8.1
- 示例脚本放在
testdata/ 并确保可直接运行
许可证
MIT License