Storybook Next.js 框架指南:在 preview 中覆盖默认 Router 并接管路由 Mock 行为
2026/9/8 20:03:02 网站建设 项目流程

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()接管pushreplace等路由方法的 Mock 实现。读完本文,你能掌握从“全局参数覆盖路由默认值”到“逐故事细粒度覆盖”的完整配置路径,并能结合仓库源码理解覆盖值是如何被合并进 Mock 单例路由的。

一、背景:Storybook 如何 Stub Next.js 的 Router

在 Storybook 中渲染依赖路由的 Next.js 组件(例如通过useRouter()读取pathnamequery,或点击链接触发push)时,真实的路由环境并不存在。Storybook 的 Next.js 框架 preset 会自动为next/router创建一个 Stub:

  • 路由的状态字段pathnameasPathquery等)使用一套确定的默认值;
  • 路由的方法pushreplacereloadbackforwardprefetchbeforePopState,以及events.on/off/emit)全部是 Mock 函数,组件与路由的每次交互都会自动记录到 Actions 面板,可以直接观察;
  • 方法 Mock 基于 Storybook 的storybook/test提供的fn构造,因此支持所有标准 Mock API(mockImplementationmock.callsexpect(...).toHaveBeenCalled等)。

源码层面的证据在 router Mock 工厂:defaultRouterState定义了默认路由状态,createRouter为每个动作方法生成带mockName的 Mock,并把结果写回next/dist/client/router.js的单例路由(见 singletonRouter 覆写逻辑)。

需要区分两种路由体系(详见 Next.js 框架文档的 routing 章节):

场景使用模块覆盖入口
pages目录组件next/routerparameters.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(本文核心配置)

有两类覆盖需求:

  1. 覆盖路由属性(状态值),例如把basePath改成/app/
  2. 接管路由方法的行为,例如改写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),其余字段(asPathisReady等)仍沿用默认值。

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.tsxparameters.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(...)是更常见的运行时接管方式,因为它在每个故事执行前生效,便于按故事动态调整。

五、验证与排查要点

  1. 观察路由交互:在故事页点击触发push/replace的链接,Actions 面板会显示形如next/router::useRouter().push的调用事件——这正是各 Mock 的mockName(见 mockName 列表),可用于在 Actions 中过滤。
  2. 断言路由行为:由于方法是标准 Mock,可以在play函数或组件测试中写expect(getRouter().push).toHaveBeenCalledWith('/app/login')
  3. getRouter()抛错:若在appDirectory: true的故事中调用getRouter(),会因 page router Mock 未创建而抛出NextjsRouterMocksNotAvailable,提示你使用的是next/navigation体系,应改用对应的 navigation Mock。
  4. 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),仅供参考

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

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

立即咨询