Storybook @storybook/nextjs-vite 框架源码解析:用 Vite 构建器在 Storybook 中隔离开发 Next.js 组件
本文围绕 Storybook 仓库中 @storybook/nextjs-vite 框架包(位于 code/frameworks/nextjs-vite)展开,讲解它如何让你以 Vite 构建器运行 Next.js 组件的 Storybook,涵盖安装配置、frameworkOptions 与 parameters.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.0,vite支持^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0,react/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-vite。docs/_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.ts 的viteFinal中,预设通过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.mock、navigation.mock、router.mock、styled-jsx 等条目。这些正是包的子路径导出(见 package.json 的 exports 字段),预构建它们可以避免 Vite 启动时的重复优化。
viteFinal:叠加 React 预设并挂入 Next.js 插件
viteFinal 的执行顺序是:
- 先调用
@storybook/react-vite/preset的reactViteFinal(config, options),得到完整的 React + Vite 基础配置; - 调用
normalizePostCssConfig(searchPath)(实现于 src/find-postcss-config.ts)处理项目内 PostCSS 配置,使其能在 Vite 中正确加载; - 合并
resolve.alias,将styled-jsx、styled-jsx/style、styled-jsx/style.js全部指向依赖包中的具体文件路径——这是为了让 Next.js 组件中的styled-jsx导入在浏览器端预览时可用; - 最后把
vitePluginStorybookNextjs({ image, dir: nextDir })追加到plugins数组末尾,负责next/image等 Next.js 特有能力。
预览装饰器:让 Next.js 组件在浏览器里“跑得起来”
包的 ./preview 导出指向 src/preview.tsx,它由预设自动注入到预览环境(见上文 previewAnnotations)。文件顶部有三个值得注意的全局补丁:
next-head-countmeta 标签:addNextHeadCount()手动向document.head追加<meta name="next-head-count" content="0">,因为 Next.js 的Head组件依赖该标签来追踪自己插入的节点,而 Storybook 的宿主页面中并不存在;- 错误过滤:对
console.error打补丁,吞掉isNextRouterError(来自next/dist/client/components/is-next-router-error.js)以及异步客户端组件相关的已知报错(如 “Only Server Components can be async at the moment.”),避免 Next.js 在无 SSR 环境下的噪音输出; - 装饰器链:
export const decorators: Addon_DecoratorFunction<ReactRenderer>[] = [
asDecorator(StyledJsxDecorator),
asDecorator(ImageDecorator),
asDecorator(RouterDecorator),
asDecorator(HeadManagerDecorator),
];
四个装饰器分别对应源码目录中的四块实现:
StyledJsxDecorator(src/styledJsx/decorator.tsx):让<style jsx>样式在 Storybook 中正确挂载;ImageDecorator(src/images/decorator.tsx):配合vite-plugin-storybook-nextjs处理next/image的渲染;RouterDecorator(src/routing/decorator.tsx):根据parameters.nextjs.appDirectory决定包裹AppRouterProvider(App Router 场景,并额外包一层 Next 的RedirectBoundary以支持redirect())还是PageRouterProvider(Pages Router 场景);HeadManagerDecorator(src/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.ts 与 src/export-mocks/router/index.ts,设计上有三点共性:
- 可 spy 的 mock:
push、replace、back等导航动作全部用storybook/test的fn()包装并赋予 mockName(如next/navigation::useRouter().push),因此你可以直接在play函数中用expect(useRouter().push).toHaveBeenCalledWith(...)做交互测试; - overrides 支持:
createRouter(overrides)允许用户传入Partial<NextRouter>,指定的动作会被替换为“调用用户函数再记录”的包装实现,状态字段(pathname、query等)则直接合并进默认路由状态; - 模块级再导出: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.mock、navigation.mock、link.mock、headers.mock(src/export-mocks/headers/index.ts 提供 headers/cookies mock)与 cache.mock。next/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参数中的字段(如pathname、query、locale相关动作)覆盖默认路由状态,globals.locale(可来自 addon 的 locale globalType)会被自动并入;- 两者二选一,
navigation与router分别对应 Next.js 的两套路由体系(next/navigation与next/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 始终生效,同时通过 NextJsTypes 为 parameters.nextjs 提供类型推导。
版本兼容细节:styled-jsx 与 Next 16 分叉
有两个源码细节值得注意,它们直接影响实际接入时的行为:
- styled-jsx 版本锁定:
styled-jsx以5.1.6精确版本作为本包依赖(而非 peer),并且viteFinal中把styled-jsx/style.js显式 alias 到styled-jsx/style的解析路径。这样无论项目自身安装了什么版本,预览端加载的都是框架受控的 styled-jsx,规避了 styled-jsx 在 Vite/ESM 下的模块解析差异; - Next 16 兼容分叉:
previewAnnotations中仅对next < 16注入setConfig(process.env.__NEXT_RUNTIME_CONFIG)注解(src/config/preview.ts)。若你升级到 Next 16 后遇到运行时配置相关的问题,可从源码结构看这是旧版注入逻辑被移除后的预期行为,应检查是否残留了对该机制的依赖。
内置模板故事:框架能力的可运行示例
template/ 目录提供了两类可直接参考的故事与 CLI 初始化模板,是验证各特性是否工作的“活文档”:
- code/frameworks/nextjs-vite/template/cli:
create-storybook初始化时的 JS/TS 双版本模板(Button、Header、Page及对应 stories 与Configure.mdx); - code/frameworks/nextjs-vite/template/stories:覆盖框架全部难点特性的示例故事,包括
Cache(cache选项)、DynamicImport、EnvironmentVariables、Font/GetImageProps/Image(next/image与字体)、Head、Navigation/Router/Redirect(两套路由体系与重定向)、RSC/ServerActions(React Server Components 相关)、StyledJsx、UnstableRethrow等。调试框架行为时,对照这些故事的写法与预期输出是最快的验证方式。
此外,e2e-sandbox/framework-nextjs.spec.ts 提供了端到端测试,用于验证 Next.js 框架在沙箱中的整体表现;frameworkOptions、App Directory 等特性在官方文档中也有对应的配置片段(如 docs/_snippets/nextjs-app-directory-in-meta.md、docs/_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 中的 frameworkOptions(nextConfigPath、image 过滤)负责构建期行为,parameters.nextjs(appDirectory、router、navigation)负责预览期行为;遇到版本差异时,对照 package.json 的 peerDependencies 与 preset 中 Next 16 的分叉逻辑定位问题。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00