实测环境
本卷全部代码在下列环境中实测通过:
| 组件 | 版本 |
|---|---|
| Node.js | 26.5.0 |
| npm | 11.17.0 |
| TypeScript | 7.0.2 |
| vitest | 5.0.3 |
| 操作系统 | macOS(Darwin 25.3.0,Apple Silicon) |
Node 26 是本卷的硬性要求,因为它原生支持直接运行 .ts 文件。更低版本需要额外的加载器(tsx / ts-node),与本卷的「零构建步骤」口径不符。
为什么是 Node 26
Node 26 的 type-stripping 允许 node src/core/arena.ts 直接跑,靠的是只做类型剥离、不做语法转换:它把类型标注、interface、type 这些「可擦除」的东西删掉,剩下的就是普通 JS。代价是有些语法不能出现——凡是编译后需要生成运行时代码的,都不可擦除:
enum(会生成一个运行时对象)namespace(会生成一个运行时对象)- 构造函数参数属性(
constructor(private x: number),会生成赋值语句)
这三样在 Node 26 下会抛 ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX。本卷的替代写法是 const 对象 + as const 与显式字段声明,详见附录 D
。
从零搭建 playground
mkdir helio-playground && cd helio-playground
npm init -y
npm i -D typescript vitest
package.json
npm init -y 生成的 package.json 默认是 CommonJS,需要改成 ESM,否则 verbatimModuleSyntax 会报 TS1295:
{
"name": "helio-playground",
"version": "1.0.0",
"type": "module",
"scripts": {
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"typescript": "^7.0.2",
"vitest": "^5.0.3"
}
}
关键的一行是 "type": "module"。 漏了它,tsc 会把 .ts 当成 CommonJS 文件,报出一堆 TS1295 和 TS1287。
tsconfig.json
{
"compilerOptions": {
"target": "ES2023",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noEmit": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"allowImportingTsExtensions": true,
"isolatedModules": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"]
}
tsconfig 选项逐个解释
上面那份配置不是抄来的模板,每个选项都有理由。逐条说明:
| 选项 | 取值 | 作用 |
|---|---|---|
target | ES2023 | 语法下限。Node 26 支持到 ES2023+,没必要往下压 |
lib | ["ES2023","DOM","DOM.Iterable"] | 可用的全局类型。带上 DOM 是为了卷二用 WebGPU 时无缝衔接 |
module | NodeNext | 按 Node 的 ESM 规则解析模块,与 "type": "module" 配套 |
moduleResolution | NodeNext | 与 module 成对出现,缺一个解析就会错 |
strict | true | 打开全部严格检查(strictNullChecks 等),内核代码必须严 |
noEmit | true | 只做类型检查,不产出文件——Node 自己跑 .ts,不需要产物 |
erasableSyntaxOnly | true | 禁止 enum / namespace / 参数属性,保证 Node 26 能直接跑 |
verbatimModuleSyntax | true | 要求 import type 显式标注,避免类型导入被当值导入 |
allowImportingTsExtensions | true | 允许 import './arena.ts' 这种带扩展名的写法(Node 直跑必需) |
isolatedModules | true | 保证每个文件都能独立转译,与 type-stripping 的前提一致 |
skipLibCheck | true | 跳过 .d.ts 的内部一致性检查,显著加快 tsc |
forceConsistentCasingInFileNames | true | 大小写不一致直接报错,避免 macOS 大小写不敏感带来的假通过 |
四个「必开」的是 erasableSyntaxOnly、verbatimModuleSyntax、allowImportingTsExtensions、noEmit——它们直接对应「Node 26 直跑 .ts」这条工作方式。其余是为了工程严谨与编译速度。
版本核对
装完先核对版本,避免「版本对不上却怪代码」:
node --version # 应输出 v26.x 或更高
npx tsc --version
npx vitest --version
如果 node --version 低于 26,后面的 node src/xxx.ts 会报 ERR_UNKNOWN_FILE_EXTENSION。
各层代码怎么跑
本卷每节都标了可运行性层级(L1–L4 / B),跑法各不相同。
L1:纯 TypeScript 内核
Node 26 不需要任何 flag 就能跑 .ts:
node src/core/arena.ts
如果文件里有单元测试,用 vitest:
npx vitest run src/core/arena.test.ts
一次跑全部单测:
npx vitest run
L2:需要浏览器渲染(本卷用不到,卷二要用)
L2 的代码要真实浏览器环境才能跑。本卷是内核,没有任何 L2 内容;但卷二《渲染引擎》会大量使用,所以这里把准备步骤提前交代,免得读到卷二再回头折腾。
准备 headless Chromium 的两种常见方式:方式一用 Playwright(推荐,自带浏览器管理),方式二用 Puppeteer(会下载一份自带 Chromium)。
npm i -D playwright && npx playwright install chromium
npm i -D puppeteer
装好后用一段最小脚本验证浏览器能起来、WebGPU 能拿到适配器:
// snippet: 最小 L2 冒烟测试(卷二用,本卷不需要)
import { chromium } from 'playwright';
const browser = await chromium.launch({
args: ['--enable-unsafe-webgpu', '--use-angle=metal'], // 开启 WebGPU
});
const page = await browser.newPage();
const hasGpu = await page.evaluate(() => 'gpu' in navigator);
console.log('WebGPU available:', hasGpu);
await browser.close();
两点提醒:headless Chromium 默认不开 WebGPU,要显式加 --enable-unsafe-webgpu 等 flag;macOS 上走 Metal 后端,--use-angle=metal 更稳。这些细节本卷用不上,记在心里即可。
L3:只能做类型检查
npx tsc --noEmit
本卷的 L3 内容极少(主要是配置文件与类型垫片),大部分代码都是 L1。
B:性能基准
基准脚本产出 JSON,不参与断言:
node bench/soa-vs-aos.mjs
node --expose-gc bench/soa-vs-aos.mjs
基准跑得比单测慢,且对机器负载敏感,所以基准门禁是软告警——数字波动不会让流程失败,但会提示你复核。方法论与原始数字见附录 C 。
目录结构约定
本卷代码按模块组织,你可以照此搭建:
helio/
src/
core/ 内存、句柄、容器、数学(第 2、4–10 章)
ecs/ 实体、组件、系统(第 11–13 章)
jobs/ 任务系统(第 14–15 章)
reflect/ 反射与序列化(第 16–17 章)
platform/ 平台抽象层(第 3 章)
bench/ 性能基准(B 层)
各目录里你会遇到的典型文件:
| 路径 | 内容 | 章节 |
|---|---|---|
src/core/arena.ts | 线性 arena 与 scratch arena | 4.2 |
src/core/object-pool.ts | 对象池与 freelist | 5.1 |
src/core/slotmap.ts | 世代句柄与 slot map | 6.2 |
src/core/sparse-set.ts | 稀疏集 | 6.3 |
src/core/frame-timer.ts | 帧计时器与滑窗统计 | 2.1 |
src/core/fixed-step-loop.ts | 固定步长主循环 | 2.2 |
src/core/subsystem.ts | 子系统注册与生命周期编排 | 3.1 |
src/core/platform.ts | IPlatform 与平台无关辅助 | 3.2 |
src/ecs/world.ts | ECS 世界 | 12.3 |
src/jobs/job-system.ts | 任务系统入口 | 14.1 |
src/reflect/type-registry.ts | 类型注册表 | 16.2 |
bench/soa-vs-aos.mjs | SoA / AoS 基准 | 11.2 |
每个代码块顶部的 // file: 注释就是它在真实工程里的路径,照抄即可。
常见环境问题
跟读时最容易卡住的地方都在这张表里。按「现象 → 原因 → 解决」查:
| 现象 | 原因 | 解决 |
|---|---|---|
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX | 用了 enum / namespace / 参数属性 | 改用 const X = {...} as const 与显式字段 |
TS1295 / TS1287 | package.json 缺 "type": "module" | 加上它 |
TS1294: not allowed when 'erasableSyntaxOnly' is enabled | 写了不可擦除的语法 | 改写法,别关这个选项 |
TS5097: An import path can only end with a '.ts' extension ... | 缺 allowImportingTsExtensions | 打开该选项 |
Cannot find module './arena.ts' | 同上,或文件名大小写不符 | 打开选项并核对文件名 |
ERR_MODULE_NOT_FOUND | ESM 下 import 路径漏了 .ts 扩展名 | 补上 .ts |
Cannot find name 'process' / readFileSync | 缺 @types/node | npm i -D @types/node 或在 types 里加 |
Cannot find name 'GPUBufferUsage' | 缺 @webgpu/types(卷二才需要) | npm i -D @webgpu/types 并在 types 里加 |
node: command not found | Node 未安装或版本过低 | 装 Node ≥ 26 |
ERR_UNKNOWN_FILE_EXTENSION: Unknown file extension ".ts" | Node 版本低于 26,不认识 .ts | 升级到 Node ≥ 26 |
SyntaxError: Cannot use import statement outside a module | 同 TS1295,运行时表现 | 加 "type": "module" |
__dirname is not defined in ES module scope | ESM 下没有 __dirname | 用 import.meta.url 换算 |
| vitest 找不到测试文件 | 文件名不以 .test.ts 结尾 | 改名为 *.test.ts |
| vitest 无输出卡住 | watch 模式在等交互 | 用 vitest run 而非 vitest |
| Playwright 浏览器下载失败 | 代理/网络 | 设 HTTPS_PROXY,或改用系统已装的 Chromium |
| 基准数字与书里差很多 | JIT / GC / 负载 | 看方向不看倍数,见附录 C |
每章结束时的复现清单
跟着本卷做时,建议每章结束时确认下面几项。它们是「这一章我真的跑通了」的最小证据:
-
npx tsc --noEmit通过,零类型错误 -
npx vitest run全绿,没有 skipped 的用例 - 本章的 L1 代码能在
node下直接跑,不需要任何 flag - 新增文件都在正确的模块目录下(
core//ecs//jobs//reflect//platform/) - 如果本章有基准,跑一次
node bench/*.mjs,数字与附录 C 的方向一致(不必完全相等) - 本章用到的新类型/API,在附录 A 里能找到对应条目
复现的常见误区
最后提醒三个跟读时反复出现的误区,避免把时间花在错的地方。
误区一:急着装一堆工具。 本卷不需要 webpack、vite、ts-node、esbuild。Node 26 直跑 .ts,vitest 跑单测,仅此而已。多装的构建工具只会引入新的报错源。
误区二:把 noEmit 关掉想「编译出 JS」。 不需要。Node 直接吃 .ts,编译产物反而会让「我跑的是哪一份」变得含糊。本卷全程 noEmit。
误区三:把基准数字当验收标准。 基准是软告警。你的机器跑出来 1.4× 而书里写 1.59×,不是你没跑对,是微基准本就不可移植。真正要核对的是方向和结构,不是小数位。
环境没问题了,从第 1 章 1.1 引擎的分层与模块边界 开始。
阅读上一节:附录 A helio 内核 API 与模块速查 · 阅读下一节:附录 C 实测数据汇总
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。