Storybook × Next.js:手动将 React 项目的 framework 切换到 @storybook/nextjs,并看懂其 Preset 实现
本文以 Storybook 官方文档中「手动安装 Next.js framework」的核心配置片段为主体,完整讲解如何将一个已有的 React + Webpack 项目切换到 @storybook/nextjs 框架:从安装依赖、修改 .storybook/main.js|ts 中的 framework 字段,到清理不再需要的旧 Addon,并结合开源仓库源码深入解析这个字段背后触发的 Preset 解析、Builder/Renderer 装配与 defineMain 类型函数的真实行为。读完本文,你可以独立完成框架切换,并理解 Storybook 配置项与底层 preset 加载机制之间的对应关系。
1. 适用场景:为什么需要手动切换 framework
当你的项目最初使用 @storybook/react-webpack5(或其他 React 框架 preset)初始化了 Storybook,但业务本身是 Next.js 应用时,官方推荐的做法是把整个框架 preset 切换为 @storybook/nextjs。这一流程在官方文档 Next.js framework 页面的 FAQ「How do I manually install the Next.js framework?」 中给出了完整步骤,其核心配置片段正是 nextjs-add-framework.md 所定义的 diff 内容:修改 framework 属性并同步更换配置类型的 import 来源。
整个流程分为三步:
- 安装
@storybook/nextjs开发依赖; - 修改
.storybook/main.js|ts,将framework从原框架改为@storybook/nextjs,并更换StorybookConfig类型或defineMain的 import 路径; - 移除此前用于集成 Next.js 的第三方 Addon(如
storybook-addon-next)。
以下各节依次展开这三步,并在第 5 节深入源码说明 framework 字段在 Storybook 内部究竟做了什么。
2. 第一步:安装 @storybook/nextjs 包
在修改任何配置之前,先按你的包管理器安装框架包(来自 nextjs-install.md):
# npm
npm install --save-dev @storybook/nextjs
# pnpm
pnpm add --save-dev @storybook/nextjs
# yarn
yarn add --dev @storybook/nextjs
安装完成后,从 package.json 的 peerDependencies 可以确认适用前提:该 preset 要求项目满足 next ^14.1.0 || ^15.0.0 || ^16.0.0、react ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0,以及 webpack ^5.0.0(在 peerDependenciesMeta 中标记为可选,因为 webpack 5 由 preset 内部依赖提供)。如果你的 Next.js 版本不在上述范围内,需要先升级 Next.js 再执行本切换。
3. 第二步:修改 .storybook/main.js|ts 中的 framework 属性
这是核心文档 nextjs-add-framework.md 的主体内容。该片段覆盖了两种配置写法风格(CSF 3 传统写法与 CSF Next 实验性写法)以及两种文件语言(.js / .ts),共四种 diff 变体,切换时请按你项目实际使用的风格对照执行。
3.1 CSF 3 风格:.storybook/main.js
最小改动只有一个字段:
export default {
// ...
- framework: '@storybook/react-webpack5',
+ framework: '@storybook/nextjs',
};
3.2 CSF 3 风格:.storybook/main.ts
TypeScript 版本除了改 framework 字段外,还必须同步更换配置对象的类型来源——StorybookConfig 类型由框架包提供,切换框架后 import 路径要从旧框架包改为 @storybook/nextjs:
- import type { StorybookConfig } from '@storybook/your-previous-framework';
+ import type { StorybookConfig } from '@storybook/nextjs';
const config: StorybookConfig = {
// ...
- framework: '@storybook/react-webpack5',
+ framework: '@storybook/nextjs',
};
export default config;
这里 your-previous-framework 是占位符,代表你当前正在使用的框架包名(如 @storybook/react-webpack5)。之所以必须改 import,是因为 StorybookConfig 类型中 framework 字段的合法值与各框架扩展的 options 类型都由框架包自身声明,沿用旧包的类型会导致字段校验与新框架的 options(见第 5.5 节)不匹配。
3.3 CSF Next(实验性)风格:defineMain 写法
如果你的 main.ts|js 采用实验性的 defineMain 辅助函数写法,改动点从类型 import 变为 defineMain 的 import 来源:
- import { defineMain } from '@storybook/your-previous-framework/node';
+ import { defineMain } from '@storybook/nextjs/node';
export default defineMain({
// ...
- framework: '@storybook/react-webpack5',
+ framework: '@storybook/nextjs',
});
同一段配置在 .storybook/main.js(JS 版 CSF Next 写法)中完全相同:
- import { defineMain } from '@storybook/your-previous-framework/node';
+ import { defineMain } from '@storybook/nextjs/node';
export default defineMain({
// ...
- framework: '@storybook/react-webpack5',
+ framework: '@storybook/nextjs',
});
关于 @storybook/nextjs/node 子路径导出,可以在 src/node/index.ts 中确认其真实实现:
import type { StorybookConfig } from '../types.ts';
export function defineMain(config: StorybookConfig) {
return config;
}
export type { StorybookConfig };
可以看到 defineMain 是一个恒等函数(identity function),运行时不做任何处理,它的全部价值在于静态层面:借助 @storybook/nextjs/node 子路径导出的、由该框架 types.ts 定义的 StorybookConfig 类型,让配置对象获得 Next.js 框架专属的字段补全与校验。因此把 defineMain 的 import 从旧框架包切到 @storybook/nextjs/node,等价于把类型系统整体切换到 Next.js 框架的 schema 上。
4. 第三步:移除不再需要的 Next.js 集成 Addon
切换到 @storybook/nextjs 框架后,此前用于把 Next.js 特性「注入」到普通 React Storybook 的第三方 Addon 会被 preset 的原生能力取代,应当从 addons 数组中删除。根据 nextjs-remove-addons.md,可以移除的是:
export default {
// ...
addons: [
// ...
// 👇 These can both be removed
// 'storybook-addon-next',
// 'storybook-addon-next-router',
],
};
即 storybook-addon-next 与 storybook-addon-next-router 两个 Addon。TS 配置(CSF 3 或 CSF Next 写法)中的清理方式相同,只是配置外层包的是 StorybookConfig 对象或 defineMain({...}) 调用。清理后请确认 package.json 中对应的依赖也一并卸载,避免残留依赖拉入与 preset 冲突的 webpack 插件。
5. 源码纵深:framework: '@storybook/nextjs' 在 Storybook 内部触发了什么
配置层面改一个字段,但运行时 Storybook 会据此把整个构建链换掉。以下基于本仓库 code/frameworks/nextjs 的源码说明这条链路。
5.1 framework 字段即 preset 名称
Storybook 的 framework 字段在内部按 preset 规则解析:字符串值 @storybook/nextjs 会加载该包的 preset 入口。对应到本仓库就是 preset.js(一行 re-export 到构建产物 dist/preset.js),其源码为 src/preset.ts。preset 文件按约定导出若干 PresetProperty 钩子,Storybook 在启动时逐个应用。
5.2 core 钩子:锁定 builder-webpack5 与 React renderer
src/preset.ts 的 core 钩子是切换后行为变化的核心:
- 首先它通过
options.presets.apply('framework')回读framework配置,并在 webpack 真正启动之前调用configureConfig加载 Next.js 的next.config.js配置。源码注释说明了原因:让 Next.js 有机会先行覆写 webpack 内部行为,否则@storybook/builder-webpack5的文件系统缓存(fsCache: true)无法正常工作。同时支持从对象形式的framework.options.nextConfigPath中读取自定义的 Next.js 配置路径; - 然后返回固定的构建链:
builder指向@storybook/builder-webpack5(可选合并framework.options.builder),renderer指向@storybook/react/preset,addons钩子则自动追加@storybook/preset-react-webpack。
这也解释了为什么 package.json 的 dependencies 中会直接依赖 @storybook/builder-webpack5、@storybook/react 与 @storybook/preset-react-webpack——框架 preset 自身保证了构建链的完整性。
5.3 previewAnnotations:注入 preview 与 Next.js 运行时兼容层
previewAnnotations 钩子会在 preview 编译前自动注入 @storybook/nextjs/preview 注解(对应 src/preview.tsx);对于 Next.js 16 以下版本,还会额外注入 @storybook/nextjs/config/preview(源码中留有 TODO,待只支持 Next.js 16+ 后移除)。这两个文件承载了路由 Provider、next/image 装饰器、styled-jsx、head 管理等运行时能力,全部对用户透明——这正是「切换 framework 后无需再挂第三方 Addon」的原因。
5.4 babel 钩子:复用项目的 Next.js Babel 配置
pretset.ts 的 babel 钩子 会解析项目现有的 Babel 配置,识别其中的 next/babel preset(字符串、数组或含 file.request 的配置项三种形态),据此决定如何组装 Storybook 侧的 Babel 处理链(内置 src/babel/preset.ts 与若干 Next.js 兼容插件,如 react-loadable-plugin、optimize-hook-destructuring 等)。从源码结构看,这套机制保证你在 Next.js 项目中启用的 Babel 插件在 Storybook 里也能生效。
5.5 framework 的对象形式:nextConfigPath 与 builder
从 core 钩子的实现 可以确认,framework 除了字符串形式外还支持对象形式,其中 options.nextConfigPath 用于指定非默认的 next.config.js 位置,options.builder 用于向 @storybook/builder-webpack5 透传 builder 级选项:
// 对应 preset.ts 中的读取逻辑
nextConfigPath: typeof framework === 'string' ? undefined : framework.options.nextConfigPath,
// builder.options 合并自
...(typeof framework === 'string' ? {} : framework.options.builder || {}),
这两个选项的完整类型定义位于 src/types.ts,即 @storybook/nextjs 包导出的 StorybookConfig 所依赖的类型源。
6. @storybook/nextjs 提供的能力边界
切换 framework 后获得哪些能力、边界在哪,可以直接从 package.json 的 exports 映射与 src 目录结构 相互印证。exports 暴露的用户可用子路径包括:
| 子路径 | 对应源码 | 用途 |
|---|---|---|
@storybook/nextjs/preview |
src/preview.tsx | preview 运行时注入入口 |
@storybook/nextjs/node |
src/node/index.ts | defineMain 与 StorybookConfig 类型 |
@storybook/nextjs/export-mocks(及 headers.mock、navigation.mock、router.mock、link.mock 等) |
src/export-mocks/ | 在 Storybook 中 mock Next.js 的 headers/cookies/useRouter/usePathname 等 API |
@storybook/nextjs/images/next-image 与 images/next-legacy-image |
src/images/ | 以 Next.js 方式渲染 next/image |
@storybook/nextjs/rsc/server-only |
src/rsc/server-only.ts | 实验性 React Server Components 支持所需的桩模块 |
@storybook/nextjs/storybook-nextjs-font-loader |
src/font/webpack/loader/ | next/font 的字体加载 Webpack loader |
从 src 目录结构看,preset 还内置了:routing/(App Router 与 Pages Router 两种 Provider 的装饰器)、styledJsx/(styled-jsx 编译支持)、swc/(Next.js SWC loader 补丁)、nodePolyfills/(Node 模块 polyfill,由依赖 node-polyfill-webpack-plugin 驱动)、aliases/ 与 imports/(基于 tsconfig-paths-webpack-plugin 复用项目的 tsconfig.json 路径别名)等。依赖列表中的 styled-jsx、probe-image-size、react-refresh/@pmmmwh/react-refresh-webpack-plugin 等也都与这些能力一一对应。
结合官方 FAQ(nextjs.mdx)还有两个切换后必须知道的行为变化与限制:
- 图片导入语义变化:启用该框架后,静态图片 import 返回的是
{ src, height, width, blurDataURL }对象(Next.js 方式),而不再是裸路径字符串,故事中处理图片的地方需要相应调整; - Yarn v2/v3 用户注意:由于 Yarn 2/3 的包解析规则不同,可能出现
Can't resolve 'css-loader'/'style-loader'报错,此时需要把这两个 loader 直接安装为项目依赖; - 数据获取型页面:
app目录中直接 fetch 数据的页面组件引入 Node 专用模块会导致 Webpack 构建崩溃,官方建议把纯组件拆到单独文件供故事使用,或在webpackFinal中 polyfill 相关模块。
7. 小结与验证路径
- 手动切换
@storybook/nextjs框架的完整动作只有三处:安装依赖、把framework改为@storybook/nextjs并同步更换StorybookConfig/defineMain的 import 来源、移除storybook-addon-next(-router)类旧 Addon; framework字段在运行时等价于「加载该包的 preset」,@storybook/nextjs的 preset(src/preset.ts)负责把 builder 锁定为@storybook/builder-webpack5、renderer 锁定为 React preset,并在启动前加载next.config.js;defineMain是纯类型层面的恒等函数,其意义在于把配置对象绑定到@storybook/nextjs/node导出的框架类型上;- 切换后 Next.js 的图片、字体、路由、
headers/routermock 等能力由 preset 原生提供,能力清单与边界可从 package.json 的exports/peerDependencies精确核对(当前仓库版本为10.6.0-beta.1,适用 Next.js 14.1+/15/16)。
关键参考文件:核心配置片段、安装命令片段、旧 Addon 清理片段、Next.js framework 官方文档页、preset 入口、preset 源码、node 子路径导出、包清单。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00