首页
/ Storybook for Next.js 开启 App Router 支持:`nextjs.appDirectory` 参数深度解析与实战配置

Storybook for Next.js 开启 App Router 支持:`nextjs.appDirectory` 参数深度解析与实战配置

2026-09-08 22:07:28作者:丁柯新Fawn

本文聚焦 Storybook 官方 Next.js 框架(@storybook/nextjs / @storybook/nextjs-vite)中用于启用 App Router 能力的核心参数 nextjs.appDirectory。当你的 Story 导入的组件内部调用了 next/navigation 的 hook(如 useRouterusePathnameuseSelectedLayoutSegmentuseParams)时,必须把该参数设为 true,框架才会用 App Router 上下文包裹并模拟 next/navigation,否则这些依赖在 Story 渲染时会直接报错。读完本文,你将掌握该参数在单条 Story、组件级 Meta、全局 Preview 三个作用域的配置方法,理解其底层 Router/导航 Mock 的实现机制,并能配合 nextjs.navigation 精确模拟路由段与动态参数。

为什么需要 nextjs.appDirectory: true

Next.js 应用存在两种路由模型,二者对应的 Hooks 与 Context 完全不同,且互不通用:

  • Pages Router:使用 next/routeruseRouter(),对应 pages/ 目录;
  • App Router:使用 next/navigationuseRouterusePathnameuseParamsuseSelectedLayoutSegment(s) 等),只能在 app/ 目录下使用。

Storybook 的 Next.js 框架默认会对 next/router 做桩(stub)处理,自动注入一套模拟的 Page Router 上下文,因此 Pages Router 下的组件可以「零配置」直接渲染。但 next/navigation 不在默认模拟范围内——它依赖 App Router 特有的多层 React Context(AppRouterContextPathnameContextSearchParamsContextPathParamsContext 等)。

从源码类型定义即可看到该参数的定位(code/frameworks/nextjs/src/types.ts):

export interface NextJsParameters {
  nextjs?: {
    /**
     * 如果你的 Story 导入了使用 next/navigation 的组件,需要将该参数设为 true
     */
    appDirectory?: boolean;

    /** 使用 next/navigation 时的导航配置(仅适用于 app 目录下的组件/页面) */
    navigation?: Partial<NextRouter>;

    /** next/router 的路由器配置 */
    router?: Partial<NextRouter>;

    /** 传递给每个 next/image 实例的 props */
    image?: Partial<NextImage.ImageProps>;
  };
}

因此判断是否需要它的标准非常简单:只要 Story 渲染链路中的某个组件 import 并调用了 next/navigation,就必须开启 nextjs.appDirectory。官方推荐先看「框架是否直接支持」——@storybook/nextjs-vite 因构建更快、测试特性支持更好,被官方文档建议作为大多数 Next.js 项目的首选框架(docs/get-started/frameworks/nextjs.mdx);而 Webpack 版 @storybook/nextjs 仅在存在与 Vite 不兼容的自定义 Webpack/Babel 配置时才需选用。无论选择哪一个,本文讨论的 appDirectory 参数在两个框架中行为一致(@storybook/nextjs-viterouting/decorator.tsxpreview.tsxtypes.ts 中均有同名实现)。

配置方式一:在组件 Meta 中开启(本文核心)

最精确的做法是只对「包含导航依赖组件」的 Stories 文件开启,避免影响其他使用 Page Router 的 Stories。原文档示例在 docs/_snippets/nextjs-app-directory-in-meta.md 中提供了 CSF 3 与 CSF Next 两种书写范式。

CSF 3(TypeScript,推荐类型安全的写法)

// Replace your-framework with nextjs or nextjs-vite
import type { Meta, StoryObj } from '@storybook/your-framework';

import NavigationBasedComponent from './NavigationBasedComponent';

const meta = {
  component: NavigationBasedComponent,
  parameters: {
    nextjs: {
      appDirectory: true, // 👈 Set this
    },
  },
} satisfies Meta<typeof NavigationBasedComponent>;
export default meta;

CSF 3(JavaScript 等价写法)

import NavigationBasedComponent from './NavigationBasedComponent';

export default {
  component: NavigationBasedComponent,
  parameters: {
    nextjs: {
      appDirectory: true, // 👈 Set this
    },
  },
};

CSF Next(实验性新语法):如果你在 .storybook/preview 中通过 preview.meta(...) 声明 Meta,则可写为(TypeScript 与 JavaScript 两种形态结构一致):

import preview from '../.storybook/preview';

import NavigationBasedComponent from './NavigationBasedComponent';

const meta = preview.meta({
  component: NavigationBasedComponent,
  parameters: {
    nextjs: {
      appDirectory: true, // 👈 Set this
    },
  },
});

