Docs
Storybook Docs

使用 Webpack 的 Next.js Storybook

Storybook for Next.js (Webpack) 是一款 framework,可让你轻松地使用 Webpack 5 独立开发和测试 Next.js 应用的 UI 组件。

¥Storybook for Next.js (Webpack) is a framework that makes it easy to develop and test UI components in isolation for Next.js applications using Webpack 5.

我们建议大多数 Next.js 项目使用 @storybook/nextjs-vite。基于 Vite 的框架速度更快、更现代化,并且为测试功能提供了更好的支持。

¥We recommend using @storybook/nextjs-vite for most Next.js projects. The Vite-based framework is faster, more modern, and offers better support for testing features.

仅当满足以下条件时,才使用此基于 Webpack 的框架 (@storybook/nextjs):

¥Use this Webpack-based framework (@storybook/nextjs) only if:

  • 你的项目使用了自定义 Webpack 配置,与 Vite 不兼容。

    ¥Your project has custom Webpack configurations that are incompatible with Vite

  • 你的项目使用了自定义 Babel 配置,需要 Webpack。

    ¥Your project has custom Babel configurations that require Webpack

  • 你需要一些 Vite 中没有的特定 Webpack 功能

    ¥You need specific Webpack features not available in Vite

安装

¥Install

要在现有的 Next.js 项目中安装 Storybook,请在项目根目录下运行以下命令:

¥To install Storybook in an existing Next.js project, run this command in your project's root directory:

npm create storybook@latest

该命令会提示你选择使用本框架还是 @storybook/nextjs-vite。我们推荐使用基于 Vite 的框架 (了解原因)。

¥The command will prompt you to choose between this framework and @storybook/nextjs-vite. We recommend the Vite-based framework (learn why).

接下来,你可以开始使用 写故事运行测试组件文档。要更好地控制安装过程,请参阅 安装指南

¥You can then get started writing stories, running tests and documenting your components. For more control over the installation process, refer to the installation guide.

要求

¥Requirements

  • Next.js ≥ 14.1

  • Webpack 5

运行 Storybook

¥Run Storybook

要为特定项目运行 Storybook,请运行以下命令:

¥To run Storybook for a particular project, run the following:

npm run storybook

要构建 Storybook,请运行:

¥To build Storybook, run:

npm run build-storybook

你将在配置的 outputDir(默认为 storybook-static)中找到输出。

¥You will find the output in the configured outputDir (default is storybook-static).

配置

¥Configure

Storybook for Next.js with Vite 支持许多 Next.js 特性,包括:

¥Storybook for Next.js with Vite supports many Next.js features including:

Next.js 的图片组件

¥Next.js's Image component

此框架允许你无需配置即可使用 Next.js 的 next/image

¥This framework allows you to use Next.js's next/image with no configuration.

本地图片

¥Local images

本地图片 受支持。

¥Local images are supported.

index.jsx
import Image from 'next/image';
import profilePic from '../public/me.png';
 
function Home() {
  return (
    <>
      <h1>My Homepage</h1>
      <Image
        src={profilePic}
        alt="Picture of the author"
        // width={500} automatically provided
        // height={500} automatically provided
        // blurDataURL="../public/me.png" set to equal the image itself (for this framework)
        // placeholder="blur" // Optional blur-up while loading
      />
      <p>Welcome to my homepage!</p>
    </>
  );
}

远程图片

¥Remote images

远程图片 也受支持。

¥Remote images are also supported.

index.jsx
import Image from 'next/image';
 
export default function Home() {
  return (
    <>
      <h1>My Homepage</h1>
      <Image src="/me.png" alt="Picture of the author" width={500} height={500} />
      <p>Welcome to my homepage!</p>
    </>
  );
}

Next.js 字体优化

¥Next.js font optimization

next/font 在 Storybook 中得到部分支持。支持 next/font/googlenext/font/local 包。

¥next/font is partially supported in Storybook. The packages next/font/google and next/font/local are supported.

next/font/google

你不需要做任何事情。next/font/google 开箱即用。

¥You don't have to do anything. next/font/google is supported out of the box.

next/font/local

对于本地字体,你必须定义 src 属性。路径相对于调用字体加载器函数的目录。

¥For local fonts you have to define the src property. The path is relative to the directory where the font loader function is called.

如果以下组件像这样定义你的 localFont:

¥If the following component defines your localFont like this:

src/components/MyComponent.js
import localFont from 'next/font/local';
 
const localRubikStorm = localFont({ src: './fonts/RubikStorm-Regular.ttf' });
staticDir 映射

¥staticDir mapping

你必须通过 staticDirs 配置 告诉 Storybook fonts 目录的位置。from 值与 .storybook 目录相关。to 值与 Storybook 的执行上下文相关。很可能它是项目的根。

¥You have to tell Storybook where the fonts directory is located, via the staticDirs configuration. The from value is relative to the .storybook directory. The to value is relative to the execution context of Storybook. Very likely it is the root of your project.

.storybook/main.ts
// Replace your-framework with nextjs or nextjs-vite
import type { StorybookConfig } from '@storybook/your-framework';
 
const config: StorybookConfig = {
  // ...
  staticDirs: [
    {
      from: '../src/components/fonts',
      to: 'src/components/fonts',
    },
  ],
};
 
export default config;

不支持 next/font 的功能

¥Not supported features of next/font

以下功能尚不支持。未来可能会计划支持这些功能:

¥The following features are not supported (yet). Support for these features might be planned for the future:

测试期间模拟字体

¥Mocking fonts during testing

偶尔,从 Google 获取字体可能会作为 Storybook 构建步骤的一部分失败。强烈建议模拟这些请求,因为这些失败也会导致你的管道失败。Next.js 支持模拟字体 通过位于环境变量 NEXT_FONT_GOOGLE_MOCKED_RESPONSES 引用的 JavaScript 模块。

¥Occasionally fetching fonts from Google may fail as part of your Storybook build step. It is highly recommended to mock these requests, as those failures can cause your pipeline to fail as well. Next.js supports mocking fonts via a JavaScript module located where the env var NEXT_FONT_GOOGLE_MOCKED_RESPONSES references.

