首页
/ Storybook @storybook/nextjs-vite 框架源码解析:用 Vite 构建器在 Storybook 中隔离开发 Next.js 组件

Storybook @storybook/nextjs-vite 框架源码解析:用 Vite 构建器在 Storybook 中隔离开发 Next.js 组件

2026-09-06 17:10:52作者:冯梦姬Eddie

本文围绕 Storybook 仓库中 @storybook/nextjs-vite 框架包(位于 code/frameworks/nextjs-vite)展开,讲解它如何让你以 Vite 构建器运行 Next.js 组件的 Storybook,涵盖安装配置、frameworkOptionsparameters.nextjs 的完整参数说明,并结合预设(preset)、装饰器(decorator)与路由 mock 的源码实现,剖析 Router/Navigation/Image/Styled-JSX 等 Next.js 特性在浏览器预览环境中的工作原理。读完本文,你既能完成从零接入的实操配置,也能理解框架底层的 mock 注入机制与版本兼容策略。

框架定位:Next.js 的 Vite 构建器版本

Storybook 的 @storybook/nextjs-vite 是一个 framework 包,用于在 Next.js 项目中以 Vite 作为 Storybook 构建器来开发、文档化和测试 UI 组件。它在 官方文档 中提供安装说明、使用示例与 API 参考(见 code/frameworks/nextjs-vite/README.md)。

code/frameworks/nextjs-vite/package.json 可以确认该包的工程事实:

  • 包名为 @storybook/nextjs-vite,描述为 “Storybook for Next.js and Vite: Develop, document, and test UI components in isolation”;
  • 运行时依赖为 workspace 内的 @storybook/builder-vite@storybook/react@storybook/react-vite,以及 styled-jsx(固定 5.1.6)与 vite-plugin-storybook-nextjs。这说明它的实现路径是:React 渲染器 + Vite 构建器 + Next.js 专用 Vite 插件,而非走 Webpack;
  • peerDependencies 明确了版本兼容边界:next 支持 ^14.1.0 || ^15.0.0 || ^16.0.0vite 支持 ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0react/react-dom 支持 ^16.8.0^19.0.0。如果你的 Next.js 或 Vite 版本落在这些区间之外,需要以当前仓库声明为准进行升级或降级。

安装与框架切换

仓库文档片段 docs/_snippets/nextjs-vite-install.md 给出了三种包管理器的安装命令:

npm install --save-dev @storybook/nextjs-vite
pnpm add --save-dev @storybook/nextjs-vite
yarn add --dev @storybook/nextjs-vite

随后在 .storybook/main.ts 中将 framework 指向 @storybook/nextjs-vitedocs/_snippets/nextjs-vite-add-framework.md 提供了 CSF 3 与 CSF Next 两种配置风格的切换 diff(例如从 @storybook/react-webpack5 切换到 @storybook/nextjs-vite)。以 TypeScript 配置为例:

// .storybook/main.ts
import { defineMain } from '@storybook/nextjs-vite/node';

export default defineMain({
  framework: '@storybook/nextjs-vite',
  // stories、addons 等其余配置保持不变
});

其中 defineMain 来自包的 ./node 子路径导出。查看 code/frameworks/nextjs-vite/src/node/index.ts 可知,它的实现极其轻量——只是对 StorybookConfig 类型做断言后原样返回,其价值在于为 main.ts 提供 framework: '@storybook/nextjs-vite' 的字面量类型约束(类型定义见 code/frameworks/nextjs-vite/src/types.ts 中的 StorybookConfig)。

frameworkOptions 配置项

FrameworkOptions 同样定义在 code/frameworks/nextjs-vite/src/types.ts

export type FrameworkOptions = {
  /** The path to the Next.js configuration file. */
  nextConfigPath?: string;
  image?: {
    includeFiles?: string[];
    excludeFiles?: string[];
  };
  builder?: BuilderOptions;
};

