Next.js 官方示例解析:用 Storybook 构建组件驱动开发工作流(with-storybook)
本文基于 Next.js 官方仓库中的 with-storybook 示例,完整拆解如何在 Next.js 项目中集成 Storybook:包括通过 create-next-app 引导项目、.storybook 配置文件中各参数的含义、使用 @storybook/nextjs 框架在 Story 中直接使用 styled-jsx 等 Next.js 特性的方式,以及 storybook build 静态产物与 vercel.json 部署配置的配合。读完后你可以在任意 Next.js 项目中复刻这套组件开发、预览、交互测试与静态构建发布的完整工作流。
示例定位与核心能力
官方 README 对示例的定义是:一套使用 @storybook/nextjs(官方文档中提及的包名,仓库内 devDependencies 版本为 ^8.0.9)的标准 Storybook 配置,并且示例中的 stories 专门用来演示在 Storybook 中使用 Next.js 特性的能力。
从源码可以印证这一点:Button 组件 在渲染时内联了一段 styled-jsx 样式,直接消费 backgroundColor prop:
// examples/with-storybook/stories/Button.tsx#L40-L55
return (
<button
type="button"
className={["storybook-button", `storybook-button--${size}`, mode].join(" ")}
{...props}
>
{label}
<style jsx>{`
button {
background-color: ${backgroundColor};
}
`}</style>
</button>
);
styled-jsx 是 Next.js 内置的 CSS-in-JS 方案,普通 React 项目中无法直接使用;该示例选择它作为演示载体,正是为了说明 @storybook/nextjs 框架已经替组件开发环境接好了 Next.js 的编译链路(styled-jsx、next.config.mjs 等),组件在 Storybook Canvas 中的表现与在 Next.js 应用中一致。
快速开始:引导项目与运行命令
使用 create-next-app 引导
按照 README 的说明,使用 create-next-app 配合 npm、Yarn 或 pnpm 引导示例:
npx create-next-app --example with-storybook with-storybook-app
yarn create next-app --example with-storybook with-storybook-app
pnpm create next-app --example with-storybook with-storybook-app
引导完成后得到一个名为 with-storybook-app 的独立项目,示例源码位于仓库的 examples/with-storybook 目录,其目录结构为:
| 路径 | 作用 |
|---|---|
| .storybook/main.ts | Storybook 主配置:stories 发现规则、addons、框架(@storybook/nextjs)与 builder 选项 |
| .storybook/preview.ts | 全局预览参数(controls 匹配规则) |
| stories/ | 示例组件(Button、Header、Page)及其 .stories.ts 与 CSS 文件,另有 Configure.mdx 文档页 |
| app/ | 标准 App Router 落地页(layout.tsx、page.tsx),与 Storybook 并行存在 |
| vercel.json | 声明部署框架为 storybook、构建命令为 storybook build |
| package.json | 脚本与依赖声明(next 14.2.2、storybook ^8.0.9,要求 Node >= 18) |
运行开发态 Storybook
package.json 中的 storybook 脚本实际执行的是 storybook dev -p 6006:
npm run storybook
# or
yarn storybook
# or
pnpm storybook
服务默认监听 6006 端口,打开后左侧面板按 Example/Button、Example/Header、Example/Page 的 title 组织各 Story。
构建静态 Storybook
npm run build-storybook
# or
yarn build-storybook
# or
pnpm build-storybook
该脚本执行 storybook build,输出到 storybook-static 目录,产物为纯静态站点,可直接被任意静态托管服务或 Vercel 部署。README 特别指出:部署到 Vercel 时需要将输出目录指定为 storybook-static,而 vercel.json 正是为此准备的:
{
"framework": "storybook",
"buildCommand": "storybook build"
}
framework 字段让部署平台识别这是一个 Storybook 项目,buildCommand 显式指定构建命令,两者配合使 storybook-static 产物成为部署输出。
深入 .storybook/main.ts:框架、构建器与 stories 发现
main.ts 是理解本示例的关键文件,完整内容如下:
import type { StorybookConfig } from "@storybook/nextjs";
const config: StorybookConfig = {
stories: [
"../stories/**/*.mdx",
"../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@chromatic-com/storybook",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/nextjs",
options: {
builder: {
useSWC: true,
},
},
},
docs: {
autodocs: "tag",
},
staticDirs: ["../public"],
};
export default config;
逐项说明各配置的作用:
- stories:两个 glob 定义了 Story 的发现规则——
../stories目录(相对.storybook/)下的全部.mdx文档页与全部*.stories.(js|jsx|mjs|ts|tsx)文件。本示例中即 Button.stories.ts、Header.stories.ts、Page.stories.ts 与Configure.mdx。路径以../开头,因为配置目录是.storybook/,需要回到项目根才能找到stories/与public/。 - framework:声明使用
@storybook/nextjs框架,这是本示例的核心。它意味着 Story 编译走 Next.js 的转译/构建管线(styled-jsx、CSS Modules、next.config.mjs均被识别),而不仅仅是 babel/react 框架。 - framework.options.builder.useSWC:启用 SWC 作为底层构建器,显著加快 dev 启动与 HMR 速度。
- addons:
@storybook/addon-links:在 Story 之间创建链接(配合params.links实现 Story 到 Story 的跳转);@storybook/addon-essentials:捆绑 Controls、Actions、Viewport、Backgrounds、Docs 等基础能力;@storybook/addon-interactions:与@storybook/test配合,为交互测试(play 函数)提供 UI 支持;@chromatic-com/storybook:视觉回归(Chromatic)集成,在 CI 中对比 Story 渲染快照。
- docs.autodocs: "tag":自动文档页的生成策略——只为带
autodocstag 的组件生成自动文档页(见下文 Button/Header 的tags: ["autodocs"])。 - staticDirs: ["../public"]:将项目
public/目录中的静态资源(本示例中的next.svg、vercel.svg)暴露给 Story 使用,组件里可以用绝对路径引用这些资源。
preview.ts 则配置全局 Controls 的匹配器,使所有以 background/color 结尾的参数自动呈现颜色选择器、以 Date 结尾的参数呈现日期选择器:
import type { Preview } from "@storybook/react";
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
这也解释了为什么 Button 的 backgroundColor 属性在 Controls 面板中显示为颜色控件(见下文 argTypes 部分)。
逐读三个 Story:args、actions 与交互测试
Button:参数类型与动作监听
Button.stories.ts 展示了元数据(meta)驱动的写法,核心片段:
// examples/with-storybook/stories/Button.stories.ts#L13-L26
tags: ["autodocs"],
argTypes: {
backgroundColor: {
control: "color",
},
},
args: { onClick: fn() },
tags: ["autodocs"]与main.ts中的docs.autodocs: "tag"联动,为该组件自动生成文档页;argTypes.backgroundColor.control: "color"显式将backgroundColor声明为颜色控件(与 preview.ts 的正则匹配器双重保险);args.onClick: fn()中的fn来自@storybook/test,作为 spy 监听onClick——点击画布中的按钮后,调用会出现在 Actions 面板中。
组件本身基于 args 派生出四个 Story:Primary(primary: true)、Secondary、Large(size: "large")、Small(size: "small")。由于 Button 组件 中 primary 默认 false、size 默认 "medium",每个 Story 只需覆盖差异参数即可,这正是 args 模式的价值。
Header:组合组件与状态变体
Header 组件 复用了 Button,按 user prop 的有无呈现两种 UI 分支(登录态显示欢迎语与 Log out,未登录显示 Log in 与 Sign up)。对应 Header.stories.ts 定义了两个 Story:
export const LoggedIn: Story = {
args: { user: { name: "Jane Doe" } },
};
export const LoggedOut: Story = {};
meta 中将 onLogin/onLogout/onCreateAccount 全部绑定 fn(),layout: "fullscreen" 让头部组件铺满画布,模拟真实页面中的展示位置。
Page:用 play 函数编写交互测试
Page.stories.ts 演示了 Storybook 的交互测试(Interaction Testing):play 函数在 Story 渲染后运行一段脚本,模拟用户操作并断言结果:
// examples/with-storybook/stories/Page.stories.ts#L21-L31
export const LoggedIn: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const loginButton = canvas.getByRole("button", { name: /Log in/i });
await expect(loginButton).toBeInTheDocument();
await userEvent.click(loginButton);
await expect(loginButton).not.toBeInTheDocument();
const logoutButton = canvas.getByRole("button", { name: /Log out/i });
await expect(logoutButton).toBeInTheDocument();
},
};
脚本语义:断言画布中存在 Log in 按钮 → 模拟点击 → 断言其消失 → 断言 Log out 按钮出现。整个链路基于 Page 组件 内的 useState 完成本地登录态切换,within/userEvent/expect 均来自 @storybook/test(内部封装了 Testing Library 与用户事件模拟)。依赖列表中 @storybook/addon-interactions 负责在 UI 中展示这些交互结果。这是"组件文档 + 轻量行为验证"合二为一的典型用法。
TypeScript 支持与版本前提
README 指出:自 Storybook v6.0 起内置 TypeScript 支持,开箱即用无需额外配置;如需定制默认配置需参考 Storybook 的 TypeScript 文档。本示例的 tsconfig.json 是标准 Next.js 严格模式配置(strict: true、moduleResolution: "bundler"、@/* 路径别名、next 插件),Story 文件在其中与 App 代码共享同一套类型检查。
适用前提(以 package.json 为准):
- Next.js 固定为
14.2.2,React 为^18; - Storybook 全家桶(
@storybook/nextjs、@storybook/react、@storybook/blocks、@storybook/test及各 addon)均为^8.0.9,即 Storybook 8.x 的 API(Meta/StoryObj类型、satisfies约束 meta 写法); - 要求 Node
>= 18(engines字段)。
如果你的项目使用 Next.js 15 或 Storybook 9,本示例的目录结构与配置思路仍然适用,但类型导入与部分 addon 包名需要以对应版本的官方文档为准。
小结:这套工作流解决了什么
该示例展示的双轨结构值得注意:app/ 目录是一个完整的 App Router 应用(落地页用于验证 next dev/next build 正常),stories/ + .storybook/ 则是并行的组件工坊。日常开发中,UI 组件先在 Storybook 6006 端口中被隔离开发、通过 Controls 调参、通过 Actions 与 play 交互测试验证行为;storybook build 产出的静态站点可独立部署供团队评审(vercel.json 已配好输出约定);而组件本身因为运行在 @storybook/nextjs 框架下,可直接使用 styled-jsx 等 Next.js 特性,所见即所得地迁移回 app/ 应用中使用。
如需继续阅读相关源码,可从 examples/with-storybook/README.md 出发,配合 examples/with-storybook/.storybook/main.ts、examples/with-storybook/stories/Button.stories.ts 与 examples/with-storybook/stories/Page.stories.ts 对照理解每一处配置的实际效果。
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证件照制作算法。Python08
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