例如,使用 GitHub Actions

¥For example, using GitHub Actions:

.github/workflows/ci.yml
- uses: chromaui/action@latest
  env:
    #👇 the location of mocked fonts to use
    NEXT_FONT_GOOGLE_MOCKED_RESPONSES: ${{ github.workspace }}/mocked-google-fonts.js
  with:
    projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
    token: ${{ secrets.GITHUB_TOKEN }}

你的模拟字体看起来会像这样:

¥Your mocked fonts will look something like this:

mocked-google-fonts.js
//👇 Mocked responses of google fonts with the URL as the key
module.exports = {
  'https://fonts.googleapis.com/css?family=Inter:wght@400;500;600;800&display=block': `
    /* cyrillic-ext */
    @font-face {
      font-family: 'Inter';
      font-style: normal;
      font-weight: 400;
      font-display: block;
      src: url(https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZJhiJ-Ek-_EeAmM.woff2) format('woff2');
      unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
    }
    /* more font declarations go here */
    /* latin */
    @font-face {
      font-family: 'Inter';
      font-style: normal;
      font-weight: 400;
      font-display: block;
      src: url(https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiJ-Ek-_EeA.woff2) format('woff2');
      unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
    }`,
};

Next.js 路由

¥Next.js routing

Next.js 的路由 会自动为你存根,以便与路由交互时,所有交互都会自动记录到 操作面板 中。

¥Next.js's router is automatically stubbed for you so that when the router is interacted with, all of its interactions are automatically logged to the Actions panel.

你只应在 pages 目录中使用 next/router。在 app 目录中,必须使用 next/navigation

¥You should only use next/router in the pages directory. In the app directory, it is necessary to use next/navigation.

覆盖默认值

¥Overriding defaults

可以通过将 nextjs.router 属性添加到故事 参数 上来完成每个故事的覆盖。框架将浅层合并你在此处放入路由的任何内容。

¥Per-story overrides can be done by adding a nextjs.router property onto the story parameters. The framework will shallowly merge whatever you put here into the router.

RouterBasedComponent.stories.ts
// Replace your-framework with nextjs or nextjs-vite
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import RouterBasedComponent from './RouterBasedComponent';
 
const meta = {
  component: RouterBasedComponent,
} satisfies Meta<typeof RouterBasedComponent>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
// Interact with the links to see the route change events in the Actions panel.
export const Example: Story = {
  parameters: {
    nextjs: {
      router: {
        pathname: '/profile/[id]',
        asPath: '/profile/1',
        query: {
          id: '1',
        },
      },
    },
  },
};

这些覆盖也可以应用于 组件的所有故事你组件中的所有故事项目。适用标准 参数继承 规则。

¥These overrides can also be applied to all stories for a component or all stories in your project. Standard parameter inheritance rules apply.

默认路由

¥Default router

存根路由上的默认值如下(有关全局变量如何工作的更多详细信息,请参阅 globals)。

¥The default values on the stubbed router are as follows (see globals for more details on how globals work).

// Default router
const defaultRouter = {
  // The locale should be configured globally: https://storybook.nodejs.cn/docs/essentials/toolbars-and-globals#globals
  locale: globals?.locale,
  asPath: '/',
  basePath: '/',
  isFallback: false,
  isLocaleDomain: false,
  isReady: true,
  isPreview: false,
  route: '/',
  pathname: '/',
  query: {},
};

此外,router 对象 包含所有原始方法(例如 push()replace() 等)作为模拟函数,可以使用 常规模拟 API 对其进行操作和断言。

¥Additionally, the router object contains all of the original methods (such as push(), replace(), etc.) as mock functions that can be manipulated and asserted on using regular mock APIs.

要覆盖这些默认值,你可以使用 参数beforeEach

¥To override these defaults, you can use parameters and beforeEach:

.storybook/preview.js|ts
import type { Preview } from '@storybook/nextjs';
 
// 👇 Must include the `.mock` portion of filename to have mocks typed correctly
import { getRouter } from "@storybook/nextjs/router.mock";
 
const preview: Preview = {
  parameters: {
    nextjs: {
      // 👇 Override the default router properties
      router: {
        basePath: '/app/',
      },
    },
  },
  async beforeEach() {
    // 👇 Manipulate the default router method mocks
    getRouter().push.mockImplementation(() => {
      /* ... */
    });
  },
};

Next.js 导航

¥Next.js navigation

请注意,next/navigation 只能在 app 目录中的组件/页面中使用。

¥Please note that next/navigation can only be used in components/pages in the app directory.

nextjs.appDirectory 设置为 true

¥Set nextjs.appDirectory to true

如果你的故事导入了使用 next/navigation 的组件,则需要为该组件的故事将参数 nextjs.appDirectory 设置为 true

¥If your story imports components that use next/navigation, you need to set the parameter nextjs.appDirectory to true in for that component's stories:

NavigationBasedComponent.stories.ts
// Replace your-framework with nextjs or nextjs-vite
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import NavigationBasedComponent from './NavigationBasedComponent';
 
const meta = {
  component: NavigationBasedComponent,
  parameters: {
    nextjs: {
      appDirectory: true, // 👈 Set this
    },
  },
} satisfies Meta<typeof NavigationBasedComponent>;
export default meta;

如果你的 Next.js 项目对每个页面都使用 app 目录(换句话说,它没有 pages 目录),你可以在 .storybook/preview.js|ts 文件中将参数 nextjs.appDirectory 设置为 true 以将其应用于所有故事。

¥If your Next.js project uses the app directory for every page (in other words, it does not have a pages directory), you can set the parameter nextjs.appDirectory to true in the .storybook/preview.js|ts file to apply it to all stories.

.storybook/preview.ts
// Replace your-framework with nextjs or nextjs-vite
import type { Preview } from '@storybook/your-framework';
 
const preview: Preview = {
  // ...
  parameters: {
    // ...
    nextjs: {
      appDirectory: true,
    },
  },
};
 
