Onyx(danswer)前端 Storybook 组件库开发指南:从本地运行、编写 Stories 到分层设计与部署
【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer
Storybook 是 Onyx(danswer)项目前端团队的组件隔离开发环境,它将每个 UI 组件渲染为独立"story",让工程师和设计师在不进入完整应用的情况下验证外观、交互 props 并捕捉回归。本文以web/.storybook/README.md为骨架,结合仓库内web/.storybook/的真实配置与lib/opal/、src/refresh-components/中的实际 Stories 源码,完整讲解本地运行、Story 编写规范、设计系统分层、暗黑模式支持与生产部署,读完即可上手为 Onyx 贡献组件级 Story。
Storybook 在 Onyx 中的角色
Onyx 前端采用分层设计系统架构:web/lib/opal/承载底层设计原语与组件原子,web/src/refresh-components/承载面向应用的复合组件,web/src/app/下则存放各业务 App 的特性组件。Storybook 将这套体系串联成一份"活的目录"(living catalog),覆盖从@opal/core低层原语到refresh-components、再到Apps/层特性组件的全部视觉状态,为设计师和工程师提供共享的视觉参考基准。
每个组件在 Storybook 中都有专属页面,提供:
- 实时 Demo:可直接交互的组件示例;
- Controls 面板:实时调整 props,观察组件响应(切换布尔值、选择枚举、输入字符串,预览即时更新);
- 自动生成的文档:通过
tags: ["autodocs"]从 props 推导出完整的 API 文档页; - 主题切换:工具栏中的调色盘图标可在明/暗两套主题间预览。
本地运行 Storybook
在仓库根目录下进入web/并执行:
cd web bun run storybook # 开发服务器,默认 http://localhost:6006 bun run storybook:build # 静态构建,输出到 storybook-static/这两条命令在 web/package.json 中定义为:
"storybook": "storybook dev -p 6006", "storybook:build": "storybook build -o storybook-static"开发服务器基于@storybook/react-vite框架(仓库中锁定的版本为10.5.0),当你编辑组件或 story 文件时会热更新。生产构建则输出纯静态站点,便于托管在任意静态服务器或 CDN 上。
底层配置:main.ts
Storybook 的入口配置位于 web/.storybook/main.ts,其中几处关键配置决定了组件发现范围与 Vite 构建行为:
- stories 扫描范围:Storybook 会递归匹配以下 glob 下的
.stories.ts/tsx文件(含 MDX):
stories: [ "./*.mdx", "../lib/opal/src/**/*.stories.@(ts|tsx)", "../src/refresh-components/**/*.stories.@(ts|tsx)", "../src/sections/**/*.stories.@(ts|tsx)", "../src/app/craft/**/*.stories.@(ts|tsx)", "../src/views/**/*.stories.@(ts|tsx)", ]- addons:启用了
@storybook/addon-themes(明暗主题切换)、@storybook/addon-docs(自动文档)以及@storybook/addon-mcp; - 路径别名:通过
viteFinal为@(映射../src)、@opal(映射../lib/opal/src)、@public(映射../public)配置别名,与应用的 TS 路径保持一致; - Next.js 模块桩:由于 Storybook 基于 Vite 而非 Next.js,
next/link、next/navigation、next/image三个模块被分别映射到web/.storybook/mocks/下的桩实现(next-link.tsx、next-navigation.tsx、next-image.tsx),保证引用了 Next 专有 API 的组件也能在 Storybook 中渲染; - PostCSS/Tailwind:将 PostCSS 配置指向
web/根目录,使 Tailwind 样式在 story 预览中生效; - 环境变量桩:
process.env被定义为空对象,避免依赖顶层process.env的模块(如src/lib/constants.ts)在 Vite 下报错; - publicDir 关闭:
config.publicDir = false用于规避 Vite 默认 public 目录拷贝与 StorybookstaticDirs(指向../public)之间的目录冲突(对应 storybook 社区 issue #24627)。
全局预览与装饰器:preview.ts
web/.storybook/preview.ts 为所有 story 注入全局行为:
- 默认布局
layout: "centered",并禁用背景色切换(backgrounds: { disabled: true }),避免与主题变量冲突; - Controls 匹配器:颜色类 props(如
background、color)自动获得色板控件,Date结尾的 props 自动获得日期控件; - 主题装饰器:
withThemeByClassName通过给预览 body 添加/移除darkclass 切换明暗主题,与应用端darkMode: "class"的 Tailwind 配置完全一致; - Tooltip 装饰器:全局包裹
TooltipPrimitive.Provider,因此任何使用 Radix tooltip 的组件在 story 中无需额外处理即可正常弹出提示; - i18n 装饰器:用
NextIntlClientProvider包裹并加载英文消息(locale: "en",消息来自 web/src/i18n/messages/en.json),保证 story 始终渲染与 App 默认一致的英文目录。
编写 Stories:co-location 约定
Stories 与组件源码同目录存放(co-located),典型结构如下:
lib/opal/src/core/interactive/ ├── components.tsx ← 组件本体 ├── Interactive.stories.tsx ← 对应 story └── styles.css src/refresh-components/buttons/ ├── Button.tsx ← 应用层组件(示意) └── Button.stories.tsx ← 对应 story实际仓库中,核心原语位于 web/lib/opal/src/core/interactive/Interactive.stories.tsx,而设计系统原子按钮的 story 则位于 web/lib/opal/src/components/buttons/button/Button.stories.tsx。src/refresh-components/下则分散着 54+ 个.stories.tsx文件(AreaChart、Calendar、CommandMenu、SimpleTabs等),覆盖 inputs、tables、modals、text、cards、tiles 等类别。
最小模板
import type { Meta, StoryObj } from "@storybook/react-vite"; import { MyComponent } from "./MyComponent"; const meta: Meta<typeof MyComponent> = { title: "Category/MyComponent", // 侧边栏路径 component: MyComponent, tags: ["autodocs"], // 从 props 自动生成文档页 }; export default meta; type Story = StoryObj<typeof MyComponent>; export const Default: Story = { args: { label: "Hello" }, };编写约定
- 标题格式:
Core/Name、Components/Name、Layouts/Name、refresh-components/category/Name,或Apps/<App>/<Category>/<Name>; - Tags:添加
tags: ["autodocs"]自动生成 props 文档页; - Decorators:使用 Radix tooltip 的组件需要
TooltipPrimitive.Provider装饰器(虽然preview.ts已全局注入,但局部装饰器可用于定制场景); - Layout:模态框/弹出层这类使用 portal 的组件,使用
parameters: { layout: "fullscreen" }避免被居中布局裁剪。
真实示例:Button 的多状态覆盖
Button.stories.tsx是理解"多状态视觉契约"的最佳范本。它以多个命名 story 穷举按钮的全部视觉状态:
Default通过args设置children、variant、prominence;VariantProminenceGrid用render函数渲染 3(default/action/danger)× 3(primary/secondary/tertiary)的变体矩阵;Sizes遍历lg、md、sm、xs、2xs、fit六种尺寸;- 另有
WithLeftIcon、WithRightIcon、IconOnly、Foldable、Disabled、WidthFull、AsLink(渲染为<a>)、WithTooltip、ResponsiveHideText、InternalProminence等故事,全面覆盖组件的 props 组合。
这种"一个组件多个 story"的模式正是 Storybook 用于视觉回归与状态审计的核心手段。
暗黑模式支持
通过 Storybook 工具栏中的**主题切换按钮(调色盘图标)**即可在明/暗两套主题间切换。其底层实现是withThemeByClassName装饰器为预览 body 添加或移除darkclass——与应用的 TailwinddarkMode: "class"配置保持一致,因此colors.css中定义的全部颜色 token 会自动适配,无需为 Storybook 编写任何额外的暗色样式。这也是"组件在 Storybook 中的表现即应用中的表现"这一原则的基础。
组件分层:侧边栏目录即设计系统
Storybook 侧边栏按设计系统分层组织组件,与仓库目录一一对应:
| Layer | 仓库路径 | 示例 |
|---|---|---|
| Core | lib/opal/src/core/ | Interactive、Hoverable |
| Components | lib/opal/src/components/ | Button、OpenButton、Tag |
| Layouts | lib/opal/src/layouts/ | Content、ContentAction、IllustrationContent |
| refresh-components | src/refresh-components/ | Inputs、tables、modals、text、cards、tiles 等 |
| Apps | src/app/<app>/... | 各 App 的特性组件 |
Apps 层:有边界的扩展
Apps/层是对组件目录的一种刻意受限的扩展:只有具备多状态视觉契约(statuses、variants、kinds)且数据驱动(接收 props、不发请求)的特性组件才有资格编写 story。而那些编排状态的组件——负责 fetch 数据、管理 SWR 缓存、持有 context 的组件——不应进入 Storybook,它们属于运行中的应用。
当前覆盖情况:
Apps/Craft/—— 专为 Craft UI 构建的组件,如CometEdge、CraftInputBar、CraftSessionDeleteModal、InterruptHint、ModelPickerButton等(见 web/src/app/craft/components/)。
标题格式为Apps/<App>/<Category>/<Name>,例如Apps/Craft/Tool Cards/Bash Body。这一约束保证了 Storybook 目录始终是"纯展示组件的可浏览参考",不会混入业务逻辑。
部署与发布
生产 Storybook 作为静态站点部署在 Vercel 上:构建阶段执行bun run storybook:build,产物输出到storybook-static/目录,由 Vercel 直接托管该目录。构建产物为纯静态文件,同样可部署到任意静态托管平台。
部署触发条件:当web/lib/opal/、web/src/refresh-components/、web/.storybook/或任意Apps/层源码路径下的文件发生变更并合并到main分支时,自动触发部署。这意味着组件库的任何视觉改动都会同步反映到在线目录,供团队即时审阅。
小结
Onyx 的 Storybook 实践可以归纳为三条原则:
- 同目录存放:story 紧跟组件源码,降低维护成本,避免"文档与代码脱节";
- 按层组织:
Core → Components → Layouts → refresh-components → Apps的分层既是目录结构,也是设计系统的演进边界; - 纯展示约束:只收录"收 props、不 fetch"的组件,用多 story 穷举视觉状态,把状态编排留在应用内部。
基于这套约定,任何开发者都可以为 Onyx 前端贡献高质量的组件文档与视觉回归保障:在web/.storybook/README.md中查看顶层约定,参考 Button.stories.tsx 的写法,遵循 main.ts 的扫描与构建规则,即可让新组件自动进入这份"活的目录"。
【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考