Storybook Next.js 框架指南:在 preview 中覆盖默认 Router 并接管路由 Mock 行为
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
本文讲解 Storybook 的@storybook/nextjs/@storybook/nextjs-vite框架如何为next/router提供自动 Stub,以及如何通过.storybook/preview.tsx中的parameters.nextjs.router覆盖默认路由状态(如basePath)、在beforeEach中用getRouter()接管push、replace等路由方法的 Mock 实现。读完本文,你能掌握从“全局参数覆盖路由默认值”到“逐故事细粒度覆盖”的完整配置路径,并能结合仓库源码理解覆盖值是如何被合并进 Mock 单例路由的。
一、背景:Storybook 如何 Stub Next.js 的 Router
在 Storybook 中渲染依赖路由的 Next.js 组件(例如通过useRouter()读取pathname、query,或点击链接触发push)时,真实的路由环境并不存在。Storybook 的 Next.js 框架 preset 会自动为next/router创建一个 Stub:
- 路由的状态字段(
pathname、asPath、query等)使用一套确定的默认值; - 路由的方法(
push、replace、reload、back、forward、prefetch、beforePopState,以及events.on/off/emit)全部是 Mock 函数,组件与路由的每次交互都会自动记录到 Actions 面板,可以直接观察; - 方法 Mock 基于 Storybook 的
storybook/test提供的fn构造,因此支持所有标准 Mock API(mockImplementation、mock.calls、expect(...).toHaveBeenCalled等)。
源码层面的证据在 router Mock 工厂:defaultRouterState定义了默认路由状态,createRouter为每个动作方法生成带mockName的 Mock,并把结果写回next/dist/client/router.js的单例路由(见 singletonRouter 覆写逻辑)。
需要区分两种路由体系(详见 Next.js 框架文档的 routing 章节):
| 场景 | 使用模块 | 覆盖入口 |
|---|---|---|
pages目录组件 | next/router | parameters.nextjs.router(本文主题) |
app目录组件 | next/navigation | 需先设置nextjs.appDirectory: true,再通过parameters.nextjs.navigation覆盖 |
二、默认 Router 的初始状态
Stubbed router 的默认值如下(locale特别说明见下):
// Default router const defaultRouter = { // The locale should be configured globally: https://storybook.js.org/docs/essentials/toolbars-and-globals#globals locale: globals?.locale, asPath: '/', basePath: '/', isFallback: false, isLocaleDomain: false, isReady: true, isPreview: false, route: '/', pathname: '/', query: {}, };这与源码中的 defaultRouterState 一一对应,唯一的差异点是locale:它不写在默认对象里,而是由框架在构造路由时从Storybook 的 globals注入。也就是说,若你在 Storybook 的 globals 中配置了locale,多语言项目的router.locale会自动跟随,无需每个故事单独设置。
另外,router对象包含push()、replace()等全部原始方法,且它们都是可用常规 Mock API 操作和断言的 Mock 函数。
三、在 preview 中覆盖默认 Router(本文核心配置)
有两类覆盖需求:
- 覆盖路由属性(状态值),例如把
basePath改成/app/; - 接管路由方法的行为,例如改写
push的执行逻辑。
前者用parameters,后者用beforeEach。
3.1 TypeScript 写法(preview.tsx)
在.storybook/preview.tsx中同时完成两类覆盖:
// Replace your-framework with nextjs or nextjs-vite import { getRouter } from '@storybook/nextjs/router.mock'; export default { parameters: { nextjs: { // 👇 Override the default router properties router: { basePath: '/app/', }, }, }, async beforeEach() { // 👇 Manipulate the default router method mocks getRouter().push.mockImplementation(() => { /* ... */ }); }, };注意源码中loaders的取值链路(见 preview loaders 实现):框架会读取parameters.nextjs.router,并以createRouter({ locale: globals.locale, ...router })的方式构造路由——框架把你写在参数里的内容浅合并(shallow merge)进默认路由。因此参数里只需写想覆盖的字段(如basePath),其余字段(asPath、isReady等)仍沿用默认值。
getRouter()是 Mock 模块导出的访问器(见 getRouter 实现):它返回当前已创建的路由 Mock 单例;若路由尚未创建(比如 app 目录模式下未调用createNavigation前误用),会抛出NextjsRouterMocksNotAvailable错误提示你导入了不适用的 Mock。
3.2 JavaScript 写法(preview.jsx)
// Replace your-framework with nextjs or nextjs-vite import type { Preview } from '@storybook/nextjs'; // 👇 Must include the `.mock` portion of filename to have mocks typed correctly import { getRouter } from '@storybook/nextjs/router.mock'; const preview: Preview = { parameters: { nextjs: { // 👇 Override the default router properties router: { basePath: '/app/', }, }, }, async beforeEach() { // 👇 Manipulate the default router method mocks getRouter().push.mockImplementation(() => { /* ... */ }); }, }; export default preview;注释强调了一点关键实践:导入路径必须包含.mock后缀(@storybook/nextjs/router.mock),这样 Mock 才能拿到正确的类型定义。
3.3 CSF Next 实验语法(definePreview)
如果使用 CSF Next 实验特性,同一套覆盖逻辑可以改用definePreview书写:
import { definePreview } from '@storybook/nextjs'; // 👇 Must include the `.mock` portion of filename to have mocks typed correctly import { getRouter } from '@storybook/nextjs/router.mock'; const preview = definePreview({ parameters: { nextjs: { router: { basePath: '/app/', }, }, }, async beforeEach() { getRouter().push.mockImplementation(() => { /* ... */ }); }, }); export default preview;对应的definePreview+.jsx变体写法与之相同,仅将import type改为运行时导入,其余结构不变。
适用前提:以上示例基于
pages目录 +next/router场景(@storybook/nextjs使用 webpack 构建,@storybook/nextjs-vite使用 Vite 构建,只需替换包名)。若你的组件位于app目录、使用next/navigation,则应改用parameters.nextjs.navigation覆盖,并先设置nextjs.appDirectory: true(见 Next.js navigation 章节)。
四、覆盖的作用域与参数继承
preview 级的覆盖只是作用域之一。nextjs.router遵循 Storybook 的标准参数继承规则(见 Parameters 文档):
| 作用域 | 配置位置 | 适用场景 |
|---|---|---|
| 项目级 | .storybook/preview.tsx的parameters.nextjs.router | 全站统一的路由环境,如固定的basePath |
| 组件级 | 故事文件默认导出的parameters.nextjs.router | 某组件的所有故事共享同一套路由 |
| 故事级 | 单个故事的parameters.nextjs.router | 仅某个故事需要特殊路由(如/profile/[id]+query: { id: '1' }) |
故事级覆盖的示例(完整片段见 nextjs-router-override-in-story.md):
import type { Meta, StoryObj } from '@storybook/nextjs'; import RouterBasedComponent from './RouterBasedComponent'; const meta = { component: RouterBasedComponent, } satisfies Meta<typeof RouterBasedComponent>; export default meta; type Story = StoryObj<typeof meta>; // Interact with the links to see the route change events in the Actions panel. export const Example: Story = { parameters: { nextjs: { router: { pathname: '/profile/[id]', asPath: '/profile/1', query: { id: '1', }, }, }, }, };从源码结构看,createRouter的合并顺序是defaultRouterState → overrides → routerActions(见 合并逻辑),即:默认状态打底,参数覆盖项在上层展开,方法 Mock 始终保留。由于是浅合并,query这类对象字段整体替换而非深度合并——故事级query: { id: '1' }会完全覆盖默认的空query,这正符合“按故事精确控制路由”的预期。
此外,createRouter还支持传入函数形式的动作覆盖(如用overrides.push替换默认push实现,见 overrides 处理);而在 preview 中通过beforeEach+getRouter().push.mockImplementation(...)是更常见的运行时接管方式,因为它在每个故事执行前生效,便于按故事动态调整。
五、验证与排查要点
- 观察路由交互:在故事页点击触发
push/replace的链接,Actions 面板会显示形如next/router::useRouter().push的调用事件——这正是各 Mock 的mockName(见 mockName 列表),可用于在 Actions 中过滤。 - 断言路由行为:由于方法是标准 Mock,可以在
play函数或组件测试中写expect(getRouter().push).toHaveBeenCalledWith('/app/login')。 getRouter()抛错:若在appDirectory: true的故事中调用getRouter(),会因 page router Mock 未创建而抛出NextjsRouterMocksNotAvailable,提示你使用的是next/navigation体系,应改用对应的 navigation Mock。basePath覆盖的影响:框架在 装饰器 中根据parameters.nextjs?.appDirectory决定注入PageRouterProvider还是AppRouterProvider,page 路由分支下parameters.nextjs.router的每个字段都会进入 Mock 路由,直接影响useRouter()返回值。
六、小结
parameters.nextjs.router:在 preview / meta / 故事三级作用域上浅合并覆盖默认路由状态,locale自动来自 globals;getRouter()(从@storybook/nextjs/router.mock导入,务必带.mock后缀):在beforeEach中接管push等方法 Mock 的行为;- 覆盖仅作用于
pages目录的next/router场景;app目录请使用nextjs.appDirectory+nextjs.navigation组合。
相关延伸阅读:Next.js 框架总览、Next.js (Vite) 框架总览、router Mock 源码、框架 preview 入口。
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考