Storybook 实战:在 TanStack React 框架中屏蔽服务端模块(sb.mock+__mocks__文件注册指南)
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
导读
TanStack Start 应用的路由文件常在模块顶层导入数据库客户端、认证库等仅运行于 Node.js 的服务端包。当 Storybook 在浏览器中加载路由树时,这些导入会直接导致运行时崩溃。本文基于 Storybook 官方文档的 TanStack React 框架集成,讲解如何在.storybook/preview.ts中通过sb.mock(import('../src/db/client.ts'))注册 mock,并结合__mocks__文件彻底阻止服务端模块及其依赖链在浏览器中被求值——同时覆盖 CSF 3 与 CSF Next 两代配置写法,并附上源码级的原理剖析。
场景背景:为什么 TanStack Start 的路由会"带崩"浏览器
TanStack Start 是构建在 TanStack Router 之上的全栈 React 框架,其路由文件经常在模块顶层直接导入服务端专用依赖,例如:
- 数据库客户端(
postgres、pg、drizzle-orm/postgres-js等) - 认证 / 会话库(
~/auth/index.server等) - 其他依赖 Node.js 运行时能力的包
Storybook 需要加载整棵路由树才能渲染路由组件,这些模块级导入一旦进入浏览器,就会出现类似does not provide an export named 'default'或AsyncLocalStorage is not defined的报错。
针对这一问题,TanStack React 框架文档 给出了三层处理策略:
- 框架级自动 mock:
@storybook/tanstack-react的 preset 会自动拦截@tanstack/react-start、@tanstack/react-start/server、@tanstack/start-storage-context等 TanStack 相关模块,并把createServerFn().handler(...)的结果替换为可观察、可覆写的 mock 函数,此层无需开发者干预; - 应用级服务端模块:对于应用自有的服务端代码(如
~/db/client、~/auth/index.server),需要在.storybook/preview.ts中注册sb.mock,并配套__mocks__文件; - 识别排查:根据报错堆栈定位需要 mock 的模块(详见下文)。
本文聚焦第二层——即关联文档中演示的注册写法。
在.storybook/preview.ts中注册模块 mock
当你的路由引入了应用自有的服务端模块(例如~/db/client内部import了postgres)时,需要先在项目级 Storybook 配置.storybook/preview.ts中注册 mock。关联文档提供了两种写法:
CSF 3 写法
import { sb } from 'storybook/test'; // Prevents postgres (Node-only) from loading in the browser sb.mock(import('../src/db/client.ts')); export default {};CSF Next 🧪 写法
import { definePreview } from '@storybook/tanstack-react'; import { sb } from 'storybook/test'; // Prevents postgres (Node-only) from loading in the browser sb.mock(import('../src/db/client.ts')); export default definePreview({});两种写法的核心都是这一行:
sb.mock(import('../src/db/client.ts'));区别仅在于配置文件的导出方式:CSF 3 导出普通对象,而 CSF Next 使用@storybook/tanstack-react提供的definePreview包裹配置。从 源码实现 可以看到,definePreview本质上是调用@storybook/react的__definePreview,并把 TanStack 专用的 preview 注解(路由装饰器、loader、beforeEach 等,定义于 preview.tsx)注入进去:
export function definePreview(preview) { return __definePreview({ ...preview, addons: [tanstackPreview, ...(preview.addons ?? [])], }); }创建配套的__mocks__文件
注册之后,还要在真实模块旁边创建对应的__mocks__文件。以src/db/client.ts为例,创建src/db/__mocks__/client.ts,且只使用import type,确保不会把任何服务端包带入浏览器:
import type { drizzle } from 'drizzle-orm/postgres-js'; import type * as schema from '../schema'; export const db = new Proxy({} as ReturnType<typeof drizzle<typeof schema>>, { get: () => () => Promise.resolve([]), });关键点:
- 用
import type引入drizzle与schema——类型导入在编译期会被擦除,不会产生任何运行时加载; - 用
Proxy提供惰性db对象,任何属性访问都返回一个 resolve 为空数组的 Promise 函数,模拟查询方法的形状; - mock 文件必须以 JavaScript/ESM 的命名导出方式导出与原始模块同名的导出(模块 mock 文档 的 automocking 章节对此有完整约束说明)。
为什么这里必须用__mocks__文件而不是 automocking
sb.mock的 automocking 机制默认会把原模块的导出替换为 Vitest mock 函数,但原模块本身及其依赖仍然会被求值。对于导入postgres、pg这类纯 Node.js 包的模块,一旦模块被求值就会在浏览器中崩溃。
因此,TanStack React 框架文档 明确指出:__mocks__文件是唯一能完全阻止原始模块及其依赖链被求值的方案。sb.mock在解析时会优先在对应目录下查找__mocks__文件,命中后直接加载 mock 文件,原始模块永远不会进入浏览器。
该机制底层如何工作
从 preset.ts 的实现可以看到,@storybook/tanstack-react的viteFinal会注入三个关键 Vite 插件:
serverCodeEliminationPlugin:在构建期消除服务端专用代码;serverOnlyStubPlugin:对服务端专用模块提供桩(stub);moduleInterceptionPlugin:把@tanstack/react-router等模块的导入重定向到@storybook/tanstack-react内置的 mock 层(export-mocks 目录)。
而sb.mock本身建立在 Storybook 的 automocking 机制之上(基于 Vitest mocking 引擎):所有 mock 决策在构建期完成,没有运行时开销;mock 的注册只允许出现在项目级.storybook/preview.*文件中,不能在单个 story 文件中注册。
定位"该 mock 谁"的排查方法
如果浏览器仍报错,可按以下规则定位目标模块(框架文档):
- mock 服务端模块本身,而不是使用它的组件或路由。例如
Dashboard.tsx→~/auth/session→~/db/client→postgres,应 mock 离postgres最近的、且由你控制的模块(这里是~/db/client); - 从报错堆栈顶部往下走,停在第一个你自己编写的 import处,为它添加
__mocks__文件; - 以下两种情况无需mock:模块来自
@tanstack/*(已被框架 preset 处理,请确保使用最新版@storybook/tanstack-react);模块只导入createServerFn(已被自动 mock,报错应来自同一文件中的其他 import)。
使用场景与后续扩展
完成上述两步后,TanStack Start 的组件(包括依赖服务端函数的页面)即可在 Storybook 中直接渲染。在此基础上你还可以:
- 在 story 中用标准 mock API 覆写被自动 mock 的
createServerFn().handler(...),分别呈现 loading、success、error 状态(详见 TanStack 框架文档); - 结合 Storybook 的模块 mock 全量能力(spy-only、fully automocked、mock files 三种注册方式与 Vitest mock 方法表),见 Mocking modules 文档。
参考
- 框架文档:docs/get-started/frameworks/tanstack-react.mdx
- 模块 mock 总览:docs/writing-stories/mocking-data-and-modules/mocking-modules.mdx
- 框架源码:code/frameworks/tanstack-react/src/index.ts、code/frameworks/tanstack-react/src/preset.ts
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考