构建和运行
在提交任何更改之前,通过运行完整的预检查来验证它们是至关重要的。此命令将构建仓库、运行所有测试、检查类型错误并对代码进行 lint 检查。
要运行完整的检查套件,请执行以下命令:
npm run preflight
这个单一命令确保您的更改满足项目的所有质量门槛。虽然您可以分别运行各个步骤(build
、test
、typecheck
、lint
),但强烈建议使用 npm run preflight
来确保全面验证。
编写测试
此项目使用 Vitest 作为其主要测试框架。在编写测试时,请努力遵循现有模式。主要约定包括:
测试结构和框架
- 框架:所有测试都使用 Vitest 编写(
describe
、it
、expect
、vi
)。 - 文件位置:测试文件(逻辑用
*.test.ts
,React 组件用*.test.tsx
)与它们测试的源文件位于同一位置。 - 配置:测试环境在
vitest.config.ts
文件中定义。 - 设置/清理:使用
beforeEach
和afterEach
。通常,在beforeEach
中调用vi.resetAllMocks()
,在afterEach
中调用vi.restoreAllMocks()
。
模拟(Vitest 中的 vi
)
- ES 模块:使用
vi.mock('module-name', async (importOriginal) => { ... })
进行模拟。使用importOriginal
进行选择性模拟。- 示例:
vi.mock('os', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, homedir: vi.fn() }; });
- 示例:
- 模拟顺序:对于影响模块级常量的关键依赖(如
os
、fs
),将vi.mock
放在测试文件的_最顶部_,在其他导入之前。 - 提升:如果模拟函数需要在
vi.mock
工厂中使用之前定义,请使用const myMock = vi.hoisted(() => vi.fn());
。 - 模拟函数:使用
vi.fn()
创建。使用mockImplementation()
、mockResolvedValue()
或mockRejectedValue()
定义行为。 - 监视:使用
vi.spyOn(object, 'methodName')
。在afterEach
中使用mockRestore()
恢复监视。
常见模拟模块
- Node.js 内置模块:
fs
、fs/promises
、os
(特别是os.homedir()
)、path
、child_process
(execSync
、spawn
)。 - 外部 SDK:
@google/genai
、@modelcontextprotocol/sdk
。 - 内部项目模块:来自其他项目包的依赖通常被模拟。
React 组件测试(CLI UI - Ink)
- 使用
ink-testing-library
中的render()
。 - 使用
lastFrame()
断言输出。 - 在必要的
Context.Provider
中包装组件。 - 使用
vi.mock()
模拟自定义 React hooks 和复杂的子组件。
异步测试
- 使用
async/await
。 - 对于定时器,使用
vi.useFakeTimers()
、vi.advanceTimersByTimeAsync()
、vi.runAllTimersAsync()
。 - 使用
await expect(promise).rejects.toThrow(...)
测试 Promise 拒绝。
一般指导
- 添加测试时,首先检查现有测试以理解并符合既定约定。
- 密切关注现有测试文件顶部的模拟;它们揭示了关键依赖以及如何在测试环境中管理它们。
Git 仓库
此项目的主分支称为 “main”
JavaScript/TypeScript
在为这个 React、Node 和 TypeScript 代码库做贡献时,请优先使用带有相应 TypeScript 接口或类型声明的普通 JavaScript 对象,而不是 JavaScript 类语法。这种方法提供了显著的优势,特别是在与 React 的互操作性和整体代码可维护性方面。
优先使用普通对象而非类
JavaScript 类本质上是为了封装内部状态和行为而设计的。虽然这在某些面向对象的范式中很有用,但在与 React 基于组件的架构工作时,它经常引入不必要的复杂性和摩擦。以下是优先使用普通对象的原因:
-
无缝 React 集成:React 组件在明确的 props 和状态管理上蓬勃发展。类倾向于在实例内部直接存储内部状态,这可能使 prop 和状态传播更难推理和维护。另一方面,普通对象本质上是不可变的(当深思熟虑地使用时),可以轻松地作为 props 传递,简化数据流并减少意外的副作用。
-
减少样板代码并提高简洁性:类通常促进使用构造函数、this 绑定、getter、setter 和其他可能不必要地膨胀代码的样板代码。TypeScript 接口和类型声明提供了强大的静态类型检查,而无需运行时开销或类定义的冗长。这允许更简洁和可读的代码,与 JavaScript 在函数式编程方面的优势保持一致。
-
增强的可读性和可预测性:普通对象,特别是当其结构由 TypeScript 接口明确定义时,通常更易于阅读和理解。它们的属性可以直接访问,没有隐藏的内部状态或复杂的继承链需要导航。这种可预测性导致更少的错误和更可维护的代码库。
-
简化的不可变性:虽然不是严格强制的,但普通对象鼓励对数据采取不可变的方法。当您需要修改对象时,您通常会创建一个具有所需更改的新对象,而不是改变原始对象。这种模式与 React 的协调过程完美契合,有助于防止与共享可变状态相关的微妙错误。
-
更好的序列化和反序列化:普通 JavaScript 对象天然易于序列化为 JSON 并反序列化回来,这在 Web 开发中是一个常见要求(例如,用于 API 通信或本地存储)。带有方法和原型的类可能会使这个过程复杂化。
采用 ES 模块语法进行封装
我们强烈建议利用 ES 模块语法(import
/export
)来封装私有和公共 API,而不是依赖类似 Java 的私有或公共类成员,这些成员可能冗长且有时限制灵活性。
-
更清晰的公共 API 定义:使用 ES 模块,任何导出的内容都是该模块公共 API 的一部分,而任何未导出的内容本质上对该模块是私有的。这提供了一种非常清晰和明确的方式来定义代码的哪些部分是供其他模块使用的。
-
增强的可测试性(不暴露内部结构):默认情况下,未导出的函数或变量无法从模块外部访问。这鼓励您测试模块的公共 API,而不是其内部实现细节。如果您发现自己需要为了测试目的而监视或存根未导出的函数,这通常是一个"代码异味",表明该函数可能是一个很好的候选者,可以提取到具有定义良好的公共 API 的独立、可测试模块中。这促进了更强大和可维护的测试策略。
-
减少耦合:通过导入/导出明确定义的模块边界有助于减少代码库不同部分之间的耦合。这使得重构、调试和单独理解各个组件变得更容易。
避免 any
类型和类型断言;优先使用 unknown
TypeScript 的强大之处在于其提供静态类型检查的能力,在代码运行之前捕获潜在错误。为了充分利用这一点,避免使用 any
类型并谨慎使用类型断言是至关重要的。
-
any
的危险:使用 any 实际上是选择退出 TypeScript 对该特定变量或表达式的类型检查。虽然这在短期内可能看起来方便,但它引入了重大风险:- 类型安全性丢失:您失去了类型检查的所有好处,使得容易引入 TypeScript 本来可以捕获的运行时错误。
- 降低的可读性和可维护性:带有
any
类型的代码更难理解和维护,因为数据的预期类型不再明确定义。 - 掩盖根本问题:通常,需要 any 表明您的代码设计或与外部库交互的方式存在更深层次的问题。这是一个信号,表明您可能需要完善您的类型或重构您的代码。
-
优先使用
unknown
而非any
:当您绝对无法在编译时确定值的类型,并且您想要使用 any 时,请考虑使用 unknown 代替。unknown 是 any 的类型安全对应物。虽然 unknown 类型的变量可以保存任何值,但您必须执行类型收窄(例如,使用 typeof 或 instanceof 检查,或类型断言)然后才能对其执行任何操作。这迫使您明确处理未知类型,防止意外的运行时错误。function processValue(value: unknown) { if (typeof value === 'string') { // value 现在安全地是字符串 console.log(value.toUpperCase()); } else if (typeof value === 'number') { // value 现在安全地是数字 console.log(value * 2); } // 没有收窄,您不能访问 'value' 的属性或方法 // console.log(value.someProperty); // 错误:对象是 'unknown' 类型。 }
-
类型断言(
as Type
)- 谨慎使用:类型断言告诉 TypeScript 编译器,"相信我,我知道我在做什么;这绝对是这种类型。"虽然有合法的用例(例如,当处理没有完美类型定义的外部库时,或者当您拥有比编译器更多的信息时),但应该谨慎使用。- 绕过类型检查:像
any
一样,类型断言绕过了 TypeScript 的安全检查。如果您的断言不正确,您引入了一个 TypeScript 不会警告您的运行时错误。 - 测试中的代码异味:
any
或类型断言可能诱人的常见场景是在尝试测试"私有"实现细节时(例如,监视或存根模块内的未导出函数)。这强烈表明您的测试策略和潜在的代码结构存在"代码异味"。不要试图强制访问私有内部结构,而是考虑这些内部细节是否应该重构为具有定义良好的公共 API 的独立模块。这使它们本质上可测试,而不会妥协封装。
- 绕过类型检查:像
拥抱 JavaScript 的数组操作符
为了进一步提高代码清洁度并促进安全的函数式编程实践,尽可能多地利用 JavaScript 丰富的数组操作符集合。像 .map()
、.filter()
、.reduce()
、.slice()
、.sort()
等方法对于以不可变和声明式的方式转换和操作数据集合是非常强大的。
使用这些操作符:
- 促进不可变性:大多数数组操作符返回新数组,保持原始数组不变。这种函数式方法有助于防止意外的副作用,并使您的代码更可预测。
- 提高可读性:链式数组操作符通常比传统的 for 循环或命令式逻辑产生更简洁和表达性的代码。操作的意图一目了然。
- 促进函数式编程:这些操作符是函数式编程的基石,鼓励创建纯函数,这些函数接受输入并产生输出而不引起副作用。这种范式对于编写与 React 良好配合的强大和可测试代码非常有益。
通过一致地应用这些原则,我们可以维护一个不仅高效和高性能,而且现在和将来都是一种工作乐趣的代码库。
React(调整自 react-mcp-server)
角色
您是一个 React 助手,帮助用户编写更高效和可优化的 React 代码。您专门识别使 React 编译器能够自动应用优化的模式,减少不必要的重新渲染并提高应用程序性能。
在您产生和建议的所有代码中遵循这些准则
使用带有 Hooks 的函数组件:不要生成类组件或使用旧的生命周期方法。使用 useState 或 useReducer 管理状态,使用 useEffect(或相关 Hooks)管理副作用。对于任何新的组件逻辑,始终优先使用函数和 Hooks。
在渲染期间保持组件纯净且无副作用:不要产生在组件函数体内直接执行副作用(如订阅、网络请求或修改外部变量)的代码。此类操作应该包装在 useEffect 中或在事件处理程序中执行。确保您的渲染逻辑是 props 和状态的纯函数。
尊重单向数据流:通过 props 向下传递数据,避免任何全局突变。如果两个组件需要共享数据,将该状态提升到共同的父组件或使用 React Context,而不是试图同步本地状态或使用外部变量。
永远不要直接改变状态:始终生成不可变地更新状态的代码。例如,在更新状态时使用展开语法或其他方法创建新的对象/数组。不要在状态变量上使用像 state.someValue = … 或数组突变如 array.push() 这样的赋值。使用状态设置器(来自 useState 的 setState 等)来更新状态。
准确使用 useEffect 和其他效果 Hooks:每当您认为可以使用 useEffect 时,请更深入地思考和推理以避免它。useEffect 主要仅用于同步,例如将 React 与某些外部状态同步。重要 - 不要在 useEffect 中 setState(useState 返回的第二个值),因为这会降低性能。编写效果时,在依赖数组中包含所有必要的依赖项。不要抑制 ESLint 规则或省略效果代码使用的依赖项。构造效果回调以正确处理更改的值(例如,在 prop 更改时更新订阅,在卸载或依赖项更改时清理)。如果一段逻辑应该仅响应用户操作(如表单提交或按钮点击)而运行,请将该逻辑放在事件处理程序中,而不是在 useEffect 中。在可能的情况下,useEffects 应该返回一个清理函数。
遵循 Hooks 规则:确保任何 Hooks(useState、useEffect、useContext、自定义 Hooks 等)在 React 函数组件或其他 Hooks 的顶层无条件地调用。不要生成在循环、条件语句或嵌套辅助函数内部调用 Hooks 的代码。不要在非组件函数或 React 组件渲染上下文之外调用 Hooks。
仅在必要时使用 refs:除非任务真正需要(如聚焦控件、管理动画或与非 React 库集成),否则避免使用 useRef。不要使用 refs 来存储应该是响应式的应用程序状态。如果您确实使用 refs,永远不要在组件渲染期间写入或读取 ref.current(除了像惰性初始化这样的初始设置)。任何 ref 使用都不应该直接影响渲染输出。
优先使用组合和小组件:将 UI 分解为小的、可重用的组件,而不是编写大的单体组件。您生成的代码应该通过组合组件来促进清晰和可重用性。同样,在适当的时候将重复的逻辑抽象为自定义 Hooks,以避免重复代码。
为并发优化:假设 React 可能出于调度目的多次渲染您的组件(特别是在严格模式下的开发中)。编写即使组件函数运行多次也保持正确的代码。例如,避免在组件体中产生副作用,在基于先前状态更新状态时使用函数状态更新(例如,setCount(c => c + 1))以防止竞态条件。始终在订阅外部资源的效果中包含清理函数。不要为"当这个改变时做这个"的副作用编写 useEffects。这确保您生成的代码将与 React 的并发渲染功能无问题地工作。
优化以减少网络瀑布 - 尽可能使用并行数据获取(例如,同时开始多个请求而不是一个接一个)。利用 Suspense 进行数据加载,并保持请求与需要数据的组件位于同一位置。在以服务器为中心的方法中,在服务器端一起获取相关数据(例如,使用服务器组件)以减少往返。另外,考虑使用缓存层或全局获取管理来避免重复相同的请求。
依赖 React 编译器 - 如果启用了 React 编译器,可以省略 useMemo、useCallback 和 React.memo。避免使用手动记忆化进行过早优化。相反,专注于编写具有直接数据流和无副作用渲染函数的清晰、简单组件。让 React 编译器处理树摇、内联和其他性能增强,以保持您的代码库更简单和更可维护。
为良好的用户体验而设计 - 提供清晰、最小和非阻塞的 UI 状态。当数据加载时,显示轻量级占位符(例如,骨架屏幕)而不是到处都是侵入性的加载器。使用专用的错误边界或友好的内联消息优雅地处理错误。在可能的情况下,在数据可用时渲染部分数据,而不是让用户等待所有内容。Suspense 允许您以自然的方式在组件树中声明加载状态,防止"闪烁"状态并提高感知性能。
流程
-
分析用户代码的优化机会:
- 检查阻止编译器优化的 React 反模式
- 寻找限制编译器效率的组件结构问题
- 思考您正在提出的每个建议,并参考 React 文档了解最佳实践
-
提供可行的指导:
- 用清晰的推理解释具体的代码更改
- 在建议更改时显示前后示例
- 只建议有意义地提高优化潜力的更改
优化指南
- 状态更新应该结构化以启用粒度更新
- 副作用应该被隔离,依赖项应该清楚地定义
注释政策
仅在必要时编写高价值注释。避免通过注释与用户对话。