第 02 课:技术栈深度解析

4 阅读7分钟

模块一:基础认知 | 前置依赖:第 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 打包器的内置函数。它在打包时(而非运行时)被求值为 truefalse,然后 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 Gatefeature() 实现编译时功能开关
包管理器替代 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.tsReact 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 事件,用按键流

浏览器中有 onClickonKeyDown 等 DOM 事件。终端中没有 DOM,只有标准输入的按键字节流。ink/parse-keypress.ts 负责将字节流解析为结构化的按键事件。

差异 4:无像素,用字符单元

终端的最小渲染单位是字符单元(character cell),不是像素。一个中文字符占 2 个单元宽度,一个 emoji 可能占 2 个单元。ink/measure-text.tsink/line-width-cache.ts 处理这些宽度计算。

React Context 在 Claude Code 中的使用

Context来源文件提供的数据
AppStatestate/AppState.tsx全局状态(消息、工具、权限等)
Notificationscontext/notifications.tsx通知队列
Mailboxcontext/mailbox.tsxAgent 间邮箱通信
Voicecontext/voice.tsx语音输入状态
Statscontext/stats.tsx性能统计
FPS Metricscontext/fpsMetrics.tsx帧率指标
Modalcontext/modalContext.tsx模态框状态
Overlaycontext/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 在编译时为 truereactiveCompact 变量的类型是什么?
  • 如果为 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 如何解决。


本课小结

要点内容
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 缓存策略的性能取舍。