Topic | N-API 原生模块集成模板¶
重要性:⭐⭐⭐⭐(学 N-API 集成的最佳范本) 出现位置:
src/native-ts/(3 模块)、vendor/(4 模块) 关联:phase-07-advanced.md 的 Native 桥接
1. N-API 是什么¶
Node-API(N-API):Node.js / Bun 提供的 C/C++ 原生模块 API。
为什么需要: - 某些能力纯 JS 做不到:剪贴板读取、键盘事件捕获、GPU 加速、文件索引 - 性能敏感场景:图像处理、压缩、加密 - 平台 API 调用:macOS AVFoundation、Windows Win32 API
2. Claude Code 的 7 个 N-API 模块¶
src/native-ts/ # 项目内 N-API 绑定
├── color-diff/ 颜色差异
│ └── index.ts
├── file-index/ 文件索引
│ └── index.ts
└── yoga-layout/ Yoga 布局
├── enums.ts
└── index.ts (2578 行)
vendor/ # 外部 N-API 绑定(4 模块)
├── audio-capture-src/ 音频捕获
│ └── index.ts
├── image-processor-src/ 图像处理
│ └── index.ts
├── modifiers-napi-src/ 键盘修饰键
│ └── index.ts
└── url-handler-src/ URL scheme handler
└── index.ts
总计 7 个 N-API 模块,TS 侧只写类型(type XxxNapi = { ... }),实际实现是 .node 二进制。
3. 模板一:audio-capture-src(最完整案例)¶
3.1 TypeScript 类型定义(20 行)¶
// vendor/audio-capture-src/index.ts
type AudioCaptureNapi = {
startRecording(
onData: (data: Buffer) => void,
onEnd: () => void,
): boolean
stopRecording(): void
isRecording(): boolean
startPlayback(sampleRate: number, channels: number): boolean
writePlaybackData(data: Buffer): void
stopPlayback(): void
isPlaying(): boolean
// TCC microphone authorization status (macOS only):
// 0 = notDetermined, 1 = restricted, 2 = denied, 3 = authorized.
// Linux: always returns 3 (authorized) — no system-level microphone permission API.
// Windows: returns 3 (authorized) if registry key absent or allowed,
// 2 (denied) if microphone access is explicitly denied.
microphoneAuthorizationStatus?(): number
}
3.2 注释揭示的"集成设计"¶
观察注释,揭示了 4 个关键设计:
| 设计点 | 注释原文 | 含义 |
|---|---|---|
| callback 风格 | onData: (data: Buffer) => void |
N-API 用回调推数据,不用 Promise |
| TCC 权限码 | 0=notDetermined, 1=restricted, 2=denied, 3=authorized |
macOS 系统权限枚举 |
| 跨平台 fallback | Linux: always returns 3, Windows: registry check |
每个平台有自己的处理 |
| 可选属性 | microphoneAuthorizationStatus?(): number |
平台无此 API 时字段缺失 |
3.3 实际调用(在 React 组件里)¶
// 推测的实际使用(src/voice/ 下某个组件)
import type { AudioCaptureNapi } from '../../vendor/audio-capture-src/index.js'
let napiModule: AudioCaptureNapi | null = null
// 懒加载 .node 二进制
try {
napiModule = require('@anthropic-cc/audio-capture') // 实际 .node 文件
} catch (err) {
console.warn('Audio capture not available:', err)
}
function useAudioCapture() {
const [recording, setRecording] = useState(false)
const start = useCallback(() => {
if (!napiModule) return
napiModule.startRecording(
(data) => { /* 实时音频数据 */ },
() => { setRecording(false) }
)
setRecording(true)
}, [])
const stop = useCallback(() => {
if (!napiModule) return
napiModule.stopRecording()
}, [])
const status = useMemo(() => {
return napiModule?.microphoneAuthorizationStatus?.() ?? 3
}, [])
return { recording, start, stop, status }
}
关键点:
- 懒加载 —— 启动时不调,启动后再 require
- 容错 —— 没装 .node 也不崩
- Tree-shake 友好 —— 通过 feature() 在不需要的构建里清除
4. 模板二:image-processor-src(可选 API 案例)¶
4.1 TypeScript 类型¶
// vendor/image-processor-src/index.ts
export type ClipboardImageResult = {
png: Buffer
originalWidth: number
originalHeight: number
width: number
height: number
}
// Clipboard functions are macOS-only and only present in darwin binaries;
// older/non-darwin binaries built before this addition won't export them.
// Typed as optional so callers can guard. These property names appear only
// in type-space here; all runtime property access lives in src/ behind
// feature() so they tree-shake out of builds that don't want them.
export type NativeModule = {
processImage: (input: Buffer) => Promise<ImageProcessor>
readClipboardImage?: (maxWidth: number, maxHeight: number) => ClipboardImageResult | null
hasClipboardImage?: () => boolean
}
4.2 "可选 API" 模式¶
观察类型里的 ?::
- readClipboardImage? —— 老版本没有
- hasClipboardImage? —— 同上
注释解释:
- 老 .node 二进制没这些方法
- 类型用 ?: 表示可能不存在
- 调用方需要运行时检查:
5. 模板三:modifiers-napi-src(小工具案例)¶
// vendor/modifiers-napi-src/index.ts
type ModifiersNapi = {
// 监听全局键盘修饰键状态
getModifierState(modifier: 'shift' | 'ctrl' | 'alt' | 'meta'): boolean
// 注册回调
onModifierChange(handler: (mods: ModifierState) => void): () => void
}
为什么需要:Ink 拿到的是单个 KeyboardEvent,但有时需要知道"是否按住了 Shift"(用于判断 chord)。
6. 模板四:url-handler-src(协议注册案例)¶
// vendor/url-handler-src/index.ts
type UrlHandlerNapi = {
// 注册 claude:// URL scheme
registerScheme(scheme: string): void
// 收到 URL 时回调
onUrlOpened(handler: (url: string) => void): () => void
// macOS: 让 Claude Code 成为默认 handler
setAsDefaultHandler(scheme: string): boolean
}
用途:
- 用户点 claude://open-session?xxx 链接
- 操作系统调起 Claude Code
- 自动跳到对应 session
7. 项目内 N-API 模板:yoga-layout¶
// src/native-ts/yoga-layout/index.ts (2578 行)
import Yoga from 'yoga-layout' // N-API 绑定
// 重新导出 + 加 TS 类型 + 包装便利函数
export * from 'yoga-layout'
export type YogaNode = Yoga.YogaNode
export const YGAlign = { /* 枚举包装 */ }
export const YGFlexDirection = { /* 枚举包装 */ }
// 高级 API(推测)
export function layoutInkNode(
node: InkNode,
availableWidth: number,
availableHeight: number
): LayoutResult {
// 1. 创建 Yoga 节点
const yogaNode = Yoga.Node.create()
// 2. 应用 props
applyProps(yogaNode, node.props)
// 3. 递归子节点
for (const child of node.children) {
layoutInkNode(child, ...)
}
// 4. 算最终位置
yogaNode.calculateLayout(availableWidth, availableHeight)
return {
x: yogaNode.getComputedLeft(),
y: yogaNode.getComputedTop(),
width: yogaNode.getComputedWidth(),
height: yogaNode.getComputedHeight(),
}
}
2578 行 = 完整 Yoga API 包装。
8. N-API 集成的"惯用法"¶
8.1 4 个共同模式¶
观察 7 个 N-API 模块的 TS 类型,共同模式:
- callback 异步 ——
onData、onEnd、onModifierChange等回调 - 可选 API —— 老二进制兼容用
?:可选属性 - 跨平台 fallback —— 注释里说清每个平台行为
- Tree-shake 友好 —— 通过
feature()控制
8.2 调用方 3 步¶
// Step 1:声明类型
import type { AudioCaptureNapi } from './vendor/audio-capture-src/index.js'
// Step 2:懒加载 + 容错
let napi: AudioCaptureNapi | null = null
try {
napi = require('@anthropic-cc/audio-capture') // .node 文件
} catch {}
// Step 3:调用 + 可选检查
if (napi?.microphoneAuthorizationStatus) {
const status = napi.microphoneAuthorizationStatus()
}
8.3 Bun 怎么 require .node¶
Bun 运行时自动支持 N-API:
- .node 文件在 node_modules 里
- require('xxx') 解析为原生模块
- 不需要 node-gyp / build(编译好的 .node 直接用)
9. 实战:写一个"屏幕截图" N-API 模块¶
模仿 audio-capture-src 的模式:
// vendor/screen-capture-src/index.ts
export type ScreenCaptureResult = {
png: Buffer
width: number
height: number
}
export type ScreenCaptureNapi = {
captureScreen(displayId?: number): ScreenCaptureResult | null
captureWindow(windowId: number): ScreenCaptureResult | null
// macOS: 需要 Screen Recording 权限(TCC code 0-3)
// Windows: 需要 admin 或 UAC 同意
// Linux: 需要 xwd / grim 等外部工具
hasScreenPermission?(): boolean
getDisplays(): Array<{ id: number, width: number, height: number }>
}
注释:
// TCC screen recording permission (macOS only):
// 0 = notDetermined, 1 = restricted, 2 = denied, 3 = authorized.
// Linux: uses xwd (X11) or grim (Wayland) — always returns true if binary present.
// Windows: uses BitBlt API — no system-level permission required but
// some apps may have anti-screenshot protection.
10. 关键洞察¶
10.1 "TS 只写类型" 的好处¶
- 类型即文档
- 编译时类型安全
- 实现可以替换(不同平台不同二进制)
- 测试 mock 容易(用
vi.mock替换)
10.2 "Tree-shake 友好" 的工程意义¶
- 外部构建不包含 TCC 权限检查代码
- bundle size 不膨胀
- 跨平台构建只包含对应平台代码
10.3 "可选 API" 处理向后兼容¶
- 类型
?:+ 运行时if (xxx)检查 - 不用版本号、不用 feature detection
- JS 的 duck typing 哲学
10.4 N-API 是"性能逃生舱"¶
- 大多数 JS 代码够用
- 性能 / 平台能力瓶颈时,N-API 是唯一选项
- Claude Code 用了 7 个 = 知道哪些是真瓶颈
11. 阅读清单¶
- ✅
vendor/audio-capture-src/index.ts(最完整案例) - ✅
vendor/image-processor-src/index.ts(可选 API 模式) - ✅
src/native-ts/yoga-layout/index.ts:1-80(项目内 N-API) - ✅
src/native-ts/file-index/(文件索引 N-API) - ✅
src/native-ts/color-diff/(颜色差异 N-API) - 📌
vendor/modifiers-napi-src/index.ts+url-handler-src/index.ts
12. 练习任务¶
- 设计一个屏幕截图 N-API 模块的类型定义 —— 模仿 audio-capture-src 的格式
- 实现一个 mock —— 用普通 JS 模拟 N-API(用 setTimeout 模拟回调)
- 写懒加载包装 —— 容错的
try { require } catch {}模式 - 思考:为什么 Claude Code 选择 N-API 而不是 Wasm?N-API vs Wasm 的 trade-off 是什么?