模块一:基础认知 | 前置依赖:第 01 课 | 预计学习时间:60 分钟
学习目标
完成本课后,你将能够:
1. 解释 Claude Code 为何选择 Bun 而非 Node.js,以及 feature()编译时替换的工作原理2. 描述 Ink 终端 React 渲染器的三阶段渲染管线 3. 说明 Zod 如何同时服务于运行时校验和 TypeScript 类型推导 4. 识别终端环境中 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 的其他角色
feature() | |
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 — 创建自定义 Reconcilerimport { 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 | |
reconciler.ts | |
renderer.ts | |
render-node-to-output.ts | |
render-to-screen.ts | |
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 EventsSDK 负责:认证(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 中的使用
state/AppState.tsx | ||
context/notifications.tsx | ||
context/mailbox.tsx | ||
context/voice.tsx | ||
context/stats.tsx | ||
context/fpsMetrics.tsx | ||
context/modalContext.tsx | ||
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 行:
1. ink.tsx(入口)2. reconciler.ts(Reconciler)3. renderer.ts(渲染器)4. render-node-to-output.ts(节点到输出)5. 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 如何解决。
本课小结
feature() | |
下一课预告
第 03 课:Feature Gate 双层体系 — 深入 92 个编译时开关和 1100+ 个运行时 flag 的完整清单,理解 USER_TYPE === 'ant' 如何隔离内部功能,以及 GrowthBook 缓存策略的性能取舍。
夜雨聆风