Onyx(Danswer)Web 前端工程规范全解:Opal 设计系统、组件分层、i18n 与测试实践
2026/9/10 15:30:35 网站建设 项目流程

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给出了明确的组件来源优先级链,这是整个前端规范的地基:

  1. web/lib/opal/src/@opal/*):设计系统,第一选择;
  2. web/src/refresh-components/:尚未进入 Opal 的生产组件;
  3. 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(配合fontcolorprops),禁止裸露文本节点refresh-components/texts/Text的布尔 flag API 已废弃@opal/components
图标@opal/icons禁止lucide-reactreact-icons
悬停显示Hoverable;必须手写时补no-hover:opacity-100以兼容触屏@opal/core
交互原语InteractiveDisabled仅用于构建组件,应用代码不得直接使用@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 修饰符

Nodark:Tailwind modifier.令牌本身已定义了两套主题,覆盖写法会破坏暗色模式。仅createLogoIcon可用。

从源码看,设计令牌确实承载了双主题能力:web/lib/shared/tokens/ 下同时存在semantic-light.jsonsemantic-dark.json,构建后通过 web/lib/shared/README.md 中描述的.dark类在运行时切换。组件只消费令牌变量,主题翻转由令牌层完成;若在业务代码里写dark:bg-...,就会在令牌之外"另起炉灶",造成覆盖失效与样式漂移。

2. 禁止内置 Tailwind 颜色

不要写bg-gray-100text-blue-600,改用令牌类:text-0Xbackground-neutral-0Xbackground-tint-0Xborder-0Xaction-selection-0Xaction-danger-0Xstatus-{info,success,warning,error}-0Xtheme-*

令牌定义在 web/lib/shared/tokens/(primitives.jsonsemantic-light.jsonsemantic-dark.jsonshadow.jsonsize.jsontypography.jsontypography-presets.json)。语义色引用原色,例如"{alpha-grey-100-90}",构建后生成var(--alpha-grey-100-90),从而在暗色模式下整体翻转。业务代码只与语义层(text-0Xstatus-*等)打交道,颜色语义与具体色值解耦——这是企业级设计令牌体系的标准做法。

3. 文本 props 接受 Markdown

任何渲染为可见文本的 prop(titledescriptionlabel)都要类型化为string | RichStr(来自@opal/types),并用Text渲染;调用方通过@opal/utilsmarkdown()显式开启解析。纯字符串永不解析。

这个设计很精妙:默认情况下文案就是普通字符串,不会被 Markdown 引擎误解析;只有显式调用markdown()的调用点才具备富文本能力,避免了"字符串里出现*就被渲染成斜体"之类的隐式陷阱。

4. Size props 默认值为"md"

当 prop 类型是@opal/typesSizeVariants(或其子集)时,缺省值必须为"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.tsinterfaces.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 是唯一事实源。新增或修改键时,必须把最佳翻译同步到该目录下的其他所有语言文件(仓库实际包含ardeenesfrjakoptzh共 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. 日期、数字与排版方向

  • 日期与数字一律用useFormatteruseLocale,禁止硬编码"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 类(如ChatPageInputBar),存放在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>

不要bunxnpx——它们可能拉取未固定版本的 Playwright。这一要求在 web/package.json 中可验证:"playwright": "playwright test"直接调用仓库本地固定版本(@playwright/test: ^1.39.0),"test": "jest"则用于组件测试。更多脚本(types:checklintformatstorybook等)同样可以在该文件的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/coreInteractive原语构建,以及新增组件的六步流程(kebab-case 目录 →styles.csscomponents.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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询