说明:nextjs 是标准的 Storybook Parameters 命名空间,遵守参数继承规则。CSF 3 与 CSF Next 两种范式只是在 Meta 声明方式上不同,parameters.nextjs.appDirectory 的取值与生效语义完全一致。

配置方式二:在全局 Preview 中开启

若整个 Next.js 项目所有页面都位于 app/ 目录(即不存在 pages/ 目录),则无需在每个 Stories 文件里重复声明,可在 .storybook/preview 中一次性对全部 Story 生效,详见官方导航章节(docs/get-started/frameworks/nextjs.mdx)。

对应片段 docs/_snippets/nextjs-app-directory-in-preview.md 提供以下写法(TS / JS / CSF Next 的 definePreview 变体在此省略 JS 与重复形式):

// Replace your-framework with nextjs or nextjs-vite
import type { Preview } from '@storybook/your-framework';

const preview: Preview = {
  // ...
  parameters: {
    // ...
    nextjs: {
      appDirectory: true,
    },
  },
};

export default preview;

CSF Next 范式下使用 definePreview

// Replace your-framework with nextjs or nextjs-vite
import { definePreview } from '@storybook/your-framework';

export default definePreview({
  // ...
  parameters: {
    // ...
    nextjs: {
      appDirectory: true,
    },
  },
});

同理,由于 Parameters 支持三层继承(全局 Project Parameters组件级 Meta Parameters单条 Story Parameters),你还可以只在某一个组件(如 meta.parameters)或某一条 Story 上开启——覆盖面越小,对使用 Pages Router 的其他 Stories 影响越小。类型层面,上述 NextJsParameters 中的 appDirectory?: boolean 默认值为 false(见 docs/get-started/frameworks/nextjs.mdx)。

源码视角:appDirectory 究竟改变了什么

开启该参数后,框架的装饰器与加载器会做两件事:换掉路由 Provider、换掉被 Mock 的导航模块来源

1. 装饰器选择 App Router 还是 Page Router 上下文

code/frameworks/nextjs/src/routing/decorator.tsx 中,RouterDecorator 依据该参数进行分支:

export const RouterDecorator = (Story, { parameters }) => {
  const nextAppDirectory =
    (parameters.nextjs?.appDirectory as NextAppDirectory | undefined) ?? false;

  if (nextAppDirectory) {
    if (!AppRouterProvider) return null;
    return (
      <AppRouterProvider
        routeParams={{ ...defaultRouterParams, ...parameters.nextjs?.navigation }}
      >
        <RedirectBoundary>
          <Story />
        </RedirectBoundary>
      </AppRouterProvider>
    );
  }

  return (
    <PageRouterProvider>
      <Story />
    </PageRouterProvider>
  );
};

也就是说:为 true 时,Story 被 AppRouterProvider 包裹(并额外包上 Next.js 的 RedirectBoundary,用于兼容 App Router 场景下的 redirect() 逻辑);为默认的 false 时,则退回到 PageRouterProvider(面向 next/router)。这正是模拟的 Context 来源不同的根因。

2. Loader 决定 Mock 哪个路由模块

框架的 Loader 同样以该参数为开关(code/frameworks/nextjs/src/preview.tsx):

export const loaders: Addon_LoaderFunction = async ({ globals, parameters }) => {
  const { router, appDirectory } = parameters.nextjs ?? {};
  if (appDirectory) {
    createNavigation(router); // Mock next/navigation(App Router)
  } else {
    createRouter({ locale: globals.locale, ...router }); // Mock next/router(Pages Router)
  }
};

可以看到:开启 appDirectory 后调用的是 createNavigation(...),把 next/navigation 的导出替换为可从测试/play function 中断言的 Mock;关闭时则用 createRouter(...) 模拟 next/router,并把全局的 locale(来自 Globals,常配合 Toolbar 使用)合并进 router 对象。

3. App Router Provider 实际注入了哪些 Context

进一步看 code/frameworks/nextjs/src/routing/app-router-provider.tsxAppRouterProvider 会根据 routeParams 提供一套与 Next.js 官方对齐的 Provider 栈:

  • PathParamsContext.Provider:把 segments 中形如 ["slug", "hello"] 的键值对解析为动态路由参数对象;
  • PathnameContext.Provider:提供当前 pathname(默认 /);
  • SearchParamsContext.Provider:用 new URLSearchParams(query) 提供 searchParams
  • GlobalLayoutRouterContext.ProviderLayoutRouterContext.Provider:构造一份 Flight Router State tree(来自 pathname + segments),支撑 Layout 相关上下文;
  • AppRouterContext.Provider:value 来自 getRouter(),即 @storybook/nextjs/navigation.mock 暴露的 Mock router 对象。

