Storybook for Next.js 是一个 框架,它简化了 Next.js 应用程序中 UI 组件的独立开发和测试。它包括
- 🔀 路由
- 🖼 图片优化
- ⤵️ 绝对导入
- 🎨 样式
- 🎛 Webpack & Babel 配置
- 💫 等等!
- Next.js ≥ 13.5
- Storybook ≥ 7.0
在 Next.js 项目的根目录中运行以下命令后,请按照提示操作
更多关于 Storybook 入门的信息。
此框架设计用于与 Storybook 7+ 协同工作。如果您尚未使用 v7,请使用以下命令进行升级
运行上面的 upgrade
命令时,您应该会收到一个提示,要求您迁移到 @storybook/nextjs
,这应该可以为您处理所有事情。如果自动迁移对您的项目不起作用,请参考下面的手动迁移。
首先,安装框架
然后,更新您的 .storybook/main.js|ts
以更改 framework 属性
最后,如果您使用 Storybook 插件与 Next.js 集成,则在使用此框架时不再需要这些插件,可以将其删除
(⚠️ 实验性功能)
您可以使用我们最新推出的实验性框架 @storybook/experimental-nextjs-vite
,该框架基于 Vite,无需使用 Webpack 和 Babel。它支持此处记录的所有功能。
ℹ️使用 Next.js 框架与 Vite 需要 Next.js 14.1.0 或更高版本。
然后,更新您的 .storybook/main.js|ts
以更改 framework 属性
最后,如果您使用 Storybook 插件与 Next.js 集成,则在使用此框架时不再需要这些插件,可以将其删除
如果一切顺利,您应该会看到一个设置向导,它将帮助您开始使用 Storybook,并向您介绍主要的概念和功能,包括 UI 的组织方式、如何编写第一个故事以及如何使用 控件 测试组件对各种输入的响应。
如果您跳过了向导,只要示例故事仍然可用,您始终可以通过向 Storybook 实例的 URL 添加 ?path=/onboarding
查询参数来重新运行它。
此框架允许您在无需任何配置的情况下使用 Next.js 的 next/image。
支持 本地图像。
也支持 远程图像。
Storybook 部分支持 next/font。支持 next/font/google
和 next/font/local
包。
您无需执行任何操作。next/font/google
开箱即用。
对于本地字体,您必须定义 src 属性。该路径相对于调用字体加载器函数的目录。
如果以下组件以这种方式定义了您的 localFont
您必须通过 staticDirs
配置 告诉 Storybook fonts
目录在哪里。 from
值相对于 .storybook
目录。 to
值相对于 Storybook 的执行上下文。很可能它是项目的根目录。
以下功能尚不支持。将来可能会计划支持这些功能
在 Storybook 构建步骤中,从 Google 获取字体有时可能会失败。强烈建议模拟这些请求,因为这些失败也可能导致您的管道失败。Next.js 支持通过 JavaScript 模块模拟字体,该模块位于 env var NEXT_FONT_GOOGLE_MOCKED_RESPONSES
引用的位置。
例如,使用 GitHub Actions
您模拟的字体将如下所示
Next.js 的路由器 会自动为您进行存根,以便在与路由器交互时,如果启用了 Storybook actions 插件,其所有交互都会自动记录到 Actions 面板。
您应该只在 pages
目录中使用 next/router
。在 app
目录中,必须使用 next/navigation
。
可以通过在故事的 nextjs.router
属性上添加 参数 来进行每个故事的覆盖。框架会将您在此处添加的内容浅合并到路由器中。
RouterBasedComponent.stories.ts
存根路由器的默认值如下所示(有关全局变量的工作原理的更多详细信息,请参阅 全局变量)。
此外,router
对象 包含所有原始方法(例如 push()
、replace()
等)作为模拟函数,可以使用 常规模拟 API 对其进行操作和断言。
要覆盖这些默认值,您可以使用 参数 和 beforeEach
如果您的故事导入使用 next/navigation
的组件,则需要为该组件的故事设置参数 nextjs.appDirectory
为 true
NavigationBasedComponent.stories.ts
如果您的 Next.js 项目对每个页面都使用 app
目录(换句话说,它没有 pages
目录),则可以在 .storybook/preview.js|ts
文件中将参数 nextjs.appDirectory
设置为 true
以将其应用于所有故事。
可以通过在故事的 nextjs.navigation
属性上添加 参数 来进行每个故事的覆盖。框架会将您在此处添加的内容浅合并到路由器中。
NavigationBasedComponent.stories.ts
useSelectedLayoutSegment
、useSelectedLayoutSegments
和 useParams
钩子在 Storybook 中受支持。您必须将 nextjs.navigation.segments
参数设置为返回您要使用的片段或参数。
NavigationBasedComponent.stories.ts
使用上述配置,故事中渲染的组件将从钩子接收以下值
要使用 useParams
,您必须使用一个片段数组,其中每个元素都是包含两个字符串的数组。第一个字符串是参数键,第二个字符串是参数值。
NavigationBasedComponent.stories.ts
使用上述配置,故事中渲染的组件将从钩子接收以下值
如果未设置,nextjs.navigation.segments
的默认值为 []
。
存根导航上下文的默认值如下所示
此外,router
对象 包含所有原始方法(例如 push()
、replace()
等)作为模拟函数,可以使用 常规模拟 API 对其进行操作和断言。
要覆盖这些默认值,您可以使用 参数 和 beforeEach
next/head
原生支持。您可以在故事中像在 Next.js 应用程序中一样使用它。请记住,Head 的 children
会放置到 Storybook 用于渲染故事的 iframe 的 head 元素中。
全局 Sass/Scss 样式表 也无需任何额外配置即可支持。只需将它们导入到 .storybook/preview.js|ts
这会自动包含您在 next.config.js
文件中的任何 自定义 Sass 配置。
CSS 模块 可以按预期工作。
Next.js 内置的 CSS-in-JS 解决方案是 styled-jsx,并且该框架也开箱即用地支持它,无需任何配置。
您也可以使用自己的 babel 配置。这是一个如何自定义 styled-jsx 的示例。
Next.js 允许您 自定义 PostCSS 配置。因此,此框架将自动为您处理您的 PostCSS 配置。
这允许一些很酷的功能,例如零配置 Tailwind!(请参阅 Next.js 的示例)
支持从根目录进行 绝对导入。
对于 .storybook/preview.js|ts
中的全局样式也适用!
⚠️绝对导入**无法**在故事/测试中被模拟。有关更多信息,请参阅模拟模块部分。
也支持 模块别名。
作为 模块别名 的替代方案,您可以使用 子路径导入 来导入模块。这遵循 Node 包标准,并且在 模拟模块 时具有优势。
要配置子路径导入,您需要在项目的 package.json
文件中定义 imports
属性。此属性将子路径映射到实际的文件路径。以下示例配置了项目中所有模块的子路径导入
ℹ️因为子路径导入取代了模块别名,所以您可以从 TypeScript 配置中删除路径别名。
然后可以像这样使用它
组件通常依赖于导入到组件文件中的模块。这些可以来自外部包或项目内部。在 Storybook 中渲染这些组件或测试它们时,您可能希望 模拟这些模块 以控制和断言其行为。
此框架为许多 Next.js 的内部模块提供了模拟。
@storybook/nextjs/cache.mock
@storybook/nextjs/headers.mock
@storybook/nextjs/navigation.mock
@storybook/nextjs/router.mock
您如何在 Storybook 中模拟其他模块取决于您如何将模块导入到组件中。
无论哪种方法,第一步都是 创建模拟文件。以下是一个名为 session
的模块的模拟文件的示例
如果您使用的是 子路径导入,则可以调整您的配置以应用 条件,以便在 Storybook 内部使用模拟模块。以下示例配置了四个内部模块的子路径导入,这些模块在 Storybook 中被模拟
ℹ️每个子路径必须以 #
开头,以将其与常规模块路径区分开来。 #*
条目是一个通配符,将所有子路径映射到根目录。
如果您使用的是 模块别名,则可以将 Webpack 别名添加到 Storybook 配置中,以指向模拟文件。
Next.js 允许 运行时配置,它允许您导入一个方便的 getConfig
函数,以便在运行时获取在您的 next.config.js
文件中定义的某些配置。
在使用此框架的 Storybook 上下文中,您可以预期 Next.js 的 运行时配置 功能可以正常工作。
请注意,由于 Storybook 不会服务器端渲染您的组件,因此您的组件只会看到它们在客户端通常看到的内容(即,它们不会看到 serverRuntimeConfig
,但会看到 publicRuntimeConfig
)。
例如,请考虑以下 Next.js 配置
在 Storybook 中调用 getConfig
将返回以下对象
ℹ️如果您使用的是 @storybook/experimental-nextjs-vite
而不是 @storybook/nextjs
,则可以跳过此部分。基于 Vite 的 Next.js 框架不支持 Webpack 设置。
Next.js 开箱即用地提供了许多功能,例如 Sass 支持,但有时您会添加 对 Next.js 的自定义 Webpack 配置修改。此框架处理了您可能想要添加的大多数 Webpack 修改。如果 Next.js 开箱即用地支持某个功能,则该功能在 Storybook 中也将开箱即用。如果 Next.js 不开箱即用地支持某些功能,但易于配置,则此框架将为 Storybook 中的该功能执行相同的操作。
在 Storybook 中所需的任何 Webpack 修改都应在 .storybook/main.js|ts
中进行。
注意:并非所有 Webpack 修改都可以在 next.config.js
和 .storybook/main.js|ts
之间进行复制粘贴。建议您研究如何正确地对 Storybook 的 Webpack 配置进行修改,以及 Webpack 的工作原理。
以下是如何使用此框架向 Storybook 添加 SVGR 支持的示例。
Storybook 处理大多数 TypeScript 配置,但此框架为 Next.js 对 绝对导入和模块路径别名 的支持提供了额外的支持。简而言之,它会考虑您的 tsconfig.json
的 baseUrl 和 paths。因此,像下面这样的 tsconfig.json
将开箱即用。
(⚠️ 实验性功能)
如果您的应用程序使用 React 服务器组件 (RSC),Storybook 可以在浏览器中的故事中渲染它们。
要启用此功能,请在您的 .storybook/main.js|ts
配置中设置 experimentalRSC
功能标志
设置此标志会自动将您的故事包装在 Suspense 包装器中,该包装器能够在 NextJS 的 React 版本中渲染异步组件。
如果此包装器在您任何现有的故事中引起问题,您可以使用 react.rsc
参数 在全局/组件/故事级别选择性地禁用它
MyServerComponent.stories.ts
请注意,如果您的服务器组件访问服务器端资源(如文件系统或 Node 特定库),则将服务器组件包装在 Suspense 中不会有任何帮助。要解决此问题,您需要使用 Webpack 别名 或 storybook-addon-module-mock 等插件模拟您的数据访问层。
如果您的服务器组件通过网络访问数据,我们建议使用 MSW Storybook 插件 模拟网络请求。
将来,我们将在 Storybook 中提供更好的模拟支持,并支持 服务器操作。
如果您使用的是 Yarn v2 或 v3,您可能会遇到 Storybook 无法解析 style-loader
或 css-loader
的问题。例如,您可能会收到以下错误:
Module not found: Error: Can't resolve 'css-loader'
Module not found: Error: Can't resolve 'style-loader'
这是因为这些版本的 Yarn 与 Yarn v1.x 具有不同的包解析规则。如果您的情况如此,请直接安装该包。
Next.js 页面可以在 app
目录中的服务器组件中直接获取数据,这些数据通常包含仅在节点环境中运行的模块导入。这在 Storybook 中(目前)不起作用,因为如果您在故事中从包含这些节点模块导入的 Next.js 页面文件中导入,您的 Storybook 的 Webpack 将崩溃,因为这些模块不会在浏览器中运行。要解决此问题,您可以将页面文件中的组件提取到单独的文件中,并在故事中导入该纯组件。或者,如果由于某种原因这不可行,您可以在 Storybook 的 webpackFinal
配置 中 为这些模块提供 polyfill。
之前
之后
确保您处理图像导入的方式与在正常开发中使用 next/image
时相同。
在使用此框架之前,图像导入将导入图像的原始路径(例如 'static/media/stories/assets/logo.svg'
)。现在图像导入以“Next.js 方式”工作,这意味着您现在在导入图像时会得到一个对象。例如
因此,如果 Storybook 中的某些内容没有正确显示图像,请确保您期望从导入中返回对象,而不仅仅是资源路径。
有关 Next.js 如何处理静态图像导入的更多详细信息,请参阅 本地图像。
如果您使用的是 Yarn v2 或 v3,则可能会遇到此问题。有关更多详细信息,请参阅 Yarn v2 和 v3 用户的注意事项。
我们引入了实验性的 Vite 构建器支持。只需安装实验性框架包 @storybook/experimental-nextjs-vite
并替换所有 @storybook/nextjs
的实例为 @storybook/experimental-nextjs-vite
。
sharp
是 Next.js 图像优化功能的依赖项。如果您看到此错误,则需要在您的项目中安装 sharp
。
您可以参考 Next.js 文档中的 安装 sharp
以使用内置图像优化 以获取更多信息。
@storybook/nextjs
包导出了一些模块,使您能够 模拟 Next.js 的内部行为。
类型:{ getPackageAliases: ({ useESM?: boolean }) => void }
getPackageAliases
是一个用于生成设置 可移植故事 所需别名的助手。
类型:typeof import('next/cache')
此模块导出 next/cache
模块导出的模拟实现。您可以使用它来创建您自己的模拟实现,或在故事的 播放函数 中断言模拟调用。
类型:cookies
、headers
和 draftMode
来自 Next.js
此模块导出 next/headers
模块导出的可写模拟实现。您可以使用它来设置在您的故事中读取的 cookie 或标头,并稍后断言它们已被调用。
Next.js 的默认 headers()
导出是只读的,但此模块公开了允许您写入标头的方法
headers().append(name: string, value: string)
:如果标头已存在,则将值附加到标头。
headers().delete(name: string)
:删除标头
headers().set(name: string, value: string)
:将标头设置为提供的值。
对于 cookie,您可以使用现有 API 来写入它们。例如,cookies().set('firstName', 'Jane')
。
因为 headers()
、cookies()
及其子函数都是模拟函数,所以您可以在您的故事中使用任何 模拟实用程序,例如 headers().getAll.mock.calls
。
类型:typeof import('next/navigation') & getRouter: () => ReturnType<typeof import('next/navigation')['useRouter']>
此模块导出 next/navigation
模块导出的模拟实现。它还导出一个 getRouter
函数,该函数返回 Next.js 的 router
对象(来自 useRouter
) 的模拟版本,允许操作和断言其属性。您可以使用它模拟实现或在故事的 播放函数 中断言模拟调用。
类型:typeof import('next/router') & getRouter: () => ReturnType<typeof import('next/router')['useRouter']>
此模块导出 next/router
模块导出的模拟实现。它还导出一个 getRouter
函数,该函数返回 Next.js 的 router
对象(来自 useRouter
) 的模拟版本,允许操作和断言其属性。您可以使用它模拟实现或在故事的 播放函数 中断言模拟调用。
如果需要,您可以传递一个选项对象以进行其他配置
可用的选项是
类型:Record<string, any>
配置 框架构建器 的选项。对于 Next.js,可在 Webpack 构建器文档 中找到可用的选项。
类型:object
传递给每个 next/image
实例的属性。有关更多详细信息,请参阅next/image 文档。
类型:string
指向 next.config.js
文件的绝对路径。如果您有自定义的 next.config.js
文件不在项目根目录中,则需要此路径。
此框架在 nextjs
命名空间下向 Storybook 提供以下参数
类型:boolean
默认值:false
如果您的故事导入使用 next/navigation
的组件,则需要将参数 nextjs.appDirectory
设置为 true
。因为这是一个参数,所以您可以将其应用于单个故事、组件的所有故事或Storybook 中的每个故事。有关更多详细信息,请参阅Next.js 导航。
类型
默认值
传递给 next/navigation
上下文的路由对象。有关更多详细信息,请参阅Next.js 的导航文档。
类型
传递给 next/router
上下文的路由对象。有关更多详细信息,请参阅Next.js 的路由文档。