Onyx(Danswer)Web 前端工程规范全解:Opal 设计系统、组件分层、i18n 与测试实践
【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer
Onyx(前身 Danswer)是一个开源的 Gen-AI 与企业搜索平台,其 Web 前端基于 Next.js 16、React 19 与 TypeScript 构建。本文以仓库 web/AGENTS.md 为骨架,系统讲解 Onyx 前端团队沉淀的工程规范:组件从哪来、如何选型、为什么禁用dark:修饰符与内置 Tailwind 色、next-intl 国际化如何约束硬编码字符串,以及 Jest 组件测试与 Playwright E2E 测试的硬性规则。读完本文,你将掌握一套可复制的企业级 React 前端开发约定,并能直接在 Onyx 仓库中对照源码验证每一条规则。
一、规范文档的定位与前端仓库结构
web/AGENTS.md是 Onyx 前端(web/与desktop/,后者是 Tauri 壳)的"宪法"式标准文件。它在仓库根目录的 AGENTS.md 中被显式引用:根文档将仓库拆分为backend/(FastAPI + Celery)、web/(Next.js 前端)、mobile/(React Native + Expo)三个子项目,并说明各子项目必须先读自己的 AGENTS.md 再动手。
与web/直接相关的前端目录结构如下:
- web/lib/opal/:
@opal/*设计系统包(组件、布局、图标、核心原语),是组件的第一来源; - web/lib/shared/:
@onyx-ai/shared跨平台共享包,是设计令牌(tokens)的唯一真源; - web/src/refresh-components/:尚未沉淀进 Opal 的生产组件;
- web/src/sections/ 与 web/src/layouts/:业务特性组合与页面布局;
- web/src/components/:遗留目录,正在被删除,规范明令禁止再从这里导入。
规范还强调了一个工程约定:每一个 Opal 组件与布局旁边都有一份README.md,使用前先读 README,而不是去猜 props。这与 web/lib/opal/src/components/README.md 中"新增组件必须附带架构、props 与用法示例文档"的要求相互印证,形成"组件即文档"的文化。
二、组件来源优先级:从设计系统到业务组合
web/AGENTS.md给出了明确的组件来源优先级链,这是整个前端规范的地基:
web/lib/opal/src/(@opal/*):设计系统,第一选择;web/src/refresh-components/:尚未进入 Opal 的生产组件;web/src/sections/(特性组合,实体卡片在sections/cards/)与web/src/layouts/。
唯一的例外:严禁从web/src/components/导入任何东西,除了一处——web/src/components/icons/icons.tsx 中的createLogoIcon。也就是说,遗留组件库只剩这一个函数有"免死金牌"。
具体 UI 场景的选型表
规范针对常见 UI 场景给出了近乎"点菜式"的选型:
| 场景 | 组件 | 来源 |
|---|---|---|
| 管理页/设置页框架 | SettingsLayouts.{Root,Header,Body} | @opal/layouts |
| 图标+标题+描述(含空状态、错误页) | Content/ContentAction/IllustrationContent | @opal/layouts |
| 按钮 | Button,禁止裸<button> | @opal/components |
| 输入框 | Opal 或 refresh-components,禁止裸<input>、<textarea>、<select> | — |
| 文本 | Text(配合font、colorprops),禁止裸露文本节点;refresh-components/texts/Text的布尔 flag API 已废弃 | @opal/components |
| 图标 | 仅@opal/icons,禁止lucide-react、react-icons | — |
| 悬停显示 | Hoverable;必须手写时补no-hover:opacity-100以兼容触屏 | @opal/core |
| 交互原语 | Interactive、Disabled仅用于构建组件,应用代码不得直接使用 | @opal/core |
图标缺失时的标准流程是:用 Figma MCP 工具从 Figma 导入图标,添加到lib/opal/src/icons/(即 web/lib/opal/src/icons/)。这与 web/lib/opal/src/components/README.md 描述的组件生态一致——Opal 是一个内部自维护的设计系统,所有 UI 资产从设计源直接进入代码库。
三、"有理由的规则":每条约束背后的原理
web/AGENTS.md特意将这一节命名为 "Rules with a reason"——每条规则都附带技术理由,理解原理后执行起来才不会机械。
1. 禁止dark:Tailwind 修饰符
No
dark:Tailwind modifier.令牌本身已定义了两套主题,覆盖写法会破坏暗色模式。仅createLogoIcon可用。
从源码看,设计令牌确实承载了双主题能力:web/lib/shared/tokens/ 下同时存在semantic-light.json与semantic-dark.json,构建后通过 web/lib/shared/README.md 中描述的.dark类在运行时切换。组件只消费令牌变量,主题翻转由令牌层完成;若在业务代码里写dark:bg-...,就会在令牌之外"另起炉灶",造成覆盖失效与样式漂移。
2. 禁止内置 Tailwind 颜色
不要写
bg-gray-100、text-blue-600,改用令牌类:text-0X、background-neutral-0X、background-tint-0X、border-0X、action-selection-0X、action-danger-0X、status-{info,success,warning,error}-0X、theme-*。
令牌定义在 web/lib/shared/tokens/(primitives.json、semantic-light.json、semantic-dark.json、shadow.json、size.json、typography.json、typography-presets.json)。语义色引用原色,例如"{alpha-grey-100-90}",构建后生成var(--alpha-grey-100-90),从而在暗色模式下整体翻转。业务代码只与语义层(text-0X、status-*等)打交道,颜色语义与具体色值解耦——这是企业级设计令牌体系的标准做法。
3. 文本 props 接受 Markdown
任何渲染为可见文本的 prop(
title、description、label)都要类型化为string | RichStr(来自@opal/types),并用Text渲染;调用方通过@opal/utils的markdown()显式开启解析。纯字符串永不解析。
这个设计很精妙:默认情况下文案就是普通字符串,不会被 Markdown 引擎误解析;只有显式调用markdown()的调用点才具备富文本能力,避免了"字符串里出现*就被渲染成斜体"之类的隐式陷阱。
4. Size props 默认值为"md"
当 prop 类型是@opal/types的SizeVariants(或其子集)时,缺省值必须为"md"。这保证了不同组件在未指定尺寸时表现一致,避免了"这个组件默认小、那个组件默认大"的割裂体验。
5. 优先 padding,而非 margin
使用组件的
paddingprop,而不是在外面包一层<div>;若库组件没有该 prop,应给组件本身补上,而不是增加 wrapper。
这条规则的动机很实际:wrapper<div>会把 DOM 层级越包越深,影响样式隔离与可访问性;把内边距收进组件则保持了结构扁平、API 自洽。
6. 数据获取模式:useSWR
useSWR,客户端内、在真正需要数据的组件内部使用,pending 时显示 loader。禁止在页面顶部统一拉取再向下传。
"数据靠近消费方"是 SWR 的核心理念——每个组件自己声明依赖,缓存与失效由 SWR 全局管理,同时避免了 prop drilling 层层透传。这与 Onyx 前端大量使用 React Query/SWR 生态的现状一致。
四、代码风格:让代码库看起来像一个人写的
web/AGENTS.md的 Style 一节定义了机械但可自动化的风格约束:
- 绝对导入:
@/指向src/,@opal/指向 Opal,禁止../相对路径; - 组件用函数声明(
function Foo() {}),不用箭头函数; - props 接口(
FooProps)与组件同文件;共享类型放进同目录types.ts;interfaces.ts是旧名,碰到就改名; - 类名拼接用
cn(来自@opal/utils),禁止模板字符串拼接; - Hooks 分层:特性 hooks 放
web/src/lib/<feature>/hooks.ts;不感知业务状态的 UI hooks 进 Opal;web/src/hooks/是最后兜底。
这些规则与根目录 AGENTS.md 中"保持严格类型(Python 与 TypeScript 都要)""注释要简短且聚焦长期有效信息"的全局要求一脉相承。cn工具函数的具体实现可查看@opal/utils(web/lib/opal/src/utils.ts)。
五、国际化(next-intl):把文案关进笼子里
国际化是 Onyx 前端规范中约束最严的领域之一,核心诉求是"源码里不允许出现裸的用户可见字符串"。
1. 禁止硬编码字符串
- 客户端用
useTranslations("<namespace>"); - 服务端用
await getTranslations(...); - oxlint 规则
i18n/no-raw-jsx-text会直接让裸文案构建失败。
2. 单一事实源与键的稳定性
web/src/i18n/messages/en.json 是唯一事实源。新增或修改键时,必须把最佳翻译同步到该目录下的其他所有语言文件(仓库实际包含ar、de、en、es、fr、ja、ko、pt、zh共 9 个 locale,见 web/src/i18n/messages/)。缺键或多键都会让types:check失败——键对齐是编译期检查(由 web/src/i18n/messages/keyParity.ts 实现)。
键是稳定标识符,命名规范为<namespace>.<section>.<element>.<role>的 camelCase,例如settings.appearance.colorMode.title。改写英文文案不改变键——键只描述文案在 UI 中的位置与角色,与具体措辞解耦。
3. ICU 形状必须一致
每个 locale 的消息不仅要能通过 ICU 解析,还必须与英文源使用完全相同的 ICU 占位符。这个约束由 web/src/i18n/tests/catalog.test.ts 守护——该测试用@formatjs/icu-messageformat-parser逐条解析所有 locale 的消息,检查占位符集合是否与英文源一致。也就是说,跨语言的占位符漂移(比如中文少了{count})会在 CI 中被拦截。
4. 日期、数字与排版方向
- 日期与数字一律用
useFormatter和useLocale,禁止硬编码"en-US"; - 新样式使用逻辑属性(
ms-、pe-、start-)而非物理属性(ml-、pr-、left-),为 RTL 语言(如阿拉伯语)留好余地。
六、测试:从 Jest 组件测试到 Playwright E2E
web/AGENTS.md只给测试划了三条边界,细节交给两个 README:
1. 组件测试(Jest + React Testing Library)
完整指南在 web/tests/README.md,核心要点:
- 测试与源码同目录存放(co-located);
- 必须用
setupUser()而非userEvent.setup()——前者自动包裹 React 的act(),消除 "Not wrapped in act()" 告警; - 查询选择器优先级:Role 查询(
getByRole)> Label > Placeholder > Text;禁止getByTestId、类名、元素类型等脆弱的反模式; - 异步断言用
findBy*或waitFor,禁止在状态更新后立即getBy*; - Mock 遵循"最小化"原则:只 mock 外部依赖(fetch、Next.js router),不 mock 应用代码;
- 测试命名描述用户行为("user can create new prompt"),不描述实现细节。
2. E2E 测试(Playwright)
硬性规则见 web/tests/e2e/README.md:
- 强制 Page Object Model(POM):一个 UI 表面一个 Page Object 类(如
ChatPage、InputBar),存放在tests/e2e/pages/;spec 只调用 POM 方法,绝不内联 locator; - Locator 优先级:
data-testid/aria-label> Role > Text/Label > CSS 选择器(最后手段); - 只用自动重试断言:
expect(locator).toHaveAttribute(...)、toHaveClass(...)、toHaveText(...)、toHaveCount(...)、toBeVisible()等会重试到超时;禁止用getAttribute/page.evaluate/textContent/count的单次快照读来做异步状态断言,否则必然 flaky。
3. 运行命令
规范明确指出 E2E 的启动方式:
cd web && bun run playwright <TEST_NAME>且不要用bunx或npx——它们可能拉取未固定版本的 Playwright。这一要求在 web/package.json 中可验证:"playwright": "playwright test"直接调用仓库本地固定版本(@playwright/test: ^1.39.0),"test": "jest"则用于组件测试。更多脚本(types:check、lint、format、storybook等)同样可以在该文件的scripts段找到。
七、规范如何与仓库其他文档协同
web/AGENTS.md并非孤立的孤岛,它与仓库文档体系形成闭环:
- 根目录 AGENTS.md 定义全局工程环境(uv 虚拟环境、测试密钥解析、Postgres 连接、Playwright 登录账号
admin_user@example.com/TestPassword123!等); - web/lib/shared/README.md 说明设计令牌的唯一真源、构建命令(
bun run build:tokens)与跨平台消费方式(web 用 CSS 变量、mobile 用 NativeWind); - web/lib/opal/src/components/README.md 说明 Opal 组件如何基于
@opal/core的Interactive原语构建,以及新增组件的六步流程(kebab-case 目录 →styles.css→components.tsx→ 导入样式 → README → barrel 导出); - backend/AGENTS.md(根文档中提及)承载全局测试策略的完整描述。
因此,可以把web/AGENTS.md理解为一棵树的"主干",而各目录下的 README 是向四周伸展的"枝干"——先读主干定方向,再读枝干补细节。
结语
Onyx 前端的这套规范回答了三个根本问题:组件从哪里来(设计系统优先、遗留代码隔离)、为什么这样写(令牌体系、双主题、i18n、可访问性背后的原理)、如何保证质量(编译期键检查、ICU 一致性测试、POM 化 E2E 与自动重试断言)。对于正在建设内部设计系统或重构大型 React 前端的团队,这份规范本身就是一份可借鉴的工程蓝本——组件分层、令牌驱动主题、文案键与措辞解耦、测试定位分层,这些思路可以原样迁移到任何 Next.js 项目中。要在真实代码里验证这些约定,可直接从 web/lib/opal/src/components/、web/src/refresh-components/ 与 web/src/i18n/messages/ 入手研读。
【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考