各字段含义与底层处理逻辑如下:

  • nextConfigPath:Next.js 配置文件(如 next.config.js)的路径。在 src/preset.tsviteFinal 中,预设通过 options.presets.apply('frameworkOptions') 读取该选项,并用 dirname(nextConfigPath) 推导出 Next.js 根目录(nextDir),最终传给 vitePluginStorybookNextjs 插件的 dir 选项——也就是说,该目录决定了插件在项目中定位 Next.js 相关文件的位置;
  • image.includeFiles / image.excludeFiles:控制哪些图片文件需要走 Next.js 的 next/image 优化管线。这一配置被原样透传给 vitePluginStorybookNextjs(vitePluginOptions),与官方文档中的 image 配置说明对应;
  • builder:透传给 @storybook/builder-vite 的构建器选项。

预设(preset):框架装配的核心

code/frameworks/nextjs-vite/src/preset.ts 是理解整个框架如何工作的入口。它导出了四个关键部分:

core:固定构建器与渲染器

export const core: PresetProperty<'core'> = {
  builder: import.meta.resolve('@storybook/builder-vite'),
  renderer: import.meta.resolve('@storybook/react/preset'),
};

这意味着使用该框架时,Storybook 的构建器被锁定为 @storybook/builder-vite、渲染器为 @storybook/react——用户即使写了其他 core.builder 也会被框架覆盖。

previewAnnotations:按 Next.js 版本注入预览注解

const nextjsVersion = getNextjsVersion();
const isNext16orNewer = semver.gte(nextjsVersion, '16.0.0');

if (!isNext16orNewer) {
  annotations.push(fileURLToPath(import.meta.resolve('@storybook/nextjs-vite/config/preview')));
}

这里可以推断出一个版本分叉:getNextjsVersion()(定义在 src/utils.ts)通过解析已安装 next 包的 package.json 取得版本号;对于低于 16 的 Next.js 版本,会额外注入 src/config/preview.ts,其内容是调用 setConfig(process.env.__NEXT_RUNTIME_CONFIG)——即恢复旧版 Next.js 依赖的运行时配置全局注入。注释 TODO: Remove this once we only support Next.js v16 and above 表明这是为兼容旧版本保留的过渡逻辑。

optimizeViteDeps:依赖预构建白名单

预设导出了 optimizeViteDeps 数组,包含 @storybook/nextjs-vite/link.mocknavigation.mockrouter.mockstyled-jsx 等条目。这些正是包的子路径导出(见 package.json 的 exports 字段),预构建它们可以避免 Vite 启动时的重复优化。

viteFinal:叠加 React 预设并挂入 Next.js 插件

viteFinal 的执行顺序是:

  1. 先调用 @storybook/react-vite/presetreactViteFinal(config, options),得到完整的 React + Vite 基础配置;
  2. 调用 normalizePostCssConfig(searchPath)(实现于 src/find-postcss-config.ts)处理项目内 PostCSS 配置,使其能在 Vite 中正确加载;
  3. 合并 resolve.alias,将 styled-jsxstyled-jsx/stylestyled-jsx/style.js 全部指向依赖包中的具体文件路径——这是为了让 Next.js 组件中的 styled-jsx 导入在浏览器端预览时可用;
  4. 最后把 vitePluginStorybookNextjs({ image, dir: nextDir }) 追加到 plugins 数组末尾,负责 next/image 等 Next.js 特有能力。

预览装饰器:让 Next.js 组件在浏览器里“跑得起来”

包的 ./preview 导出指向 src/preview.tsx,它由预设自动注入到预览环境(见上文 previewAnnotations)。文件顶部有三个值得注意的全局补丁:

  1. next-head-count meta 标签addNextHeadCount() 手动向 document.head 追加 <meta name="next-head-count" content="0">,因为 Next.js 的 Head 组件依赖该标签来追踪自己插入的节点,而 Storybook 的宿主页面中并不存在;
  2. 错误过滤:对 console.error 打补丁,吞掉 isNextRouterError(来自 next/dist/client/components/is-next-router-error.js)以及异步客户端组件相关的已知报错(如 “Only Server Components can be async at the moment.”),避免 Next.js 在无 SSR 环境下的噪音输出;
  3. 装饰器链
