ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Storybook Monorepo 实战指南:共享 Preview、MSW 数据与可验证 Stories 的完整搭建规范

2026/9/10 8:52:29 拓冰建站 浏览量
Storybook Monorepo 实战指南:共享 Preview、MSW 数据与可验证 Stories 的完整搭建规范 Storybook Monorepo 实战指南共享 Preview、MSW 数据与可验证 Stories 的完整搭建规范【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook本文以 Storybook 官方仓库中的 Agent 评测任务书 scripts/eval/prompts/monorepo.md 为核心骨架系统拆解在 Nx/Workspaces 类 monorepo 项目中从零让 Storybook「完全可用」的标准流程先通过受控的 Glob/Grep/Read 探测运行时依赖再一次性构建出足以覆盖绝大多数 story 的共享.storybook/preview.tsx接入 MSW 模拟数据、处理 portal 挂载点、批量编写带CssCheck校验的 stories最后用storybook/addon-vitest在浏览器环境中批量验证。读者读完将掌握一套可直接复制的「发现 → 基础设施 → 故事编写 → 批量验证」流水线以及判断什么时候该写play、什么时候该修共享 preview 的工程决策准则。一、这份文档是什么monorepo 场景下的 Agent 任务规范scripts/eval/prompts/monorepo.md不是一份面向普通用户的 Storybook 教程而是 Storybook 官方仓库agent-eval评测体系中用于衡量 AI AgentMCP 工具与 CLI 插件两种形态能否独立完成「在 monorepo 项目中完整搭建 Storybook」能力的任务书。它规定了任务环境、交战规则、八步实施计划与明确的完成标准其产出会经由 agent-eval/lib/experiment.ts 中的评测管线如813-monorepo-leaf-create-component等核心场景自动判定。任务书对应的真实工程样板位于 agent-eval/templates/monorepo/是一个标准的 npm workspaces monorepopackages/ ├── app/ # 消费方应用acme/app依赖 acme/ui │ ├── src/main.tsx │ └── index.html └── ui/ # 组件库acme/uiStorybook 搭建的目标包 ├── src/ │ ├── components/Card.tsx │ └── index.ts ├── stories/Card.stories.tsx ├── package.json ├── tsconfig.json ├── vitest.config.ts └── vitest.storybook.config.ts其中acme/app的入口 main.tsx 直接消费acme/ui导出的Card组件acme/ui的 index.ts 负责统一导出——这正是任务书第 3 条「如果用到本地 monorepo 依赖先构建所有发现的依赖」所针对的典型结构。acme/ui的 package.json 中预置了storybook/react-vitenext 版本、storybook/addon-vitest、storybook/addon-a11y、storybook/addon-docs等依赖以及test:stories脚本说明该包就是 Storybook 要被「点亮」的叶子包。任务环境速览PropertyValueVersion10.4.0-alpha.10Rendererstorybook/reactFrameworkstorybook/react-viteBuilderstorybook/builder-viteConfig Dir.storybookLanguageTypeScriptPackage Managerunknown package manager由项目 lockfile 检测Addonsstorybook/addon-onboarding, storybook/addon-themes, storybook/addon-docs, storybook/addon-designs, storybook/addon-vitest, storybook/addon-a11y, storybook-addon-pseudo-states, chromatic-com/storybook任务书的最终目标一句话概括让 Storybook 在本项目中完全可用——正确配置.storybook/preview.tsx的 decorators、为数据接入 MSW、编写最多 10 个与源码同目录的*.stories.tsx文件并且只在能证明非平凡行为的地方添加play函数。注意eval-template.json中amazonLinuxPackages: playwright-chromium的声明说明该评测场景的验证环节依赖 Playwright 的 Chromium 浏览器环境。二、交战规则Rules of Engagement把工具纪律当作时间预算任务书开篇用「这些是时间预算不是建议」来强调前九条交战规则。它们本质上是把「在陌生仓库中高效且不破坏性地完成任务」沉淀成可执行约束用 Glob/Grep/Read 工具发现禁用 shell 探索。列目录用Glob(src/components/*)别名search_files、file_search搜字符串用Grep(pattern, { path: src })别名grep_search、search_files读文件用Read(path/to/file)别名read_file批量编辑用多次Edit或一次Editreplace_all——而不是ls、find、cat、head、tail、shellgrep、sed、node -e。理由是这些 shell 命令单次调用更慢且破坏缓存。绝不读或 grepnode_modules。任务书中给出的 import 路径是正确的不要通过检查已安装包来验证若感觉不对重读任务书而不是去翻node_modules。Nx monorepo 局部优先。不要一开始就翻其他包里的配置或既有 Storybook 内容从目标包本地的配置与工具链开始探索若用到本地 monorepo 依赖在写 stories 或跑测试前先构建全部发现的依赖。读取预算约 12 个文件。写任何代码前最多读约 12 个文件index.html、入口、App、providers、路由、根 CSS、2–3 个代表性页面/组件、1–2 个 hooks、1 个测试不够就概括后继续前进。Edit 优先于 Write。读过的文件用Edit修改Write只用于新文件项目里已有storybook init生成的.storybook/preview.tsx要Edit它而不是覆盖。批量测试循环。先写完所有 stories再统一跑一次 vitest在首次批量运行暴露失败之前不做单文件 vitest。每次安装都使用unknown package manager从本项目 lockfile 检测出的包管理器。优先修共享的.storybook/preview.tsx当多个 stories 以同样方式失败时不要在 story 局部打补丁。达到成功标准就停不要无休止打磨。这套纪律对应到仓库中的验证设施acme/ui的 vitest.storybook.config.ts 通过storybook/addon-vitest的storybookTest({ configDir: ... })插件定义storybook测试项目并用vitest/browser-playwright以 headless Chromium 实例运行——这就是规则 6「统一跑一次 vitest」所指的命令行npx vitest --project storybook run的底层配置来源。三、Step 1 — 发现运行时≤12 次读取第一步的目标是在预算内回答一个核心问题一个典型页面要渲染preview 必须供应哪些 providers、CSS、浏览器状态和网络调用按以下顺序先用 Glob/Grep 再用针对性 Readindex.html——link relstylesheet标签、内联style块、字体以及非 JS 创建的div id...挂载点或 portal 根节点入口文件main.tsx/index.tsx—— 包裹App /的 providers、根 CSS importApp.tsx—— 顶层布局、路由使用、消费的 providersproviders / context 文件 —— 它们暴露了什么根 CSS —— 全局样式、CSS 变量、主题 tokens包括 JS import 的 CSS和index.html里 link 的数据 hooks ——fetch(...)、useQuery、axios等捕获渲染时实际调用的 base URL 与 endpoints渲染时实际读取的浏览器状态 ——localStorage/sessionStorage/ cookie 键名portal 目标 ——createPortal(...)及其挂载的 DOM id如#modal-root1–2 个真实页面或功能组件作为 story 的 JSX 模式真源。以评测模板为例packages/app/index.html只有一个#root挂载点main.tsx用StrictModecreateRoot渲染且组件全部来自acme/ui——这意味着本轮发现只需确认「没有自定义 providers、没有额外 CSS 文件、没有 portal、没有数据请求」即可直接推进到 Step 2而不是盲目堆砌基础设施。四、Step 2 — 构建共享 Preview一次性解决大多数 story 的准备工作Step 2 的核心原则是把 Storybook 一次性配置好让绝大多数 story 无需逐文件设置。做法是Edit 已有的.storybook/preview.tsx由storybook init创建向既有 config 对象中合并新增内容而不是整体替换。完整的目标形态如下将新片段合并进已有内容// .storybook/preview.tsx import type { Preview } from storybook/react-vite; import ../src/index.css; import MockDate from mockdate; import { initialize, mswLoader } from msw-storybook-addon; import { SessionProvider } from ../src/contexts/SessionContext; import { mswHandlers } from ./msw-handlers; initialize({ onUnhandledRequest: bypass }); const preview: Preview { decorators: [ (Story) ( SessionProvider Story / /SessionProvider ), ], loaders: [mswLoader], parameters: { msw: { handlers: mswHandlers } }, async beforeEach() { localStorage.setItem(theme, dark); MockDate.set(2024-04-01T12:00:00Z); }, }; export default preview;Preview 的四条硬性规则使用真实的 provider 树与真实的根 CSS import不要凭空发明 providers如果应用的 CSS 是通过index.html中的link加载而非 JS import则从 preview 中 import 同一个文件保证 story 渲染出相同的样式只播种应用实际读取的浏览器状态键不要清空全部localStorage/sessionStorage/ cookies也不要重置 Storybook 自身状态mockdate仅在渲染输出依赖日期时使用不要直接 mockwindow、document、navigator、observers 或fetch。这四条规则与评测模板的组件实现相互印证Card组件的样式全部内联在 JSX 中见 Card.tsx因此共享 preview 在此场景下无需导入额外 CSS而模板的 vitest.storybook.config.ts 中setupFiles: [.storybook/vitest.setup.ts]的存在说明beforeEach这类全局生命周期钩子正是 Storybook 测试模式下被统一执行的地方。五、Step 3 — Portals写在 decorator 里而不是 preview-body.html如果在 Step 1 发现了createPortal(..., document.getElementById(foo))的调用在.storybook/preview.tsx中添加一个在 story 渲染前创建 portal 根节点的 decorator不要使用preview-body.html// Add this entry to the decorators array of your preview config: (Story) { for (const id of [modal-root, drawer-root, toast-root]) { if (!document.getElementById(id)) { const el document.createElement(div); el.id id; document.body.appendChild(el); } } return Story /; }将该 decorator 加入 preview config 的decorators数组。若 portal 只指向document.body则完全跳过此步。之所以用 decorator 而非preview-body.html是因为 decorator 能确保每个 story 渲染前根节点必然存在且逻辑与 preview 配置保持同源、可被 vitest 测试环境复用而模板中的Card组件并不使用 portal属于「跳过此步」的典型情况。六、Step 4 — MSW handlers只覆盖 stories 会命中的端点使用msw-storybook-addon安装命令为your-package-manager add -D msw msw-storybook-addon mockdate npx msw init ./public --savenpx msw init会在./public下生成 Service Worker 文件因此需要确保.storybook/main.ts把./public作为静态目录提供服务// .storybook/main.ts import type { StorybookConfig } from storybook/react-vite; const config: StorybookConfig { staticDirs: [../public] }; export default config;handlers 放在.storybook/msw-handlers.ts只覆盖你的 stories 会实际用到的端点不写 catch-all// .storybook/msw-handlers.ts import { http, HttpResponse } from msw; export const mswHandlers { products: [ http.get(https://api.example.com/products, () HttpResponse.json({ items: [{ id: p1, name: Example, price: 42 }] }) ), ], };与 Step 2 中 preview 的loaders: [mswLoader]与parameters.msw.handlers组合起来就构成了「MSW 初始化 → 全局 handler 注入 → 每个 story 渲染前由 loader 激活 mock」的完整链路。initialize({ onUnhandledRequest: bypass })表示未匹配的请求直接放行避免无关网络调用干扰 story。七、Step 5 — 批量编写最多 10 个 story 文件含唯一的 CssCheck本步交付物是两样东西缺一不可① 最多 10 个与源码同目录的*.stories.tsx② 恰好一个CssCheckstory必须加入其中一个文件见 Step 5b。Step 5 未完成CssCheck就不算完成。Step 5a — 挑选目标并编写文件从真实代码库中挑选约 10 个有意义的目标从底层可复用组件到页面组件。跳过子组件、hooks、contexts、helpers以及存在真实页面组件时的App本身。每个 story 文件典型组件约 3 个 export确有实际使用场景时可到约 10 个JSX 模式从真实页面/路由/测试中复制。每个新 story 文件从一开始就标记[ai-generated, needs-work]只有在该文件通过 vitest 之后才移除needs-work——这样所有尚未验证的内容包括来不及修复的 stories都会保持正确标记import type { Meta, StoryObj } from storybook/react-vite; import { expect } from storybook/test; import { Button } from ./Button; const meta { component: Button, tags: [ai-generated, needs-work], // strip needs-work once vitest passes } satisfies Metatypeof Button; export default meta; type Story StoryObjtypeof meta; // Smoke check — one is enough per file export const Primary: Story { args: { children: Order now }, play: async ({ canvas }) { await expect(canvas.getByRole(button, { name: /order now/i })).toBeVisible(); }, }; // Variant-only stories: no play needed export const Clear: Story { args: { children: Cancel, clear: true } }; export const Large: Story { args: { children: Checkout, large: true } }; export const WithIcon: Story { args: { icon: cart, aria-label: food cart } };Story 编写规则每个 meta 都以tags: [ai-generated, needs-work]开头显式写出所有 import不添加自定义title由 component 自动推导层级不构建大型 story 专用 harness —— 优先修复 preview不创建新的应用组件。评测模板中已有的 Card.stories.tsx 恰好是这一规范的反面示例——它带有title: UI/Card且没有 tags正是 Agent 需要按任务书「改造/重写」的对象。而Card组件源码中data-testidcard与内联样式border: 1px solid #e5e7eb、padding: padded ? 16 : 0则为CssCheck提供了真实的计算样式断言来源。Step 5b — 添加唯一的 CssCheck story在 Step 5 结束前从刚写过的文件中挑一个视觉上独特的组件为该文件添加CssCheckexport。整个项目恰好只有一个CssCheck不是每文件一个。Step 5 直到它存在才算完成。为什么它是强制项toBeVisible对未加样式的组件也能通过。一个具体的getComputedStyle值才是「共享 preview 确实加载了应用 CSS」的唯一证明——否则你根本不知道 stories 是否真的渲染正确。做法从组件源码中读取一个真实样式值如 styled-components 中的十六进制颜色、Tailwind 类bg-blue-600、主题中的 CSS 变量并断言解析后的getComputedStyle值export const CssCheck: Story { args: { children: Submit }, play: async ({ canvas }) { const button canvas.getByRole(button, { name: /submit/i }); // PrimaryButton uses bg-blue-600 — fails if Tailwind / global CSS did not load. await expect(getComputedStyle(button).backgroundColor).toBe(rgb(37, 99, 235)); }, };八、Step 6 — play 函数的取舍只在能证明非平凡行为时添加不要给每个 story 都加play。只有当play能断言「仅靠渲染输出本身无法证明」的内容时才值得写。宁可每文件一个好play也不要五个冗余的。值得写play的场景交互表单填写 提交、点击 → 菜单展开、切换 tab 揭示面板异步数据确实从 MSW 到达等待 mock 内容替换 spinnerportal 渲染进正确的根节点通过canvasElement.ownerDocument查询具有语义意义的 CSS 驱动状态如主题色、disabled 样式、能确认全局样式表已加载的布局组件负责的无障碍正确的 role/label 暴露。完全跳过play的场景story 只是同一组件的静态变体不同args无新行为。在Clear、Large、WithIcon等上面重复getByRole(...).toBeVisible()是冗余的——组件抛出异常或无法挂载时渲染本身就会失败。Smoke play 必须证明渲染本身证明不了的东西。只做await expect(canvas.getByRole(button)).toBeVisible()的 play 毫无价值。可接受的 smoke play 断言以下之一反映状态的 aria 属性aria-expanded、aria-disabled、aria-checked、aria-current以文本或属性渲染的 prop 值如args.label出现在 DOM 中、href匹配args.to异步内容到达findBy*、waitFor—— 证明 loader/MSW handler 真的解析了portal 挂载进正确的根通过canvasElement.ownerDocument.body查询。具体到包含Primary、Clear、Large、WithIcon的Button.stories.tsxPrimary保留一个 smokeplay每文件一个就够Clear、Large、WithIcon不加play。项目唯一的CssCheck已在 Step 5 添加此处不要再加。Imports 与 play 上下文——这里搞错会让 vitest 以微妙的方式失败expect和waitFor来自storybook/test—— 必须显式 importcanvas、userEvent、canvasElement来自play 参数async ({ canvas, userEvent, canvasElement }) { ... }。不要import { userEvent } from storybook/test不要写const canvas within(canvasElement)——两者都已提供仅 portal 查询时通过canvasElement.ownerDocument.body查询此时可以从storybook/testimportwithin如within(canvasElement.ownerDocument.body).findByTestId(...)其他场景不要用within。export const FilledForm: Story { play: async ({ canvas, userEvent }) { await userEvent.type(canvas.getByLabelText(email), ab.com, { delay: 50 }); await userEvent.click(canvas.getByRole(button, { name: /submit/i })); await expect(await canvas.findByText(/welcome/i)).toBeVisible(); }, };九、Step 7 — 一次性批量验证再按失败迭代运行任何测试前先读这条规则首次 vitest 调用必须一起运行所有新 stories批量运行前禁止单文件运行npx vitest --project storybook run随后运行项目的 TypeScript 检查使用package.json中的脚本通常是tsc --noEmit或unknown package manager run typecheck。直接读一次原始输出不要反复用grep/head切片。对每个失败的处理循环读错误若多个 stories 共享同一失败修复共享的 preview 配置而不是修 stories只对受影响文件重跑 vitestnpx vitest --project storybook run path/to/Foo.stories.tsx重复直到文件通过然后继续下一个。每个文件重试上限约 2 次——仍然失败就保留needs-work继续前进。文件通过后编辑其 meta 移除needs-work使 tags 变为[ai-generated]修不好的文件保留[ai-generated, needs-work]——继续前进不要无限循环。该步骤在仓库中的执行载体是 vitest.storybook.config.tsstorybookTest({ configDir: path.join(dirname, .storybook) })让 vitest 直接驱动 Storybook 渲染并执行playbrowser: { enabled: true, headless: true, provider: playwright({}), instances: [{ browser: chromium }] }表明这些 story 测试是在真实浏览器环境中运行的这也解释了为什么任务书要求「不 mockwindow/document/navigator」——浏览器环境本身已真实存在。十、Step 8 — 清理收尾结束前移除调试代码、诊断期间添加的宽泛 mocks、未使用的依赖以及评测残留物eval artifacts。对应到规则 9「达到成功标准就停」——清理只是回到干净基线不是继续打磨。十一、完成标准Done when任务书明确列出以下验收项缺一不可恰好一个CssCheckstory存在于新 stories 中且断言的是从组件源码读取的某个具体计算样式值在 Step 5 末尾添加每个被 vitest 确认通过的 story 文件都已移除needs-worktags 变为[ai-generated]仍失败的保持[ai-generated, needs-work]npx vitest --project storybook run对新文件通过项目 TypeScript 检查对变更文件通过共享 preview 足够强stories 无需逐文件的 fetch/provider 变通方案。最后一条标准与规则 8 呼应共享 preview 的强度是整套方案是否合格的终极判据——它意味着所有「每个 story 都要重复一次」的准备逻辑provider、CSS、浏览器状态、MSW handler都被正确收敛到了.storybook/preview.tsx这一处。十二、仓库中的配套证据与延伸阅读本文所述规范并非孤立的 prompt 文本它在当前仓库中有完整的工程配套可作为深入研究的入口评测模板本体agent-eval/templates/monorepo/ 中的 workspaces 结构packages/app与packages/ui、eval-template.json 的playwright-chromium环境声明验证配置vitest.storybook.config.ts 演示了storybook/addon-vitestvitest/browser-playwright的浏览器内 story 测试接线这也是npx vitest --project storybook run得以工作的底层配置组件与 story 真源Card.tsx内联样式 data-testid与 Card.stories.tsx带title的既有写法——它们是 Step 5 改写练习的典型对象评测编排agent-eval/lib/experiment.ts 中的813-monorepo-leaf-create-component等核心评测与 900s 超时配置解释了 monorepo 场景在整体评测线中的位置Storybook 自身能力文档仓库 docs/writing-stories/、docs/writing-tests/ 与 docs/api/ 目录提供了 decorators、play function 与 vitest 集成的完整用户文档任务书中的Reference小节指向的正是这些主题。如需将本流程复用到自己的项目可将「发现运行时 → 构建共享 preview → 处理 portal/MSW → 批量编写带验证的 stories → 统一跑vitest --project storybook」五段式作为模板并把CssCheck思想移植过去任何依赖全局样式的组件库都应该至少有一个断言getComputedStyle具体值的 story作为「共享基础设施真的生效了」的守门测试。【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考