export default preview;

覆盖默认值

¥Overriding defaults

可以通过将 nextjs.navigation 属性添加到故事 参数 上来完成每个故事的覆盖。框架将浅层合并你在此处放入路由的任何内容。

¥Per-story overrides can be done by adding a nextjs.navigation property onto the story parameters. The framework will shallowly merge whatever you put here into the router.

NavigationBasedComponent.stories.ts
// Replace your-framework with nextjs or nextjs-vite
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import NavigationBasedComponent from './NavigationBasedComponent';
 
const meta = {
  component: NavigationBasedComponent,
  parameters: {
    nextjs: {
      appDirectory: true,
    },
  },
} satisfies Meta<typeof NavigationBasedComponent>;
export default meta;
 
type Story = StoryObj<typeof meta>;
 
// Interact with the links to see the route change events in the Actions panel.
export const Example: Story = {
  parameters: {
    nextjs: {
      navigation: {
        pathname: '/profile',
        query: {
          user: '1',
        },
      },
    },
  },
};

这些覆盖也可以应用于 组件的所有故事你组件中的所有故事项目。适用标准 参数继承 规则。

¥These overrides can also be applied to all stories for a component or all stories in your project. Standard parameter inheritance rules apply.

useSelectedLayoutSegmentuseSelectedLayoutSegmentsuseParams 钩子

¥useSelectedLayoutSegment, useSelectedLayoutSegments, and useParams hooks

Storybook 支持 useSelectedLayoutSegmentuseSelectedLayoutSegmentsuseParams 钩子。你必须设置 nextjs.navigation.segments 参数以返回要使用的段或参数。

¥The useSelectedLayoutSegment, useSelectedLayoutSegments, and useParams hooks are supported in Storybook. You have to set the nextjs.navigation.segments parameter to return the segments or the params you want to use.

NavigationBasedComponent.stories.ts
// Replace your-framework with nextjs or nextjs-vite
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import NavigationBasedComponent from './NavigationBasedComponent';
 
const meta = {
  component: NavigationBasedComponent,
  parameters: {
    nextjs: {
      appDirectory: true,
      navigation: {
        segments: ['dashboard', 'analytics'],
      },
    },
  },
} satisfies Meta<typeof NavigationBasedComponent>;
export default meta;

使用上述配置,故事中渲染的组件将从钩子中接收以下值:

¥With the above configuration, the component rendered in the stories would receive the following values from the hooks:

NavigationBasedComponent.js
import { useSelectedLayoutSegment, useSelectedLayoutSegments, useParams } from 'next/navigation';
 
export default function NavigationBasedComponent() {
  const segment = useSelectedLayoutSegment(); // dashboard
  const segments = useSelectedLayoutSegments(); // ["dashboard", "analytics"]
  const params = useParams(); // {}
  // ...
}

要使用 useParams,你必须使用一个段数组,其中每个元素都是包含两个字符串的数组。第一个字符串是 param 键,第二个字符串是 param 值。

¥To use useParams, you have to use a segments array where each element is an array containing two strings. The first string is the param key and the second string is the param value.

NavigationBasedComponent.stories.ts
// Replace your-framework with nextjs or nextjs-vite
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import NavigationBasedComponent from './NavigationBasedComponent';
 
const meta = {
  component: NavigationBasedComponent,
  parameters: {
    nextjs: {
      appDirectory: true,
      navigation: {
        segments: [
          ['slug', 'hello'],
          ['framework', 'nextjs'],
        ],
      },
    },
  },
} satisfies Meta<typeof NavigationBasedComponent>;
export default meta;

使用上述配置,故事中渲染的组件将从钩子中接收以下值:

¥With the above configuration, the component rendered in the stories would receive the following values from the hooks:

ParamsBasedComponent.js
import { useSelectedLayoutSegment, useSelectedLayoutSegments, useParams } from 'next/navigation';
 
export default function ParamsBasedComponent() {
  const segment = useSelectedLayoutSegment(); // hello
  const segments = useSelectedLayoutSegments(); // ["hello", "nextjs"]
  const params = useParams(); // { slug: "hello", framework: "nextjs" }
  ...
}

这些覆盖也可以应用于 单个故事你组件中的所有故事项目。适用标准 参数继承 规则。

¥These overrides can also be applied to a single story or all stories in your project. Standard parameter inheritance rules apply.

如果未设置,nextjs.navigation.segments 的默认值为 []

¥The default value of nextjs.navigation.segments is [] if not set.

默认导航上下文

¥Default navigation context

存根导航上下文上的默认值如下:

¥The default values on the stubbed navigation context are as follows:

// Default navigation context
const defaultNavigationContext = {
  pathname: '/',
  query: {},
};

此外,router 对象 包含所有原始方法(例如 push()replace() 等)作为模拟函数,可以使用 常规模拟 API 对其进行操作和断言。

¥Additionally, the router object contains all of the original methods (such as push(), replace(), etc.) as mock functions that can be manipulated and asserted on using regular mock APIs.

要覆盖这些默认值,你可以使用 参数beforeEach

¥To override these defaults, you can use parameters and beforeEach:

.storybook/preview.js|ts
import type { Preview } from '@storybook/nextjs';
 
// 👇 Must include the `.mock` portion of filename to have mocks typed correctly
import { getRouter } from '@storybook/nextjs/navigation.mock';
 
const preview: Preview = {
  parameters: {
    nextjs: {
      // 👇 Override the default navigation properties
      navigation: {
        pathname: '/app/',
      },
    },
  },
  async beforeEach() {
    // 👇 Manipulate the default navigation method mocks
    getRouter().push.mockImplementation(() => {
      /* ... */
    });
  },
};

Next.js Head

next/head 开箱即用。你可以在故事中使用它,就像在 Next.js 应用中一样。请记住,Head children 放置在 Storybook 用于渲染你的故事的 iframe 的 head 元素中。

¥next/head is supported out of the box. You can use it in your stories like you would in your Next.js application. Please keep in mind, that the Head children are placed into the head element of the iframe that Storybook uses to render your stories.