export const decorators: Addon_DecoratorFunction<ReactRenderer>[] = [
  asDecorator(StyledJsxDecorator),
  asDecorator(ImageDecorator),
  asDecorator(RouterDecorator),
  asDecorator(HeadManagerDecorator),
];

四个装饰器分别对应源码目录中的四块实现:

  • StyledJsxDecoratorsrc/styledJsx/decorator.tsx):让 <style jsx> 样式在 Storybook 中正确挂载;
  • ImageDecoratorsrc/images/decorator.tsx):配合 vite-plugin-storybook-nextjs 处理 next/image 的渲染;
  • RouterDecoratorsrc/routing/decorator.tsx):根据 parameters.nextjs.appDirectory 决定包裹 AppRouterProvider(App Router 场景,并额外包一层 Next 的 RedirectBoundary 以支持 redirect())还是 PageRouterProvider(Pages Router 场景);
  • HeadManagerDecoratorsrc/head-manager/decorator.tsx):模拟 Next.js 的 Head 管理逻辑。

loaders 与路由 mock 的初始化时机

preview.tsx 还导出了一个 loader,它是路由 mock 的“点火器”:

export const loaders: LoaderFunction<ReactRenderer> = async ({ globals, parameters }) => {
  const { router, appDirectory } = parameters.nextjs ?? {};
  if (appDirectory) {
    createNavigation(router);
  } else {
    createRouter({ locale: globals.locale, ...(router as Record<string, unknown>) });
  }
};

即在故事运行前,根据 appDirectory 参数选择创建 App Router 或 Pages Router 的 mock。两个 mock 的实现分别在 src/export-mocks/navigation/index.tssrc/export-mocks/router/index.ts,设计上有三点共性:

  • 可 spy 的 mockpushreplaceback 等导航动作全部用 storybook/testfn() 包装并赋予 mockName(如 next/navigation::useRouter().push),因此你可以直接在 play 函数中用 expect(useRouter().push).toHaveBeenCalledWith(...) 做交互测试;
  • overrides 支持createRouter(overrides) 允许用户传入 Partial<NextRouter>,指定的动作会被替换为“调用用户函数再记录”的包装实现,状态字段(pathnamequery 等)则直接合并进默认路由状态;
  • 模块级再导出:mock 文件同时 export * from 'next/dist/client/router.js' / next/dist/client/components/navigation.js,保持与真实模块 API 的一致,并在 mock 未初始化时通过 getRouter() 抛出 NextjsRouterMocksNotAvailable 错误提示。

这些子模块通过 package.json 的 exports 映射为公开子路径:@storybook/nextjs-vite/router.mocknavigation.mocklink.mockheaders.mocksrc/export-mocks/headers/index.ts 提供 headers/cookies mock)与 cache.mocknext/link 的 mock 甚至附带了测试用例 src/export-mocks/link/index.test.tsx,可以在本仓库中查看 Link mock 对 href/locale 等属性的具体模拟行为。

parameters.nextjs:预览侧的三个开关

NextJsParameters 类型(code/frameworks/nextjs-vite/src/types.ts)定义了预览侧的全部参数:

export interface NextJsParameters {
  nextjs?: {
    /** 启用 App Directory 特性;若故事导入了使用 next/navigation 的组件需设为 true */
    appDirectory?: boolean;
    /** next/navigation 的导航配置,仅适用于 app 目录下的组件/页面 */
    navigation?: Partial<NextRouter>;
    /** next/router 的路由配置 */
    router?: Partial<NextRouter>;
  };
}

结合 src/routing/decorator.tsx 与 loader 的实现,可以给出明确的语义边界:

  • appDirectory: true 时,装饰器使用 AppRouterProvider,默认路由参数为 { pathname: '/', query: {} },并允许用 navigation 覆盖(routeParams 为默认值与 navigation 的浅合并);loader 也会转而调用 createNavigation(router)
  • appDirectory 未开启时,走 PageRouterProvider + createRouter({ locale: globals.locale, ...router }) 路径,router 参数中的字段(如 pathnamequerylocale 相关动作)覆盖默认路由状态,globals.locale(可来自 addon 的 locale globalType)会被自动并入;
  • 两者二选一,navigationrouter 分别对应 Next.js 的两套路由体系(next/navigationnext/router),请勿混用。