理解这层实现,就能解释为什么仅仅「开着参数」还不够——usePathname() 读到的是 Provider 里写入的默认 pathname: '/'useParams() 读到的是空对象。若组件依赖真实路由值,还要配合下一节的 navigation 参数。

配套导航模拟:nextjs.navigation 与默认值

开启 appDirectory 后,你通常还需要控制路由值。框架支持在每个层级通过 nextjs.navigation 参数做浅合并覆盖(docs/get-started/frameworks/nextjs.mdx)。

该参数的类型结构为:

{
  asPath?: string;
  pathname?: string;
  query?: Record<string, string>;
  segments?: (string | [string, string])[];
}
  • 默认未设置时,框架使用的默认导航上下文为 { pathname: '/', query: {} }nextjs.navigation.segments 的默认值为 []
  • push()replace()useRouter() 方法会被替换为可被 Mock API 断言与操纵的 mock 函数。

例如在 Meta 中通过 segments 模拟 layout segment(完整示例见 docs/_snippets/nextjs-navigation-segments-override-in-meta.md):

// NavigationBasedComponent 将读到:
//   useSelectedLayoutSegment()  -> 'dashboard'
//   useSelectedLayoutSegments() -> ['dashboard', 'analytics']
//   useParams()                 -> {}
parameters: {
  nextjs: {
    appDirectory: true,
    navigation: {
      segments: ['dashboard', 'analytics'],
    },
  },
}

若组件用到了 useParams(),需把 segments 元素写成 [key, value] 二元组(见 docs/_snippets/nextjs-navigation-segments-for-use-params-override-in-meta.md):

// 此时:
//   useSelectedLayoutSegment()  -> 'hello'
//   useSelectedLayoutSegments() -> ['hello', 'nextjs']
//   useParams()                 -> { slug: 'hello', framework: 'nextjs' }
parameters: {
  nextjs: {
    appDirectory: true,
    navigation: {
      segments: [
        ['slug', 'hello'],
        ['framework', 'nextjs'],
      ],
    },
  },
}

在源码层面,AppRouterProviderpathParams 正是这样解析二元组 segments 的(code/frameworks/nextjs/src/routing/app-router-provider.tsx):仅当元素是长度为 2 的数组且首项为字符串时,才把 [key, value] 写入 params。

同时框架还提供 @storybook/nextjs/navigation.mock 这一具名 Mock 模块,便于在 story 的 play function 中直接断言导航调用;其类型为 typeof import('next/navigation') & { getRouter(): ... }getRouter() 返回可操纵的 Mock router 对象(docs/get-started/frameworks/nextjs.mdx)。

常见误区与建议

  • 不要混淆 nextjs.routernextjs.navigationrouter 作用于 Page Router 的 next/router(此时无需 appDirectory),而 navigation 只对 App Router 的 next/navigation 生效,前提是 appDirectory: true。两者类型都接近 Partial<NextRouter>,但作用于完全不同的 Context 树。
  • Story 同时包含两类组件怎么办:App Router 与 Page Router 上下文互斥,Storybook 会在一个 Preview 中二选一注入。若必须同屏渲染依赖不同路由模型的组件,建议把它们拆到不同的 Stories 中,分别用对应作用域的参数声明,再通过 Composition 等方式组合展示。
  • 全局开启需谨慎:把 appDirectory 写进 .storybook/preview 会对项目内全部 Stories 生效。若项目仍保留少量 pages/ 目录下的页面/组件 Story,优先采用 Meta 级或 Story 级覆盖,保持参数继承语义清晰(详情可查阅 Parameters 继承文档)。
  • 类型提示:使用 TypeScript 时,建议 import 自 @storybook/nextjs@storybook/nextjs-vite(即替换代码注释中的 your-framework),Meta/StoryObj/Preview 泛型会自动带上 nextjs.appDirectory 的完整类型约束。

小结

nextjs.appDirectory 是 Storybook Next.js 框架中「一套参数切换两套路由运行时」的关键开关:为 true 时,Story 由 AppRouterProvider 包裹、Loader 改为 Mock next/navigation,使依赖 App Router Hooks 的组件可在隔离环境中稳定渲染并接受测试断言;为默认 false 时则走 next/router 的 Page Router 模拟。结合 nextjs.navigation.segments 等配套参数,你可以在 Story 中精确复现任意路径、layout segment 与动态参数,让组件测试与真实 Next.js 运行时的行为保持一致。需要继续探索时,可阅读 Storybook for Next.js (Webpack) 官方指南Next.js Vite 指南 的 Navigation 章节,或直接查看上述源码文件中的实现细节。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395