1. 本文目标
前面几篇已经把编译器和 runtime 的核心部件搭起来了。接下来要解决的是工程化闭环:
如何把 bytecode、constantPool、函数元信息和 runtime 打包成一个可运行 JS 文件?
本篇覆盖:
ProgramArtifact- Runtime Source
- Pack
- IIFE / CJS / ESM 包装
- Module Bundling
- CLI
- Debug Info
2. 为什么需要打包
如果只生成 bytecode:
[<span>4</span>, <span>0</span>, <span>0</span>, <span>19</span>, <span>0</span>]
这还只是编译数据,用户不能把它当作完整程序直接运行。
要真正执行,还需要 runtime:
<span>function</span> <span>__jsvmpRun</span>(<span>metadata, globalObject</span>) { ... }
同时还要把 runtime 所需的 metadata 一并带上:
{
bytecode,
constantPool,
functions
}
形象化比喻:游戏卡带
bytecode 像游戏关卡数据,runtime 像游戏机,metadata 像说明书。三者单独存在都不构成最终交付物;打包就是把它们装进一张卡带,让用户拿到后可以直接运行。
3. 前置知识
| 概念 | 说明 |
|---|---|
| Artifact | 编译产物数据 |
| Runtime | 执行 bytecode 的解释器 |
| Pack | 把 runtime 与 artifact 拼成 JS |
| IIFE | 立即执行函数表达式 |
| CJS | CommonJS 输出 |
| ESM | ES Module 输出 |
| CLI | 命令行入口 |
4. 源码中的对应位置
| 概念 | 源码位置 | 说明 |
|---|---|---|
| `compile()` | `src/compiler/pipeline.ts` | 主编译流程 |
| `ProgramArtifact` | `src/compiler/types.ts` | bytecode 与元数据结构 |
| `generateRuntimeSource()` | `src/compiler/runtime-gen.ts` | 生成运行时源码 |
| `packArtifact()` | `src/compiler/pack.ts` | 打包 runtime + metadata |
| `bundle()` | `src/compiler/bundler.ts` | 模块打包 |
| CLI | `src/cli.ts` | 命令行参数与入口 |
5. 核心数据结构
ProgramArtifact
它解决什么问题
把 runtime 执行程序时需要的 bytecode、常量和函数信息集中保存。
教学版简化实现
<span>const</span> artifact = {
<span>format</span>: <span>'iife'</span>,
<span>bytecode</span>: [],
<span>constantPool</span>: [],
<span>functions</span>: [],
<span>entryFunctionId</span>: <span>0</span>,
};
Runtime Source
它解决什么问题
提供真正读取并解释 bytecode 的执行函数。
教学版简化实现
<span>function</span> <span>generateRuntimeSource</span>(<span></span>) {
<span>return</span> <span>`function __run(metadata){ return 0 }`</span>;
}
Packed Output
它解决什么问题
把 runtime 与 artifact 合到一起,让用户得到一个可直接运行的 JS 文件。
教学版简化实现
<span>const</span> output = <span>`
(function(){
const metadata = <span>${<span>JSON</span>.stringify(artifact)}</span>;
<span>${runtimeSource}</span>
return __run(metadata);
})()
`</span>;
6. Mermaid 图解
flowchart LR
A["Source File"] --> B["compile()"]
B --> C["ProgramArtifact"]
C --> D["Runtime Source"]
D --> E["packArtifact()"]
C --> E
E --> F["Output JS"]
flowchart TD
A["CLI input"] --> B["resolve format"]
B --> C{"bundle?"}
C -->|yes| D["bundle modules"]
C -->|no| E["compile single file"]
D --> F["parse/lower/emit"]
E --> F
F --> G["pack"]
G --> H["write output"]
7. 伪代码
function compile(inputFile, outputFile, options):
source = read inputFile
format = resolveFormat(source)
if bundle enabled:
source = bundle(source)
ast = parse(source)
ir = lower(ast)
bytecode = emit(ir)
runtime = generateRuntimeSource()
code = pack(runtime, bytecode)
write outputFile
return code
8. 教学版实现代码
<span>function</span> <span>packArtifact</span>(<span>artifact</span>) {
<span>const</span> metadata = <span>JSON</span>.<span>stringify</span>(artifact);
<span>return</span> <span>`
(function () {
function __miniRun(metadata) {
const code = metadata.bytecode;
const pool = metadata.constantPool;
const regs = [];
let pc = 0;
while (pc < code.length) {
const op = code[pc++];
if (op === 4) regs[code[pc++]] = pool[code[pc++]];
else if (op === 19) return regs[code[pc++]];
else throw new Error('unknown opcode: ' + op);
}
}
return __miniRun(<span>${metadata}</span>);
})()
`</span>.<span>trim</span>();
}
9. 示例输入与输出
Artifact
<span>const</span> artifact = {
<span>bytecode</span>: [<span>4</span>, <span>0</span>, <span>0</span>, <span>19</span>, <span>0</span>],
<span>constantPool</span>: [<span>42</span>],
};
Packed Output
<span>const</span> code = <span>packArtifact</span>(artifact);
<span>console</span>.<span>log</span>(<span>eval</span>(code)); <span>// 42</span>
10. 执行过程拆解
| Step | 阶段 | 产物 |
|---|---|---|
| 1 | parse/lower/emit | `ProgramArtifact` |
| 2 | generate runtime | `__jsvmpRun` 源码 |
| 3 | pack | 自包含 JS 文件 |
| 4 | execute output | VM runtime 执行 bytecode |
| 5 | return | 输出结果 |
11. 与原始源码的差异
| 主题 | 教学版 | 正式源码 |
|---|---|---|
| Runtime | 极简同步执行 | 支持 sync/async/generator |
| Format | 只演示 IIFE | 支持 IIFE/ESM/CJS |
| Module | 不支持 | 支持 ESM/CJS bundling |
| Global | 简化 | 注入 `__vmp_global`、require/module/exports |
| Debug | 不支持 | 可包含 debugInfo |
12. 常见问题
Q1:为什么 output 仍然是 JS?
因为这个项目做的是 JS virtualization:源码先变成 bytecode,再由自定义 JS runtime 解释,所以最终产物仍然需要交给 JS 引擎运行。
Q2:打包是不是混淆?
不是。打包只是把 runtime 和 metadata 放到一起。混淆属于额外阶段。
13. 本文小结
到这里,从“编译数据”到“可运行产物”的工程闭环就完整了。
下一篇将继续讲:
第 12 篇:完整最小 JSVM 实现与源码设计复盘
为 JS VM 编译产物提供从数据到可执行文件的完整打包思路,适合学习编译器/运行时工程化、CLI 工具链与模块封装的开发者参考。