对应的配置写法示例(在 .storybook/preview.ts 中):

import { definePreview } from '@storybook/nextjs-vite';

export default definePreview({
  parameters: {
    nextjs: {
      appDirectory: true,          // 若组件使用 next/navigation
      navigation: { pathname: '/dashboard', query: { tab: 'overview' } },
    },
  },
});

definePreview 的定义在 src/index.ts:它在 @storybook/react__definePreview 之上,强制把 nextPreview 作为第一个 addon 注入,从而保证上述装饰器与 loader 始终生效,同时通过 NextJsTypesparameters.nextjs 提供类型推导。

版本兼容细节:styled-jsx 与 Next 16 分叉

有两个源码细节值得注意,它们直接影响实际接入时的行为:

  1. styled-jsx 版本锁定styled-jsx5.1.6 精确版本作为本包依赖(而非 peer),并且 viteFinal 中把 styled-jsx/style.js 显式 alias 到 styled-jsx/style 的解析路径。这样无论项目自身安装了什么版本,预览端加载的都是框架受控的 styled-jsx,规避了 styled-jsx 在 Vite/ESM 下的模块解析差异;
  2. Next 16 兼容分叉previewAnnotations 中仅对 next < 16 注入 setConfig(process.env.__NEXT_RUNTIME_CONFIG) 注解(src/config/preview.ts)。若你升级到 Next 16 后遇到运行时配置相关的问题,可从源码结构看这是旧版注入逻辑被移除后的预期行为,应检查是否残留了对该机制的依赖。

内置模板故事:框架能力的可运行示例

template/ 目录提供了两类可直接参考的故事与 CLI 初始化模板,是验证各特性是否工作的“活文档”:

  • code/frameworks/nextjs-vite/template/clicreate-storybook 初始化时的 JS/TS 双版本模板(ButtonHeaderPage 及对应 stories 与 Configure.mdx);
  • code/frameworks/nextjs-vite/template/stories:覆盖框架全部难点特性的示例故事,包括 Cachecache 选项)、DynamicImportEnvironmentVariablesFont/GetImageProps/Imagenext/image 与字体)、HeadNavigation/Router/Redirect(两套路由体系与重定向)、RSC/ServerActions(React Server Components 相关)、StyledJsxUnstableRethrow 等。调试框架行为时,对照这些故事的写法与预期输出是最快的验证方式。

此外,e2e-sandbox/framework-nextjs.spec.ts 提供了端到端测试,用于验证 Next.js 框架在沙箱中的整体表现;frameworkOptions、App Directory 等特性在官方文档中也有对应的配置片段(如 docs/_snippets/nextjs-app-directory-in-meta.mddocs/_snippets/nextjs-framework-options-next-config-path.md),可作为配置参数的逐字段参考。

致谢与延伸阅读

按照 code/frameworks/nextjs-vite/README.md 的说明,本框架的实现借鉴了社区中 storybook-addon-next(RyanClementsHax)与 storybook-addon-next-router(lifeiscontent)两个 addon 的经验——这也解释了为什么其路由 mock 与 head 管理的设计与这两个 addon 的思路高度一致。

小结@storybook/nextjs-vite 通过“@storybook/builder-vite 构建器 + @storybook/react 渲染器 + vite-plugin-storybook-nextjs 插件 + 预览端装饰器/路由 mock”的四层组合,把 Next.js 组件拉进了纯浏览器的 Storybook 预览环境。接入时把握两个入口即可:main.ts 中的 frameworkOptionsnextConfigPathimage 过滤)负责构建期行为,parameters.nextjsappDirectoryrouternavigation)负责预览期行为;遇到版本差异时,对照 package.json 的 peerDependencies 与 preset 中 Next 16 的分叉逻辑定位问题。

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