Bruno 的 React 代码审查清单:`bruno-app` 前端评审规则与主题令牌机制解析
这篇文章以 Bruno 仓库内置 AI 代码审查技能中的 React 评审者文件 react.md 为主体,完整拆解它定义的审查范围、blocker/suggestion 两级违规清单,以及背后真实生效的主题令牌(theme token)校验机制。读完后,你可以把同一套规则迁移到自己项目的前端评审流程中,并理解为什么"主题令牌不一致"在 Bruno 中会被直接判定为阻断级缺陷。
一、评审范围与输出契约
react.md 是 code-review 技能 下的一个"评审者人格"文件,其开篇用一句话锁定职责边界:
- Scope(审查范围):
packages/bruno-app/**—— 即 Bruno 桌面客户端(React 前端)的全部代码。Bruno 是一个 monorepo,packages/bruno-app/存放 Electron 渲染进程侧的 UI,其余包(CLI、请求引擎、文件存储等)不在此评审者管辖之内。
评审者需要同时遵循两份上游文档,形成"通用契约 + 专项清单"的结构:
- _contract.md 定义所有评审者共享的人格与输出契约:
- 人格要求:面向 TypeScript / JavaScript / Node.js / Electron 的企业级资深评审者,每条发现只写一句清晰的话,不因人而软化严重度;
- 输出格式为扁平列表,一行一条发现:
<blocker|suggestion|nit> | <file>:<line> | <one-sentence finding>; - 范围干净时只返回
no findings,禁止为了填满列表而编造 nit; - 每条发现必须落在真实代码上——"仓库与文档不一致时以仓库为准,且不得引用未验证过的行号或虚构示例值"。
- CODING_STANDARDS.md 的 §React 小节,是组件代码评审的基线标准(见下节)。
二、基线标准:CODING_STANDARDS.md §React 中的硬性约定
react.md 明确要求"对照 CODING_STANDARDS.md §React 审查被修改的组件(必须通读该文件)"。该小节位于 CODING_STANDARDS.md 的 "UI Specific instructions → React" 部分,核心条目与评审规则一一对应:
| 约定 | 级别 | 说明 |
|---|---|---|
颜色一律走 styled-components 的 theme prop,而不是 CSS 变量 |
MUST | 在 styled component 或任何使用 styled-components 的组件上下文中生效 |
| styled-components 负责自身与子组件样式(也可改布局),Tailwind 只用于布局类样式,不得定义颜色 | 规则 | 两套体系的分工边界 |
| 业务逻辑、数据获取、副作用优先封装为自定义 Hook | MUST | — |
非必要不使用 useEffect,优先派生状态(derived state)与事件处理器 |
MUST | 这是 React 反模式的第一来源 |
useMemo/useCallback 只在确有需要时记忆化,且优先把逻辑移入 Hook |
SHOULD | 防止"无谓 memo" |
禁止命名空间式访问 Hook(React.useCallback(...)),必须直接具名导入 |
MUST | 正确写法:import { useCallback, useMemo, useState } from "react"; |
可测试元素加 data-testid(供 Playwright 使用) |
MUST | 对应仓库中 tests/**/*.spec.ts 的 E2E 测试体系 |
| 工具函数就近共置:组件专属的放组件旁,共享的放公共目录 | 规则 | 对应 bruno-app-layout 的放置规则 |
| 避免受控/非受控状态混用;状态需要单一事实来源,禁止"先用 props 算一遍、组件内再算一遍" | MUST | — |
能用派生状态变量就不要新增 useState |
SHOULD | — |
评审清单中几乎每一条 blocker 都能在这个表里找到出处——这正是"以编码标准为基线、以专项清单为执行细则"的设计意图。
三、blocker 级清单一:React 正确性缺陷
react.md 把下列情形全部定为 blocker(阻断级),要求报告 file:line:
- 本可用派生状态/事件处理器/自定义 Hook 解决的
useEffect。与标准中"AvoiduseEffectunless absolutely needed"直接呼应:副作用里做状态同步是典型的冗余状态,会引入不一致窗口。 - 硬编码颜色:hex / rgb / hsl / 命名色(如
#fff、rgb(0,0,0)、lightblue)替代了 styled-components 的themeprop。清单特别要求评审者"验证该令牌路径确实存在于 theme 对象上"——即不能为了消掉一个硬编码值而引用一个不存在的theme.xxx.yyy。 - 命名空间式 Hook 导入(
React.useX):违反上表中的导入约定。 - 受控与非受控状态混用的组件:状态没有单一事实来源。
- 条件早退之后调用 Hook:违反 Hooks 规则,直接破坏 Hook 调用顺序稳定性。
其中第 2 条之所以在 Bruno 中格外严重,是因为它牵连下一节的主题体系:一个硬编码颜色会"破坏其余 12 个主题"。
四、blocker 级清单二:目录布局(bruno-app-layout)
react.md 第二条 blocker 是"任何违反 bruno-app-layout.md 的改动,包含'组件专属 vs 共享'的放置问题"。该规则文件(frontmatter 限定作用于 packages/bruno-app/**)把布局视为"承载性约定"(load-bearing),放错文件等同于架构缺陷,与依赖方向违规同级、同样阻断。四条核心规则:
- 组件拥有自己的目录:
<ComponentName>/index.js(x),绝不写成<ComponentName>.js——任何嵌套深度都如此,包括组件的子组件;Hook(src/hooks/useX/index.js)与src/ui/基元同样适用。 - 组件拥有的一切放在其目录内:
StyledWrapper.js、spec、组件专属工具。向上"借用"父组件的StyledWrapper.js,或把样式留在组件目录外,等于"没人拥有它们"。 src/ui/是叶子层:只放与 app 无关的展示型基元,且不得反向导入src/components/——依赖边只能components → ui。utils/只放"被调用的无状态辅助函数":被"注册"到某处的模块(编辑器扩展、插件、node view、keymap)属于行为或配置,归入utils/会掩盖其"是某个 schema 或文档契约的一部分"的事实,应单独立目录。
规则文件末尾还列出了两条既成偏差(bare PascalCase.js 组件文件、ui/MenuDropdown 导入 components/Dropdown)——评审规则特意注明:"文件末尾记录的偏差不是发现项"。这是清单设计中很关键的一笔:只对变更负责,不考古存量。
五、blocker 级清单三:主题令牌与 schema 的一致性
这是 react.md 中最有"Bruno 特色"的一条 blocker,它描述的是一条真实存在的运行时校验链:
在
themes/light|dark/*.js中定义了新的主题令牌,却没有在themes/schema/oss.js中补充对应属性;或者令牌已加入 schema 却在 13 个主题文件中的某些文件里缺失。由于 schema 是additionalProperties: false,且providers/Theme/index.js会在运行时按它做校验,任一侧缺漏都会使整个主题失效,用户被静默回退到默认主题并收到一个错误 toast。
结合仓库源码可以逐环验证这条链路:
- 13 个主题文件确实存在:
packages/bruno-app/src/themes/light/下有 5 个(light.js、vscode.js、catppuccin-latte.js、light-pastel.js、light-monochrome.js),packages/bruno-app/src/themes/dark/下有 8 个(dark.js、vscode.js、nord.js、三个catppuccin-*.js、dark-pastel.js、dark-monochrome.js),合计 13 个。这就是"破坏其余 12 个主题"说法的来源。 - schema 确实是
additionalProperties: false:oss.js 全文出现 120 余处additionalProperties: false,从顶层分组(primary、accents、background、status等)一路锁死到叶子对象,并配合required数组强制必填。例如primary组要求solid/text/strong/subtle四键齐全且禁止多余键。 - 运行时校验与回退:providers/Theme/index.js 在文件顶部导入
jsonschema的Validator与react-hot-toast的toast,并在计算主题对象时先做存在性检查——选中变体缺失或无效时回退到themes.light/themes.dark,并对非默认变体弹出 toast 提示。评审清单中"整主题失效、静默回退默认 + 错误 toast"的后果描述与此实现一致。
从这条机制可以推断出评审规则背后的产品考量:主题令牌是跨 13 套配色的一致性契约,"少一个属性"不会抛异常崩溃,而是让整个主题静默失效——这类缺陷极难被肉眼发现,因此只能靠评审期阻断。
六、suggestion 级清单:性能、边界与体验细节
react.md 的 suggestion(建议级)条目数量更多,覆盖五类:
- 记忆化失当:缺失的 memo 导致依赖数组失效或重型子组件重复渲染;以及反向的"给廉价原语无谓套 memo"。对应标准中"SHOULD: Memoize only when necessary"。
- Tailwind 越界:用 Tailwind 定义颜色(仅允许用于布局)。
- 可测试元素缺
data-testid:与仓库根下tests/目录中大量 Playwright spec(要求"用 role / label / test id 等稳定选择器")形成配套。 - 令牌替换不保真:主题令牌替换后解析出的值与被替换的字面量不同;或同组值中有的转了主题化、有的仍硬编码(要求先到
themes/里把值定下来再替换)。 - 全局监听未加门槛:
document/window监听器没有以"使其相关的状态"为前置条件(gate)——即使当前某个偶发细节让它恰好无害。 - EditableTable 列的门控缺失:新增列没有像同列表那样带上
readOnly/editMode门控(渲染处形如readOnly={column.readOnly})——结果是字段在 view / read-only 模式下依然可编辑,而变更处理器又会丢弃这次编辑,形成"看起来能改、实际白改"的矛盾状态。 - 乐观成功态无条件置位:
copied/saved/done之类的成功状态不看操作是否真正完成就置为true。文档给出的反例极具代表性:navigator.clipboard?.writeText后直接copied = true——在不可信(insecure)上下文中可选链调用会静默 no-op,用户却看到虚假的"已复制"确认。
这些建议级条目共同的特点是:不阻断合并,但会在特定环境/状态组合下制造真实的用户困惑,因此规则要求评审者"即使今天恰好无害"也要报告监听器未加门槛一类的问题。
七、涉及 Store 的改动:叠加 redux-store 规则
react.md 中还有前置条件:"对被改动的组件,按 CODING_STANDARDS.md §React 审查;若改动触及 store,还需参阅 redux-store.md"。该规则文件补充了 bruno-app 的 Redux 侧约束,评审时与 React 清单叠加生效,要点包括:
- Store 基于 Redux Toolkit v1.8(而非 v2),
configureStore位于src/providers/ReduxStore/index.js,其reducermap 是唯一的 slice 权威清单;中间件为getDefaultMiddleware().concat([...])且不带选项,因此serializableCheck/immutableCheck在开发期全部生效——状态必须保持可序列化。 - Reducer 直接改写 Immer draft(
state.collections.push(...)、item.name = ...),不应以 spread 克隆/返回新对象的方式"纠正"它。 - slice 组织上的两个坑:reducer key 不等于文件夹名(如
globalEnvironments来自slices/global-environments.js);action type 前缀跟随 slice 的name:而非 reducer key,追踪 dispatch 时应 grep action type。
八、如何把这套清单落地为自己的评审流程
从 react.md 的结构可以提炼出一条可复用的设计模式:
- 范围先行:每个评审者文件第一行声明
Scope(glob 路径),使多评审者并行时互不越界; - 基线与细则分层:把长期稳定的编码标准(CODING_STANDARDS.md)与具体人格的执行清单分离,基线改动时所有评审者自动跟进;
- 严重度与后果绑定:blocker 的判定标准不是"风格不好看",而是"会造成运行时失效/状态不一致/架构漂移",每条都写清后果(如主题静默回退);
- 对存量偏差显式豁免:写明哪些历史问题是"drift, not a second pattern",只对新变更追责;
- 输出契约机器可读:
<severity> | <file>:<line> | <一句话>的扁平格式便于下游工具聚合、去重与统计。
适用前提与限制:这套规则深度绑定 Bruno 的具体实现——13 个主题文件、additionalProperties: false 的 schema、src/ui 叶子层约定——迁移时须替换为对应仓库的真实路径与约定;同时它面向"变更审查"(review changed components),不适合作为全量代码审计清单使用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0630
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00