模块一:基础认知 | 前置依赖:第 01 课 | 预计学习时间:60 分钟
学习目标
完成本课后,你将能够:
- 解释 Claude Code 为何选择 Bun 而非 Node.js,以及
feature()编译时替换的工作原理 - 描述 Ink 终端 React 渲染器的三阶段渲染管线
- 说明 Zod 如何同时服务于运行时校验和 TypeScript 类型推导
- 识别终端环境中 React 模式的适配差异
2.1 Bun:不只是更快的 Node.js
为什么选 Bun?
Claude Code 选择 Bun 作为运行时和打包器,核心原因不是"更快",而是 Bun 提供了一个 Node.js 没有的关键能力:编译时 Feature Gate。
// 来自 bun:bundle 的特殊导入
import { feature } from 'bun:bundle'
feature() 是 Bun 打包器的内置函数。它在打包时(而非运行时)被求值为 true 或 false,然后 Bun 的死代码消除(DCE)会移除不可达的分支。
feature() 的工作原理
// 源码中的写法(query.ts 第 15-21 行)
const reactiveCompact = feature('REACTIVE_COMPACT')
? (require('./services/compact/reactiveCompact.js') as typeof import('./services/compact/reactiveCompact.js'))
: null
// ─── 打包时,如果 REACTIVE_COMPACT = false ───
// 上面的代码会被编译为:
const reactiveCompact = null
// require() 调用被完全移除,相关模块不会被打包进最终产物
关键点: 这不是运行时的 if/else,而是编译时的代码消除。禁用的功能完全不存在于发布的包中 — 不占体积、不可被逆向发现(除非通过 sourcemap 泄露,正如这次事件)。
Bun 的其他角色
| 角色 | 说明 |
|---|---|
| 运行时 | 执行 TypeScript,无需预编译步骤 |
| 打包器 | 将数千个源文件打包为单个发布文件 |
| Feature Gate | feature() 实现编译时功能开关 |
| 包管理器 | 替代 npm/yarn 管理依赖 |
2.2 Ink:终端中的 React
什么是 Ink?
Ink 是一个让你用 React 组件构建终端 UI 的框架。Claude Code 没有使用社区版 Ink,而是在 ink/ 目录中维护了一个完整的自定义 fork(50+ 文件),以获得更深层的控制。
三阶段渲染管线
React 组件树 Ink 节点树 终端输出
┌──────────┐ Reconciler ┌──────────┐ Layout+Render ┌──────────┐
│ <Box> │ ──────────────► │ DOMNode │ ──────────────► │ ANSI │
│ <Text> │ (Stage 1) │ Layout │ (Stage 2+3) │ 字符序列 │
│ </Box> │ │ Yoga │ │ │
└──────────┘ └──────────┘ └──────────┘
Stage 1:React Reconciliation(ink/ink.tsx)
React Reconciler 处理组件树的 diff,生成 Ink 自己的 DOM 节点(DOMElement)。这和浏览器中 React 生成真实 DOM 节点的过程类似,但目标不是浏览器 DOM,而是 Ink 的虚拟终端节点。
// ink/ink.tsx — 创建自定义 Reconciler
import { createReconciler } from './reconciler.js'
// Reconciler 将 React 元素映射到 DOMElement 节点
Stage 2:Layout + Node-to-Output(ink/renderer.ts + ink/render-node-to-output.ts)
Ink 使用 Yoga(Facebook 的跨平台 Flexbox 布局引擎)计算每个节点的位置和尺寸。然后 renderNodeToOutput() 将布局后的节点树转换为二维字符矩阵。
// ink/renderer.ts — 创建渲染器
export function createRenderer() {
// charCache 跨帧复用,用于 grapheme clustering 优化
// Yoga 计算 Flexbox 布局
// renderNodeToOutput() 填充字符矩阵
}
Stage 3:Screen Output(ink/render-to-screen.ts + ink/terminal.ts)
字符矩阵最终被转换为 ANSI 转义序列,写入终端的 stdout。terminal.ts 封装了终端底层操作:光标移动、清屏、颜色控制。
核心优化: charCache 在帧间持久化,避免重复的 grapheme clustering 计算(处理 emoji 等多字节字符的分组),显著减少 CPU 消耗。
Ink 目录中的关键文件
| 文件 | 职责 |
|---|---|
ink.tsx | 入口,ThemeProvider 包装 |
reconciler.ts | React Reconciler 实现 |
renderer.ts | 渲染器核心(Yoga 布局 + 节点渲染) |
render-node-to-output.ts | 节点树 → 字符矩阵 |
render-to-screen.ts | 字符矩阵 → ANSI 序列 |
terminal.ts | 终端底层控制 |
parse-keypress.ts | 按键事件解析 |
colorize.ts | 颜色处理 |
bidi.ts | 双向文本(阿拉伯语、希伯来语) |
measure-text.ts | 文本宽度测量 |
line-width-cache.ts | 行宽缓存 |
2.3 TypeScript + Zod:类型安全的双保险
为什么不够只用 TypeScript?
TypeScript 的类型只存在于编译时。当 Claude API 返回一个 tool_use 块,或用户通过 MCP 传入参数时,TypeScript 无法保证运行时数据符合预期。这就是 Zod 的角色 — 运行时类型校验。
Zod 在工具系统中的应用
每个 Tool 都必须定义一个 inputSchema(Zod Schema),它同时承担三个职责:
// 以 ConfigTool 为例(tools/ConfigTool/ConfigTool.ts 第 35-47 行)
import { z } from 'zod/v4'
const inputSchema = z.strictObject({
setting: z.string(),
value: z.union([z.string(), z.number(), z.boolean()])
})
// 职责 1:运行时校验 — 拒绝不合法的输入
const validated = inputSchema.parse(untrustedInput)
// 职责 2:TypeScript 类型推导 — 自动生成类型
type ConfigInput = z.infer<typeof inputSchema>
// → { setting: string; value: string | number | boolean }
// 职责 3:JSON Schema 导出 — 告诉 Claude API 工具接受什么参数
const jsonSchema = zodToJsonSchema(inputSchema)
// → { type: "object", properties: { setting: { type: "string" }, ... } }
一个 Schema,三个消费者
┌─────────────────────────┐
│ Zod inputSchema │
│ z.strictObject({...}) │
└──────────┬──────────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌────────────┐ ┌──────────────┐ ┌──────────────┐
│ 运行时校验 │ │ TS 类型推导 │ │ JSON Schema │
│ .parse() │ │ z.infer<> │ │ 导出给 API │
│ 拒绝坏数据 │ │ 编译期类型 │ │ 工具参数描述 │
└────────────┘ └──────────────┘ └──────────────┘
lazySchema 模式
有些工具的 Schema 依赖运行时信息(如可用的 MCP 服务器列表),Claude Code 使用 lazySchema() 延迟求值:
// 概念示例
const inputSchema = lazySchema(() =>
z.object({
server: z.enum(getAvailableMcpServers()) // 运行时才知道
})
)
2.4 @anthropic-ai/sdk:API 客户端
SDK 的角色
@anthropic-ai/sdk 是 Anthropic 官方的 TypeScript SDK,Claude Code 用它与 Claude API 通信。
关键导入点
// query.ts — 导入类型定义
import type {
ToolResultBlockParam,
ToolUseBlock,
} from '@anthropic-ai/sdk/resources/index.mjs'
// services/api/claude.ts — 导入消息和流式支持
import { MessageStream } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
// tools/FileReadTool — 导入图片处理类型
import { Base64ImageSource } from '@anthropic-ai/sdk'
API 调用链路
query.ts
→ services/api/claude.ts // API 客户端封装
→ @anthropic-ai/sdk // 官方 SDK
→ Claude API (messages.create) // 远程 API
→ 流式响应 (SSE) // Server-Sent Events
SDK 负责:认证(OAuth/API Key)、请求构建、流式传输、类型安全、错误分类。
2.5 React 模式在终端中的适配
与浏览器 React 的关键差异
Claude Code 大量使用 React 模式(Hooks, Context, Components),但终端环境带来了一些关键适配:
差异 1:useLayoutEffect 替代 useEffect
// ink/hooks/use-input.ts
// 终端中必须用 useLayoutEffect 同步切换 raw mode
// 如果用 useEffect(异步),会导致按键丢失或闪烁
useLayoutEffect(() => {
// 同步设置终端 raw mode
}, [])
原因: 浏览器中 useEffect 在 DOM 更新后异步执行是安全的。但在终端中,raw mode(直接读取按键)必须在渲染同步完成前生效,否则用户的按键可能被终端默认行为消费。
差异 2:无 CSS,用 Yoga Flexbox
// 组件使用 Ink 的 <Box> 和 <Text> 替代 <div> 和 <span>
<Box flexDirection="column" padding={1}>
<Text bold color="green">成功</Text>
<Text>操作已完成</Text>
</Box>
布局由 Yoga(Flexbox 引擎)处理,不支持 CSS Grid、绝对定位等浏览器特有能力。
差异 3:无 DOM 事件,用按键流
浏览器中有 onClick、onKeyDown 等 DOM 事件。终端中没有 DOM,只有标准输入的按键字节流。ink/parse-keypress.ts 负责将字节流解析为结构化的按键事件。
差异 4:无像素,用字符单元
终端的最小渲染单位是字符单元(character cell),不是像素。一个中文字符占 2 个单元宽度,一个 emoji 可能占 2 个单元。ink/measure-text.ts 和 ink/line-width-cache.ts 处理这些宽度计算。
React Context 在 Claude Code 中的使用
| Context | 来源文件 | 提供的数据 |
|---|---|---|
| AppState | state/AppState.tsx | 全局状态(消息、工具、权限等) |
| Notifications | context/notifications.tsx | 通知队列 |
| Mailbox | context/mailbox.tsx | Agent 间邮箱通信 |
| Voice | context/voice.tsx | 语音输入状态 |
| Stats | context/stats.tsx | 性能统计 |
| FPS Metrics | context/fpsMetrics.tsx | 帧率指标 |
| Modal | context/modalContext.tsx | 模态框状态 |
| Overlay | context/overlayContext.tsx | 覆盖层状态 |
2.6 依赖关系总图
┌─────────────────────────────────────────────────────────┐
│ Claude Code 应用 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌───────────┐ ┌─────────────────┐ │
│ │ React │ │ Zod/v4 │ │ @anthropic-ai/ │ │
│ │Components│ │ Schemas │ │ sdk │ │
│ └────┬─────┘ └─────┬─────┘ └───────┬─────────┘ │
│ │ │ │ │
│ ┌────▼─────┐ ┌─────▼─────┐ ┌──────▼────────┐ │
│ │ Ink Fork │ │ Tool.ts │ │ services/api/ │ │
│ │ (50文件) │ │ Interface │ │ claude.ts │ │
│ └────┬─────┘ └───────────┘ └───────────────┘ │
│ │ │
│ ┌────▼──────────────────────┐ │
│ │ Yoga (Flexbox 引擎) │ │
│ └───────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────┤
│ Bun Runtime + Bundler │
│ feature() · DCE · require() · TypeScript 直接执行 │
└─────────────────────────────────────────────────────────┘
课后练习
练习 1:Feature 替换实验
在 query.ts 中找到 feature('REACTIVE_COMPACT') 的条件加载。思考:
- 如果这个 feature 在编译时为
true,reactiveCompact变量的类型是什么? - 如果为
false,类型又是什么? - 这对后续代码中使用
reactiveCompact?.someMethod()有什么影响?
练习 2:渲染管线追踪
在 ink/ 目录中,按以下顺序阅读文件的前 50 行:
ink.tsx(入口)reconciler.ts(Reconciler)renderer.ts(渲染器)render-node-to-output.ts(节点到输出)terminal.ts(终端控制)
画出数据在这 5 个文件间的流转方向。
练习 3:Zod 三合一
在 tools/FileReadTool/ 目录中找到 inputSchema 的定义。回答:
- 这个 Schema 包含哪些字段?哪些是必选,哪些是可选?
z.infer<typeof inputSchema>推导出的 TypeScript 类型是什么?- 导出为 JSON Schema 后,Claude API 看到的参数描述是什么样的?
练习 4:终端 vs 浏览器
列出至少 5 个"终端 React"和"浏览器 React"的差异,并为每个差异说明 Claude Code 如何解决。
本课小结
| 要点 | 内容 |
|---|---|
| Bun 核心价值 | feature() 编译时 Feature Gate + 死代码消除 |
| Ink 渲染管线 | React Reconciler → Yoga 布局 → 字符矩阵 → ANSI 输出 |
| Zod 三重角色 | 运行时校验 + TS 类型推导 + JSON Schema 导出 |
| SDK 用途 | API 通信:认证、请求构建、流式传输、类型安全 |
| 终端适配 | useLayoutEffect、Yoga Flexbox、按键流解析、字符单元宽度 |
下一课预告
第 03 课:Feature Gate 双层体系 — 深入 92 个编译时开关和 1100+ 个运行时 flag 的完整清单,理解 USER_TYPE === 'ant' 如何隔离内部功能,以及 GrowthBook 缓存策略的性能取舍。