样式

¥Styling

Sass/Scss

全局 Sass/Scss 样式表 也无需任何额外配置即可得到支持。只需将它们导入 .storybook/preview.js|ts

¥Global Sass/Scss stylesheets are supported without any additional configuration as well. Just import them into .storybook/preview.js|ts

.storybook/preview.js|ts
import '../styles/globals.scss';

这将自动将你的任何 自定义 Sass 配置 包含在你的 next.config.js 文件中。

¥This will automatically include any of your custom Sass configurations in your next.config.js file.

next.config.js
import * as path from 'path';
 
export default {
  // Any options here are included in Sass compilation for your stories
  sassOptions: {
    includePaths: [path.join(process.cwd(), 'styles')],
  },
};

CSS/Sass/Scss 模块

¥CSS/Sass/Scss Modules

CSS 模块 按预期工作。

¥CSS modules work as expected.

src/components/Button.jsx
// This import will work in Storybook
import styles from './Button.module.css';
// Sass/Scss is also supported
// import styles from './Button.module.scss'
// import styles from './Button.module.sass'
 
export function Button() {
  return (
    <button type="button" className={styles.error}>
      Destroy
    </button>
  );
}

样式化的 JSX

¥Styled JSX

Next.js 的内置 CSS-in-JS 解决方案是 styled-jsx,并且此框架也支持开箱即用,零配置。

¥The built in CSS-in-JS solution for Next.js is styled-jsx, and this framework supports that out of the box too, zero config.

src/components/HelloWorld.jsx
// This will work in Storybook
function HelloWorld() {
  return (
    <div>
      Hello world
      <p>scoped!</p>
      <style jsx>{`
        p {
          color: blue;
        }
        div {
          background: red;
        }
        @media (max-width: 600px) {
          div {
            background: blue;
          }
        }
      `}</style>
      <style global jsx>{`
        body {
          background: black;
        }
      `}</style>
    </div>
  );
}
 
export default HelloWorld;

你也可以使用自己的 babel 配置。这是一个如何自定义 styled-jsx 的示例。

¥You can use your own babel config too. This is an example of how you can customize styled-jsx.

// .babelrc (or whatever config file you use)
{
  "presets": [
    [
      "next/babel",
      {
        "styled-jsx": {
          "plugins": ["@styled-jsx/plugin-sass"]
        }
      }
    ]
  ]
}

PostCSS

Next.js 让你 自定义 PostCSS 配置。因此,该框架将自动为你处理 PostCSS 配置。

¥Next.js lets you customize PostCSS config. Thus this framework will automatically handle your PostCSS config for you.

这允许零配置 Tailwind 等很酷的东西!(参见 Next.js 示例)

¥This allows for cool things like zero-config Tailwind! (See Next.js' example)

导入

¥Imports

绝对导入

¥Absolute imports

支持来自根目录的 绝对导入

¥Absolute imports from the root directory are supported.

index.jsx|tsx
// All good!
import Button from 'components/button';
// Also good!
import styles from 'styles/HomePage.module.css';
 
export default function HomePage() {
  return (
    <>
      <h1 className={styles.title}>Hello World</h1>
      <Button />
    </>
  );
}

.storybook/preview.js|ts 中的全局样式也适用!

¥Also OK for global styles in .storybook/preview.js|ts!

.storybook/preview.js|ts
import 'styles/globals.scss';
 
// ...

绝对导入不能在故事/测试中模拟。请参阅 模拟模块 部分以获取更多信息。

¥Absolute imports cannot be mocked in stories/tests. See the Mocking modules section for more information.

模块别名

¥Module aliases

模块别名 也受支持。

¥Module aliases are also supported.

index.jsx|tsx
// All good!
import Button from '@/components/button';
// Also good!
import styles from '@/styles/HomePage.module.css';
 
export default function HomePage() {
  return (
    <>
      <h1 className={styles.title}>Hello World</h1>
      <Button />
    </>
  );
}

子路径导入

¥Subpath imports

作为 模块别名 的替代方案,你可以使用 子路径导入 导入模块。这遵循 Node 包标准,并且在 模拟模块 时有好处。

¥As an alternative to module aliases, you can use subpath imports to import modules. This follows Node package standards and has benefits when mocking modules.

要配置子路径导入,请在项目的 package.json 文件中定义 imports 属性。此属性将子路径映射到实际文件路径。以下示例为项目中的所有模块配置子路径导入:

¥To configure subpath imports, you define the imports property in your project's package.json file. This property maps the subpath to the actual file path. The example below configures subpath imports for all modules in the project:

package.json
{
  "imports": {
    "#*": ["./*", "./*.ts", "./*.tsx"]
  }
}

由于子路径导入替换了模块别名,因此你可以从 TypeScript 配置中删除路径别名。

¥Because subpath imports replace module aliases, you can remove the path aliases from your TypeScript configuration.

然后可以像这样使用:

¥Which can then be used like this:

index.jsx|tsx
import Button from '#components/button';
import styles from '#styles/HomePage.module.css';
 
export default function HomePage() {
  return (
    <>
      <h1 className={styles.title}>Hello World</h1>
      <Button />
    </>
  );
}

模拟模块

¥Mocking modules

组件通常依赖于导入到组件文件中的模块。这些可以来自外部包或项目内部。在 Storybook 中渲染这些组件或测试它们时,你可能希望 模拟这些模块 来控制和断言它们的行为。

¥Components often depend on modules that are imported into the component file. These can be from external packages or internal to your project. When rendering those components in Storybook or testing them, you may want to mock those modules to control and assert their behavior.

内置模拟模块

¥Built-in mocked modules

此框架为许多 Next.js 内部模块提供了模拟:

¥This framework provides mocks for many of Next.js' internal modules:

  1. @storybook/nextjs/cache.mock
  2. @storybook/nextjs/headers.mock
  3. @storybook/nextjs/navigation.mock
  4. @storybook/nextjs/router.mock

模拟其他模块

¥Mocking other modules

要模拟其他模块,请使用 automocking 或模拟模块指南中记录的 替代方法 之一。

¥To mock other modules, use automocking or one of the alternative methods documented in the mocking modules guide.

运行时配置

¥Runtime config

Next.js 允许 运行时配置,它允许你导入方便的 getConfig 函数以在运行时获取 next.config.js 文件中定义的某些配置。

¥Next.js allows for Runtime Configuration which lets you import a handy getConfig function to get certain configuration defined in your next.config.js file at runtime.

在具有此框架的 Storybook 上下文中,你可以期待 Next.js 的 运行时配置 功能正常工作。

¥In the context of Storybook with this framework, you can expect Next.js's Runtime Configuration feature to work just fine.

注意,由于 Storybook 不会在服务器渲染你的组件,因此你的组件只能看到它们通常在客户端看到的内容(即它们看不到 serverRuntimeConfig 但会看到 publicRuntimeConfig)。

¥Note, because Storybook doesn't server render your components, your components will only see what they normally see on the client side (i.e. they won't see serverRuntimeConfig but will see publicRuntimeConfig).

例如,考虑以下 Next.js 配置:

¥For example, consider the following Next.js config:

next.config.js
module.exports = {
  serverRuntimeConfig: {
    mySecret: 'secret',
    secondSecret: process.env.SECOND_SECRET, // Pass through env variables
  },
  publicRuntimeConfig: {
    staticFolder: '/static',
  },
};

在 Storybook 中调用 getConfig 时将返回以下对象:

¥Calls to getConfig would return the following object when called within Storybook:

// Runtime config
{
  "serverRuntimeConfig": {},
  "publicRuntimeConfig": {
    "staticFolder": "/static"
  }
}

自定义 Webpack 配置

¥Custom Webpack config

Next.js 附带了很多开箱即用的东西,比如 Sass 支持,但有时你会添加 对 Next.js 的自定义 Webpack 配置修改。此框架负责处理你想要添加的大多数 Webpack 修改。如果 Next.js 支持开箱即用的功能,那么该功能将在 Storybook 中开箱即用。如果 Next.js 不支持开箱即用的东西,但使其易于配置,那么这个框架将为 Storybook 做同样的事情。

¥Next.js comes with a lot of things for free out of the box like Sass support, but sometimes you add custom Webpack config modifications to Next.js. This framework takes care of most of the Webpack modifications you would want to add. If Next.js supports a feature out of the box, then that feature will work out of the box in Storybook. If Next.js doesn't support something out of the box, but makes it easy to configure, then this framework will do the same for that thing for Storybook.

Storybook 所需的任何 Webpack 修改都应在 .storybook/main.js|ts 中进行。

¥Any Webpack modifications desired for Storybook should be made in .storybook/main.js|ts.

注意:并非所有 Webpack 修改都可以在 next.config.js.storybook/main.js|ts 之间复制/粘贴。建议你研究如何正确修改 Storybook 的 Webpack 配置以及 Webpack 工作原理

¥Note: Not all Webpack modifications are copy/paste-able between next.config.js and .storybook/main.js|ts. It is recommended to do your research on how to properly make your modification to Storybook's Webpack config and on how Webpack works.

下面是如何使用此框架向 Storybook 添加 SVGR 支持的示例。

¥Below is an example of how to add SVGR support to Storybook with this framework.

.storybook/main.ts
import type { StorybookConfig } from '@storybook/nextjs';
 
const config: StorybookConfig = {
  // ...
  webpackFinal: async (config) => {
    config.module = config.module || {};
    config.module.rules = config.module.rules || [];
 
    // This modifies the existing image rule to exclude .svg files
    // since you want to handle those files with @svgr/webpack
    const imageRule = config.module.rules.find((rule) => rule?.['test']?.test('.svg'));
    if (imageRule) {
      imageRule['exclude'] = /\.svg$/;
    }
 
    // Configure .svg files to be loaded with @svgr/webpack
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    });
 
    return config;
  },
};
 
export default config;

Typescript

Storybook 处理大多数 Typescript 配置,但此框架增加了对 Next.js 对 绝对导入和模块路径别名 的支持。简而言之,它会考虑你的 tsconfig.jsonbaseUrlpaths。因此,像下面这样的 tsconfig.json 可以开箱即用。

¥Storybook handles most Typescript configurations, but this framework adds additional support for Next.js's support for Absolute Imports and Module path aliases. In short, it takes into account your tsconfig.json's baseUrl and paths. Thus, a tsconfig.json like the one below would work out of the box.

tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/components/*": ["components/*"]
    }
  }
}

React 服务器组件 (RSC)

¥React Server Components (RSC)

(⚠️实验性)

¥(⚠️ Experimental)

如果你的应用使用 React 服务器组件 (RSC),Storybook 可以在浏览器中的故事中渲染它们。

¥If your app uses React Server Components (RSC), Storybook can render them in stories in the browser.

要启用此功能,请在 .storybook/main.js|ts 配置中设置 experimentalRSC 功能标志:

¥To enable this set the experimentalRSC feature flag in your .storybook/main.js|ts config:

.storybook/main.ts
// Replace your-framework with nextjs or nextjs-vite
import type { StorybookConfig } from '@storybook/your-framework';
 
const config: StorybookConfig = {
  // ...
  features: {
    experimentalRSC: true,
  },
};
 
export default config;

设置此标志会自动将你的故事封装在 Suspense 封装器中,该封装器能够在 NextJS 版本的 React 中渲染异步组件。

¥Setting this flag automatically wraps your story in a Suspense wrapper, which is able to render asynchronous components in NextJS's version of React.

如果此封装器导致你现有的任何故事出现问题,你可以在全局/组件/故事级别使用 react.rsc 参数 有选择地禁用它:

¥If this wrapper causes problems in any of your existing stories, you can selectively disable it using the react.rsc parameter at the global/component/story level:

MyServerComponent.stories.ts
// Replace your-framework with nextjs or nextjs-vite
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import MyServerComponent from './MyServerComponent';
 
const meta = {
  component: MyServerComponent,
  parameters: {
    react: { rsc: false },
  },
} satisfies Meta<typeof MyServerComponent>;
export default meta;

请注意,如果你的服务器组件访问服务器端资源(如文件系统或特定于 Node 的库),则将服务器组件封装在 Suspense 中无济于事。要解决此问题,你需要使用 Webpack 别名storybook-addon-module-mock 等插件模拟数据访问层。

¥Note that wrapping your server components in Suspense does not help if your server components access server-side resources like the file system or Node-specific libraries. To work around this, you'll need to mock out your data access layer using Webpack aliases or an addon like storybook-addon-module-mock.

如果你的服务器组件通过网络访问数据,我们建议使用 MSW Storybook 插件 来模拟网络请求。

¥If your server components access data via the network, we recommend using the MSW Storybook Addon to mock network requests.

将来,我们将在 Storybook 中提供更好的模拟支持和对 服务器操作 的支持。

¥In the future we will provide better mocking support in Storybook and support for Server Actions.

Yarn v2 和 v3 用户的注意事项

¥Notes for Yarn v2 and v3 users

如果你使用 Yarn v2 或 v3,你可能会遇到 Storybook 无法解析 style-loadercss-loader 的问题。例如,你可能会收到如下错误:

¥If you're using Yarn v2 or v3, you may run into issues where Storybook can't resolve style-loader or css-loader. For example, you might get errors like:

Module not found: Error: Can't resolve 'css-loader'
Module not found: Error: Can't resolve 'style-loader'

这是因为这些版本的 Yarn 具有与 Yarn v1.x 不同的包解析规则。如果你遇到这种情况,请直接安装该包。

¥This is because those versions of Yarn have different package resolution rules than Yarn v1.x. If this is the case for you, please install the package directly.

常见问题

¥FAQ

如何手动安装 Next.js 框架?

¥How do I manually install the Next.js framework?

首先,安装框架:

¥First, install the framework:

npm install --save-dev @storybook/nextjs

然后,更新你的 .storybook/main.js|ts 以更改框架属性:

¥Then, update your .storybook/main.js|ts to change the framework property:

.storybook/main.ts
- 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;

最后,如果你使用 Storybook 插件与 Next.js 集成,则使用此框架时不再需要这些插件,可以将其删除:

¥Finally, if you were using Storybook plugins to integrate with Next.js, those are no longer necessary when using this framework and can be removed:

.storybook/main.ts
// Replace your-framework with nextjs or nextjs-vite
import type { StorybookConfig } from '@storybook/your-framework';
 
const config: StorybookConfig = {
  // ...
  addons: [
    // ...
    // 👇 These can both be removed
    // 'storybook-addon-next',
    // 'storybook-addon-next-router',
  ],
};
 
export default config;

如何迁移到 Next.js Vite 框架?

¥How do I migrate to the Next.js Vite framework?

请参阅 @storybook/nextjs-vite 的迁移说明

¥Please refer to the migration instructions for @storybook/nextjs-vite.

获取数据的页面/组件的故事

¥Stories for pages/components which fetch data

Next.js 页面可以直接在 app 目录中的服务器组件中获取数据,这些组件通常包括仅在节点环境中运行的模块导入。这在 Storybook 中(目前)不起作用,因为如果你从包含故事中这些节点模块导入的 Next.js 页面文件导入,则 Storybook 的 Webpack 将崩溃,因为这些模块不会在浏览器中运行。为了解决这个问题,你可以将页面文件中的组件提取到一个单独的文件中,然后将该纯组件导入到你的故事中。或者,如果由于某种原因不可行,你可以在 Storybook 的 webpackFinal 配置 中使用 填充这些模块

¥Next.js pages can fetch data directly within server components in the app directory, which often include module imports that only run in a node environment. This does not (currently) work within Storybook, because if you import from a Next.js page file containing those node module imports in your stories, your Storybook's Webpack will crash because those modules will not run in a browser. To get around this, you can extract the component in your page file into a separate file and import that pure component in your stories. Or, if that's not feasible for some reason, you can polyfill those modules in your Storybook's webpackFinal configuration.

之前

¥Before

app/my-page/index.jsx
async function getData() {
  const res = await fetch(...);
  // ...
}
 
// Using this component in your stories will break the Storybook build
export default async function Page() {
  const data = await getData();
 
  return // ...
}

之后

¥After

app/my-page/index.jsx
// Use this component in your stories
import MyPage from './components/MyPage';
 
async function getData() {
  const res = await fetch(...);
  // ...
}
 
export default async function Page() {
  const data = await getData();
 
  return <MyPage {...data} />;
}

静态导入的图片不会加载

¥Statically imported images won't load

确保你以与在正常开发中使用 next/image 时相同的方式处理图片导入。

¥Make sure you are treating image imports the same way you treat them when using next/image in normal development.

在使用此框架之前,图片导入将导入图片的原始路径(例如 'static/media/stories/assets/logo.svg')。现在图片导入适用于 "Next.js 方式",这意味着你现在在导入图片时会获得一个对象。例如:

¥Before using this framework, image imports would import the raw path to the image (e.g. 'static/media/stories/assets/logo.svg'). Now image imports work the "Next.js way", meaning that you now get an object when importing an image. For example:

// Image import object
{
  "src": "static/media/stories/assets/logo.svg",
  "height": 48,
  "width": 48,
  "blurDataURL": "static/media/stories/assets/logo.svg"
}

因此,如果 Storybook 中的某些内容未正确显示图片,请确保你期望从导入中返回对象,而不仅仅是资源路径。

¥Therefore, if something in Storybook isn't showing the image properly, make sure you expect the object to be returned from an import instead of only the asset path.

有关 Next.js 如何处理静态图片导入的更多详细信息,请参阅 本地图片

¥See local images for more detail on how Next.js treats static image imports.

未找到模块:错误:无法解析 package name

¥Module not found: Error: Can't resolve package name

如果你使用的是 Yarn v2 或 v3,你可能会得到这个。有关更多详细信息,请参阅 Yarn v2 和 v3 用户的注意事项

¥You might get this if you're using Yarn v2 or v3. See Notes for Yarn v2 and v3 users for more details.

我应该使用 Vite 版本还是 Webpack 版本?

¥Should I use the Vite or Webpack version?

我们建议大多数项目使用 @storybook/nextjs-vite(基于 Vite),因为它构建速度更快、测试支持更好、配置更简单。但是,如果你的项目有与 Vite 不兼容的自定义 Webpack 配置,请改用此框架。

¥We recommend using @storybook/nextjs-vite (Vite-based) for most projects because it offers faster builds, better test support, and a simpler configuration. However, if your project has custom Webpack configurations that are incompatible with Vite, use this framework instead.

错误:你正在导入 avif 图片,但尚未安装 sharp。你必须安装 sharp 才能在 Next.js 中使用图片优化功能。

¥Error: You are importing avif images, but you don't have sharp installed. You have to install sharp in order to use image optimization features in Next.js.

sharp 是 Next.js 图片优化功能的依赖。如果你看到此错误,则需要在你的项目中安装 sharp

¥sharp is a dependency of Next.js's image optimization feature. If you see this error, you need to install sharp in your project.

npm install sharp
yarn add sharp
pnpm add sharp

你可以参考 Next.js 文档中的 安装 sharp 以使用内置图片优化 了解更多信息。

¥You can refer to the Install sharp to Use Built-In Image Optimization in the Next.js documentation for more information.

API

模块

¥Modules

@storybook/nextjs 包导出了多个模块,使你能够 mock Next.js 的内部行为。

¥The @storybook/nextjs package exports several modules that enable you to mock Next.js's internal behavior.

@storybook/nextjs/export-mocks

类型:{ getPackageAliases: ({ useESM?: boolean }) => void }

¥Type: { getPackageAliases: ({ useESM?: boolean }) => void }

getPackageAliases 是一个用于生成设置 可移植故事 所需别名的助手。

¥getPackageAliases is a helper for generating the aliases needed to set up portable stories.

jest.config.ts
import type { Config } from 'jest';
import nextJest from 'next/jest.js';
// 👇 Import the utility function
import { getPackageAliases } from '@storybook/nextjs/export-mocks';
 
const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './',
});
 
const config: Config = {
  testEnvironment: 'jsdom',
  // ... rest of Jest config
  moduleNameMapper: {
    ...getPackageAliases(), // 👈 Add the utility as mapped module names
  },
};
 
export default createJestConfig(config);

@storybook/nextjs/cache.mock

类型:typeof import('next/cache')

¥Type: typeof import('next/cache')

此模块导出 next/cache 模块导出的模拟实现。你可以使用它来创建自己的模拟实现或在故事的 播放函数 中对模拟调用进行断言。

¥This module exports mocked implementations of the next/cache module's exports. You can use it to create your own mock implementations or assert on mock calls in a story's play function.

MyForm.stories.ts
// Replace your-framework with nextjs or nextjs-vite
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { expect } from 'storybook/test';
 
// 👇 Must include the `.mock` portion of filename to have mocks typed correctly
import { revalidatePath } from '@storybook/your-framework/cache.mock';
 
import MyForm from './my-form';
 
const meta = {
  component: MyForm,
} satisfies Meta<typeof MyForm>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const Submitted: Story = {
  async play({ canvas, userEvent }) {
    const submitButton = canvas.getByRole('button', { name: /submit/i });
    await userEvent.click(saveButton);
    // 👇 Use any mock assertions on the function
    await expect(revalidatePath).toHaveBeenCalledWith('/');
  },
};

@storybook/nextjs/headers.mock

类型:来自 Next.js 的 cookiesheadersdraftMode

¥Type: cookies, headers and draftMode from Next.js

此模块导出 next/headers 模块导出的可写模拟实现。你可以使用它来设置在故事中读取的 cookie 或标头,并在稍后断言它们已被调用。

¥This module exports writable mocked implementations of the next/headers module's exports. You can use it to set up cookies or headers that are read in your story, and to later assert that they have been called.

Next.js 的默认 headers() 导出是只读的,但此模块公开了允许你写入标题的方法:

¥Next.js's default headers() export is read-only, but this module exposes methods allowing you to write to the headers:

  • headers().append(name: string, value: string):如果值已经存在,则将其附加到标题。

    ¥headers().append(name: string, value: string): Appends the value to the header if it exists already.

  • headers().delete(name: string):删除标题

    ¥headers().delete(name: string): Deletes the header

  • headers().set(name: string, value: string):将标题设置为提供的值。

    ¥headers().set(name: string, value: string): Sets the header to the value provided.

对于 cookies,你可以使用现有的 API 来编写它们。例如,cookies().set('firstName', 'Jane')

¥For cookies, you can use the existing API to write them. E.g., cookies().set('firstName', 'Jane').

因为 headers()cookies() 及其子功能都是模拟的,所以你可以在故事中使用任何 模拟实用程序,比如 headers().getAll.mock.calls

¥Because headers(), cookies() and their sub-functions are all mocks you can use any mock utilities in your stories, like headers().getAll.mock.calls.

MyForm.stories.ts
// Replace your-framework with nextjs or nextjs-vite
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { expect } from 'storybook/test';
 
// 👇 Must include the `.mock` portion of filename to have mocks typed correctly
import { cookies, headers } from '@storybook/your-framework/headers.mock';
 
import MyForm from './my-form';
 
const meta = {
  component: MyForm,
} satisfies Meta<typeof MyForm>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const LoggedInEurope: Story = {
  async beforeEach() {
    // 👇 Set mock cookies and headers ahead of rendering
    cookies().set('username', 'Sol');
    headers().set('timezone', 'Central European Summer Time');
  },
  async play() {
    // 👇 Assert that your component called the mocks
    await expect(cookies().get).toHaveBeenCalledOnce();
    await expect(cookies().get).toHaveBeenCalledWith('username');
    await expect(headers().get).toHaveBeenCalledOnce();
    await expect(cookies().get).toHaveBeenCalledWith('timezone');
  },
};

@storybook/nextjs/navigation.mock

类型:typeof import('next/navigation') & getRouter: () => ReturnType<typeof import('next/navigation')['useRouter']>

¥Type: typeof import('next/navigation') & getRouter: () => ReturnType<typeof import('next/navigation')['useRouter']>

此模块导出 next/navigation 模块导出的模拟实现。它还导出一个 getRouter 函数,该函数返回 来自 useRouter 的 Next.js 的 router 对象 的模拟版本,允许操作和断言属性。你可以在故事的 播放函数 中使用它模拟实现或对模拟调用进行断言。

¥This module exports mocked implementations of the next/navigation module's exports. It also exports a getRouter function that returns a mocked version of Next.js's router object from useRouter, allowing the properties to be manipulated and asserted on. You can use it mock implementations or assert on mock calls in a story's play function.

MyForm.stories.ts
// Replace your-framework with nextjs or nextjs-vite
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { expect } from 'storybook/test';
 
// 👇 Must include the `.mock` portion of filename to have mocks typed correctly
import { redirect, getRouter } from '@storybook/your-framework/navigation.mock';
 
import MyForm from './my-form';
 
const meta = {
  component: MyForm,
  parameters: {
    nextjs: {
      // 👇 As in the Next.js application, next/navigation only works using App Router
      appDirectory: true,
    },
  },
} satisfies Meta<typeof MyForm>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const Unauthenticated: Story = {
  async play() {
    // 👇 Assert that your component called redirect()
    await expect(redirect).toHaveBeenCalledWith('/login', 'replace');
  },
};
 
export const GoBack: Story = {
  async play({ canvas, userEvent }) {
    const backBtn = await canvas.findByText('Go back');
 
    await userEvent.click(backBtn);
    // 👇 Assert that your component called back()
    await expect(getRouter().back).toHaveBeenCalled();
  },
};

@storybook/nextjs/router.mock

类型:typeof import('next/router') & getRouter: () => ReturnType<typeof import('next/router')['useRouter']>

¥Type: typeof import('next/router') & getRouter: () => ReturnType<typeof import('next/router')['useRouter']>

此模块导出 next/router 模块导出的模拟实现。它还导出一个 getRouter 函数,该函数返回 来自 useRouter 的 Next.js 的 router 对象 的模拟版本,允许操作和断言属性。你可以在故事的 播放函数 中使用它模拟实现或对模拟调用进行断言。

¥This module exports mocked implementations of the next/router module's exports. It also exports a getRouter function that returns a mocked version of Next.js's router object from useRouter, allowing the properties to be manipulated and asserted on. You can use it mock implementations or assert on mock calls in a story's play function.

MyForm.stories.ts
// Replace your-framework with nextjs or nextjs-vite
import type { Meta, StoryObj } from '@storybook/your-framework';
 
import { expect } from 'storybook/test';
 
// 👇 Must include the `.mock` portion of filename to have mocks typed correctly
import { getRouter } from '@storybook/your-framework/router.mock';
 
import MyForm from './my-form';
 
const meta = {
  component: MyForm,
} satisfies Meta<typeof MyForm>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const GoBack: Story = {
  async play({ canvas, userEvent }) {
    const backBtn = await canvas.findByText('Go back');
 
    await userEvent.click(backBtn);
    // 👇 Assert that your component called back()
    await expect(getRouter().back).toHaveBeenCalled();
  },
};

选项

¥Options

如果需要,你可以传递一个选项对象以进行其他配置:

¥You can pass an options object for additional configuration if needed:

.storybook/main.js
import * as path from 'path';
 
export default {
  // ...
  framework: {
    name: '@storybook/nextjs',
    options: {
      image: {
        loading: 'eager',
      },
      nextConfigPath: path.resolve(process.cwd(), 'next.config.js'),
    },
  },
};

可用选项包括:

¥The available options are:

builder

类型:Record<string, any>

¥Type: Record<string, any>

配置 框架的构建器 的选项。对于 Next.js,可用选项可以在 Webpack 构建器文档 中找到。

¥Configure options for the framework's builder. For Next.js, available options can be found in the Webpack builder docs.

image

类型:object

¥Type: object

传递给每个 next/image 实例的属性。有关更多详细信息,请参阅 next/image 文档

¥Props to pass to every instance of next/image. See next/image docs for more details.

nextConfigPath

类型:string

¥Type: string

next.config.js 文件的绝对路径。如果你有一个不在项目根目录中的自定义 next.config.js 文件,则这是必需的。

¥The absolute path to the next.config.js file. This is necessary if you have a custom next.config.js file that is not in the root directory of your project.

参数

¥Parameters

此框架在 nextjs 命名空间下为 Storybook 贡献了以下 参数

¥This framework contributes the following parameters to Storybook, under the nextjs namespace:

appDirectory

类型:boolean

¥Type: boolean

默认:false

¥Default: false

如果你的故事导入了使用 next/navigation 的组件,则需要将参数 nextjs.appDirectory 设置为 true。因为这是一个参数,所以你可以将其应用于 单个故事组件的所有故事Storybook 中的每个故事。有关更多详细信息,请参阅 Next.js 导航

¥If your story imports components that use next/navigation, you need to set the parameter nextjs.appDirectory to true. Because this is a parameter, you can apply it to a single story, all stories for a component, or every story in your Storybook. See Next.js Navigation for more details.

类型:

¥Type:

{
  asPath?: string;
  pathname?: string;
  query?: Record<string, string>;
  segments?: (string | [string, string])[];
}

默认值:

¥Default value:

{
  segments: [];
}

传递给 next/navigation 上下文的路由对象。有关更多详细信息,请参阅 Next.js 的导航文档

¥The router object that is passed to the next/navigation context. See Next.js's navigation docs for more details.

router

类型:

¥Type:

{
  asPath?: string;
  pathname?: string;
  query?: Record<string, string>;
}

传递给 next/router 上下文的路由对象。有关更多详细信息,请参阅 Next.js 的路由文档

¥The router object that is passed to the next/router context. See Next.js's router docs for more details.