文件
Storybook 文件

適用於 Next.js 的 Storybook

適用於 Next.js 的 Storybook 是一個框架,可讓您輕鬆地為 Next.js 應用程式獨立開發和測試 UI 元件。它包含

  • 🔀 路由
  • 🖼 圖片最佳化
  • ⤵️ 絕對匯入
  • 🎨 樣式
  • 🎛 Webpack 和 Babel 設定
  • 💫 以及更多!

需求

  • Next.js ≥ 13.5
  • Storybook ≥ 7.0

開始使用

在沒有 Storybook 的專案中

在您的 Next.js 專案根目錄中執行此命令後,按照提示操作

npm create storybook@latest

更多關於開始使用 Storybook 的資訊。

在有 Storybook 的專案中

此框架旨在與 Storybook 7+ 版本搭配使用。如果您尚未使用 v7,請使用此命令升級

npx storybook@latest upgrade

自動遷移

執行上述 upgrade 命令時,您應該會收到提示,詢問您是否要遷移至 @storybook/nextjs,這應該會為您處理一切。如果自動遷移不適用於您的專案,請參閱下方的手動遷移。

手動遷移

首先,安裝框架

npm install --save-dev @storybook/nextjs

然後,更新您的 .storybook/main.js|ts 以變更 framework 屬性

.storybook/main.ts
import { StorybookConfig } from '@storybook/nextjs';
 
const config: StorybookConfig = {
  // ...
  // framework: '@storybook/react-webpack5', 👈 Remove this
  framework: '@storybook/nextjs', // 👈 Add this
};
 
export default config;

最後,如果您先前使用 Storybook 外掛程式與 Next.js 整合,則在使用此框架時不再需要這些外掛程式,可以移除

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

使用 Vite

(⚠️ 實驗性功能

您可以使用我們新推出的實驗性 @storybook/experimental-nextjs-vite 框架,此框架以 Vite 為基礎,並移除對 Webpack 和 Babel 的需求。它支援此處記錄的所有功能。

搭配 Vite 使用 Next.js 框架需要 Next.js 14.1.0 或更高版本。

npm install --save-dev @storybook/experimental-nextjs-vite

然後,更新您的 .storybook/main.js|ts 以變更 framework 屬性

.storybook/main.ts
import { StorybookConfig } from '@storybook/experimental-nextjs-vite';
 
const config: StorybookConfig = {
  // ...
  // framework: '@storybook/react-webpack5', 👈 Remove this
  framework: '@storybook/experimental-nextjs-vite', // 👈 Add this
};
 
export default config;

如果您的 Storybook 設定在 webpackFinal 中包含自訂 Webpack 操作,您可能需要在 viteFinal 中建立對等項目。

如需更多資訊,請參閱 Vite 建置器文件

最後,如果您先前使用 Storybook 外掛程式與 Next.js 整合,則在使用此框架時不再需要這些外掛程式,可以移除

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

執行設定精靈

如果一切順利,您應該會看到設定精靈,它將協助您開始使用 Storybook,向您介紹主要概念和功能,包括 UI 的組織方式、如何撰寫您的第一個 story,以及如何利用 controls 測試您的元件對各種輸入的回應。

Storybook onboarding

如果您跳過了精靈,您可以隨時透過將 ?path=/onboarding 查詢參數新增至您的 Storybook 執行個體的 URL 再次執行,前提是範例 stories 仍然可用。

Next.js 的 Image 元件

此框架可讓您使用 Next.js 的 next/image,無需任何設定。

本機圖片

支援本機圖片

// 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>
    </>
  );
}

遠端圖片

也支援遠端圖片

// 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 字型最佳化

Storybook 部分支援 next/fontnext/font/googlenext/font/local 套件受到支援。

next/font/google

您無需執行任何操作。next/font/google 隨即支援。

next/font/local

對於本機字型,您必須定義 src 屬性。路徑是相對於呼叫字型載入器函式的目錄。

如果以下元件以下列方式定義您的 localFont

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

staticDir 對應

如果您使用 @storybook/experimental-nextjs-vite 而非 @storybook/nextjs,您可以安全地跳過此章節。以 Vite 為基礎的框架會自動處理對應。

您必須透過 staticDirs 設定,告知 Storybook fonts 目錄的位置。from 值是相對於 .storybook 目錄。to 值是相對於 Storybook 的執行環境。很可能它是您專案的根目錄。

.storybook/main.ts
import { StorybookConfig } from '@storybook/nextjs';
 
const config: StorybookConfig = {
  // ...
  staticDirs: [
    {
      from: '../src/components/fonts',
      to: 'src/components/fonts',
    },
  ],
};
 
export default config;

next/font 的不支援功能

以下功能尚不支援(但)。未來可能會計劃支援這些功能

在測試期間模擬字型

有時,從 Google 擷取字型可能會在您的 Storybook 建置步驟中失敗。強烈建議您模擬這些請求,因為這些失敗也可能導致您的管線失敗。Next.js 支援透過 JavaScript 模組模擬字型,該模組位於 env var NEXT_FONT_GOOGLE_MOCKED_RESPONSES 參考的位置。

例如,使用 GitHub Actions

# .github/workflows/ci.yml
- uses: chromaui/action@v1
  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 }}

您模擬的字型看起來會像這樣

// 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 的路由器會自動被虛設,因此當路由器互動時,如果您有 Storybook actions 附加元件,則其所有互動都會自動記錄到 Actions 面板。

您應該只在 pages 目錄中使用 next/router。在 app 目錄中,必須使用 next/navigation

覆寫預設值

每個 story 的覆寫可以透過將 nextjs.router 屬性新增至 story parameters 來完成。框架會將您在此處放置的任何內容淺層合併到路由器中。

RouterBasedComponent.stories.ts
import { Meta, StoryObj } from '@storybook/react';
 
import RouterBasedComponent from './RouterBasedComponent';
 
const meta: Meta<typeof RouterBasedComponent> = {
  component: RouterBasedComponent,
};
export default meta;
 
type Story = StoryObj<typeof RouterBasedComponent>;
 
// If you have the actions addon,
// you can interact with the links and see the route change events there
export const Example: Story = {
  parameters: {
    nextjs: {
      router: {
        pathname: '/profile/[id]',
        asPath: '/profile/1',
        query: {
          id: '1',
        },
      },
    },
  },
};

這些覆寫也可以應用於元件的所有 stories專案中的所有 stories。標準的參數繼承規則適用。

預設路由器

虛設路由器的預設值如下(如需關於全域變數如何運作的更多詳細資訊,請參閱全域變數)。

// Default router
const defaultRouter = {
  // The locale should be configured globally: https://storybook.dev.org.tw/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 操作和斷言的模擬函式。

若要覆寫這些預設值,您可以使用 parametersbeforeEach

// .storybook/preview.ts
import { Preview } from '@storybook/react';
// 👇 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/navigation 只能在 app 目錄中的元件/頁面中使用。

nextjs.appDirectory 設定為 true

如果您的 story 匯入使用 next/navigation 的元件,您需要在該元件的 stories 中將參數 nextjs.appDirectory 設定為 true

NavigationBasedComponent.stories.ts
import { Meta, StoryObj } from '@storybook/react';
 
import NavigationBasedComponent from './NavigationBasedComponent';
 
const meta: Meta<typeof NavigationBasedComponent> = {
  component: NavigationBasedComponent,
  parameters: {
    nextjs: {
      appDirectory: true, // 👈 Set this
    },
  },
};
export default meta;

如果您的 Next.js 專案對每個頁面都使用 app 目錄(換句話說,它沒有 pages 目錄),您可以將參數 nextjs.appDirectory 設定為 true.storybook/preview.js|ts 檔案中,以將其套用至所有 stories。

.storybook/preview.ts
import { Preview } from '@storybook/react';
 
const preview: Preview = {
  // ...
  parameters: {
    // ...
    nextjs: {
      appDirectory: true,
    },
  },
};
 
export default preview;

覆寫預設值

每個 story 的覆寫可以透過將 nextjs.navigation 屬性新增至 story parameters 來完成。框架會將您在此處放置的任何內容淺層合併到路由器中。

NavigationBasedComponent.stories.ts
import { Meta, StoryObj } from '@storybook/react';
 
import NavigationBasedComponent from './NavigationBasedComponent';
 
const meta: Meta<typeof NavigationBasedComponent> = {
  component: NavigationBasedComponent,
  parameters: {
    nextjs: {
      appDirectory: true,
    },
  },
};
export default meta;
 
type Story = StoryObj<typeof NavigationBasedComponent>;
 
// If you have the actions addon,
// you can interact with the links and see the route change events there
export const Example: Story = {
  parameters: {
    nextjs: {
      navigation: {
        pathname: '/profile',
        query: {
          user: '1',
        },
      },
    },
  },
};

這些覆寫也可以應用於元件的所有 stories專案中的所有 stories。標準的參數繼承規則適用。

useSelectedLayoutSegmentuseSelectedLayoutSegmentsuseParams hooks

Storybook 支援 useSelectedLayoutSegmentuseSelectedLayoutSegmentsuseParams hooks。您必須設定 nextjs.navigation.segments 參數,以傳回您想要使用的區段或 params。

NavigationBasedComponent.stories.ts
import { Meta, StoryObj } from '@storybook/react';
 
import NavigationBasedComponent from './NavigationBasedComponent';
 
const meta: Meta<typeof NavigationBasedComponent> = {
  component: NavigationBasedComponent,
  parameters: {
    nextjs: {
      appDirectory: true,
      navigation: {
        segments: ['dashboard', 'analytics'],
      },
    },
  },
};
export default meta;

透過以上設定,在 stories 中呈現的元件將會從 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 值。

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

透過以上設定,在 stories 中呈現的元件將會從 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" }
  ...
}

這些覆寫也可以應用於單個 story專案中的所有 stories。標準的參數繼承規則適用。

如果未設定,nextjs.navigation.segments 的預設值為 []

預設導覽內容

虛設導覽內容的預設值如下

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

此外,router 物件 包含所有原始方法(例如 push()replace() 等),作為可以使用 常規模擬 API 操作和斷言的模擬函式。

若要覆寫這些預設值,您可以使用 parametersbeforeEach

// .storybook/preview.ts
import { Preview } from '@storybook/react';
// 👇 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 隨即支援。您可以在您的 stories 中使用它,就像在您的 Next.js 應用程式中一樣。請記住,Head children 會放置在 Storybook 用於呈現您的 stories 的 iframe 的 head 元素中。

Sass/Scss

也支援全域 Sass/Scss 樣式表,無需任何額外設定。只需將它們匯入 .storybook/preview.js|ts

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

這將自動包含您的 next.config.js 檔案中的任何自訂 Sass 設定

// 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(__dirname, 'styles')],
  },
};

CSS/Sass/Scss 模組

CSS 模組 運作如預期。

// 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>
  );
}

Styled JSX

Next.js 的內建 CSS-in-JS 解決方案是 styled-jsx,而此框架也隨即支援,零設定。

// 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 的範例。

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

PostCSS

Next.js 可讓您自訂 PostCSS 設定。因此,此框架將自動為您處理您的 PostCSS 設定。

這允許像零設定 Tailwind 這樣的酷炫功能!(請參閱 Next.js 的範例

絕對路徑匯入

絕對路徑匯入 根目錄的路徑是被支援的。

// index.jsx
// 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 中使用全域樣式也是可以的!

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

絕對路徑匯入在 stories/tests 中無法被 mock。請參閱 Mocking modules 章節以獲取更多資訊。

模組別名

模組別名 也被支援。

// index.jsx
// 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 />
    </>
  );
}

子路徑匯入

作為 模組別名 的替代方案,您可以使用 子路徑匯入 來匯入模組。這遵循 Node 套件標準,並在 mock 模組 時具有優勢。

要設定子路徑匯入,您需要在專案的 package.json 檔案中定義 imports 屬性。此屬性將子路徑對應到實際檔案路徑。以下範例設定專案中所有模組的子路徑匯入

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

因為子路徑匯入取代了模組別名,您可以從 TypeScript 設定中移除路徑別名。

然後可以像這樣使用

// index.jsx
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 />
    </>
  );
}

Mock 模組

元件通常依賴於匯入到元件檔案中的模組。這些模組可能來自外部套件或專案內部。當在 Storybook 中渲染這些元件或進行測試時,您可能希望 mock 這些模組 以控制和斷言它們的行為。

內建 mock 模組

此框架為許多 Next.js 的內部模組提供了 mock

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

Mock 其他模組

在 Storybook 中 mock 其他模組的方式取決於您如何將模組匯入到元件中。

無論使用哪種方法,第一步都是 建立 mock 檔案。以下是一個名為 session 的模組的 mock 檔案範例

lib/session.mock.ts
import { fn } from '@storybook/test';
import * as actual from './session';
 
export * from './session';
export const getUserFromSession = fn(actual.getUserFromSession).mockName('getUserFromSession');

使用子路徑匯入

如果您正在使用 子路徑匯入,您可以調整設定以應用 條件,以便在 Storybook 內部使用 mock 模組。以下範例設定了四個內部模組的子路徑匯入,這些模組隨後在 Storybook 中被 mock

package.json
{
  "imports": {
    "#api": {
      // storybook condition applies to Storybook
      "storybook": "./api.mock.ts",
      "default": "./api.ts",
    },
    "#app/actions": {
      "storybook": "./app/actions.mock.ts",
      "default": "./app/actions.ts",
    },
    "#lib/session": {
      "storybook": "./lib/session.mock.ts",
      "default": "./lib/session.ts",
    },
    "#lib/db": {
      // test condition applies to test environments *and* Storybook
      "test": "./lib/db.mock.ts",
      "default": "./lib/db.ts",
    },
    "#*": ["./*", "./*.ts", "./*.tsx"],
  },
}

每個子路徑都必須以 # 開頭,以將其與常規模組路徑區分開來。#* 條目是一個 catch-all,它將所有子路徑對應到根目錄。

使用模組別名

如果您正在使用 模組別名,您可以將 Webpack 別名添加到您的 Storybook 設定,以指向 mock 檔案。

.storybook/main.ts
// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite)
import type { StorybookConfig } from '@storybook/your-framework';
 
const config: StorybookConfig = {
  framework: '@storybook/your-framework',
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  viteFinal: async (config) => {
    if (config.resolve) {
      config.resolve.alias = {
        ...config.resolve?.alias,
        // 👇 External module
        lodash: require.resolve('./lodash.mock'),
        // 👇 Internal modules
        '@/api': path.resolve(__dirname, './api.mock.ts'),
        '@/app/actions': path.resolve(__dirname, './app/actions.mock.ts'),
        '@/lib/session': path.resolve(__dirname, './lib/session.mock.ts'),
        '@/lib/db': path.resolve(__dirname, './lib/db.mock.ts'),
      };
    }
 
    return config;
  },
};
 
export default config;

執行階段設定

Next.js 允許 執行階段設定,讓您可以匯入方便的 getConfig 函數,以在執行階段取得在 next.config.js 檔案中定義的特定設定。

在此框架的 Storybook 環境中,您可以期望 Next.js 的 執行階段設定 功能運作良好。

請注意,由於 Storybook 不會伺服器端渲染您的元件,因此您的元件只會看到它們通常在客戶端看到的內容(即,它們不會看到 serverRuntimeConfig,但會看到 publicRuntimeConfig)。

例如,考慮以下 Next.js 設定

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

在 Storybook 內呼叫 getConfig 將傳回以下物件

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

自訂 Webpack 設定

如果您正在使用 @storybook/experimental-nextjs-vite 而不是 @storybook/nextjs,則可以安全地跳過此章節。基於 Vite 的 Next.js 框架不支援 Webpack 設定。

Next.js 免費提供了許多功能,例如 Sass 支援,但有時您會將 自訂 Webpack 設定修改添加到 Next.js。此框架處理了您可能想要添加的大多數 Webpack 修改。如果 Next.js 預設支援某項功能,則該功能將在 Storybook 中預設運作。如果 Next.js 預設不支援某些功能,但使其易於設定,則此框架將為 Storybook 執行相同的操作。

任何希望用於 Storybook 的 Webpack 修改都應在 .storybook/main.js|ts 中進行。

注意:並非所有 Webpack 修改都可以在 next.config.js.storybook/main.js|ts 之間複製/貼上。建議您研究如何正確地對 Storybook 的 Webpack 設定進行修改,以及 Webpack 的運作方式

以下是如何使用此框架將 SVGR 支援添加到 Storybook 的範例。

.storybook/main.ts
import { 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 將可直接使用。

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

React 伺服器元件 (RSC)

(⚠️ 實驗性功能

如果您的應用程式使用 React 伺服器元件 (RSC),Storybook 可以在瀏覽器中的 stories 中渲染它們。

要啟用此功能,請在您的 .storybook/main.js|ts 設定中設定 experimentalRSC 功能標誌

.storybook/main.ts
import { StorybookConfig } from '@storybook/nextjs';
 
const config: StorybookConfig = {
  // ...
  features: {
    experimentalRSC: true,
  },
};
 
export default config;

設定此標誌會自動將您的 story 包裹在 Suspense wrapper 中,該 wrapper 能夠在 NextJS 版本的 React 中渲染非同步元件。

如果此 wrapper 在您的任何現有 stories 中引起問題,您可以使用全域/元件/story 層級的 react.rsc 參數 選擇性地停用它

MyServerComponent.stories.ts
import { Meta, StoryObj } from '@storybook/react';
 
import MyServerComponent from './MyServerComponent';
 
const meta: Meta<typeof MyServerComponent> = {
  component: MyServerComponent,
  parameters: {
    react: { rsc: false },
  },
};
export default meta;

請注意,如果您的伺服器元件存取伺服器端資源(如檔案系統或 Node 專用函式庫),則將伺服器元件包裹在 Suspense 中並無幫助。要解決此問題,您需要使用 Webpack 別名 或像 storybook-addon-module-mock 這樣的 addon 來 mock 您的資料存取層。

如果您的伺服器元件透過網路存取資料,我們建議使用 MSW Storybook Addon 來 mock 網路請求。

未來,我們將在 Storybook 中提供更好的 mock 支援,並支援 伺服器行為

給 Yarn v2 和 v3 使用者的注意事項

如果您正在使用 Yarn v2 或 v3,您可能會遇到 Storybook 無法解析 style-loadercss-loader 的問題。例如,您可能會收到類似以下的錯誤

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

這是因為這些版本的 Yarn 具有與 Yarn v1.x 不同的套件解析規則。如果您的情況是這樣,請直接安裝套件。

常見問題

頁面/元件的 Stories 取得資料

Next.js 頁面可以直接在 app 目錄中的伺服器元件內取得資料,其中通常包含僅在 node 環境中執行的模組匯入。這(目前)在 Storybook 中不起作用,因為如果您在 stories 中從包含這些 node 模組匯入的 Next.js 頁面檔案匯入,您的 Storybook 的 Webpack 將會崩潰,因為這些模組將無法在瀏覽器中執行。為了解決這個問題,您可以將頁面檔案中的元件提取到一個單獨的檔案中,並在您的 stories 中匯入該純元件。或者,如果由於某些原因不可行,您可以在 Storybook 的 webpackFinal 設定polyfill 這些模組

之前

// 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 // ...
}

之後

// 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} />;
}

靜態匯入的圖片無法載入

請確保您以與在正常開發中使用 next/image 時相同的方式處理圖片匯入。

在使用此框架之前,圖片匯入將匯入圖片的原始路徑(例如 'static/media/stories/assets/logo.svg')。現在圖片匯入以 "Next.js 方式" 運作,這表示您現在在匯入圖片時會取得一個物件。例如

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

因此,如果 Storybook 中的某些內容無法正確顯示圖片,請確保您期望從匯入中傳回物件,而不僅僅是資源路徑。

有關 Next.js 如何處理靜態圖片匯入的更多詳細資訊,請參閱 本機圖片

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

如果您正在使用 Yarn v2 或 v3,您可能會遇到此問題。有關更多詳細資訊,請參閱 給 Yarn v2 和 v3 使用者的注意事項

如果我正在使用 Vite builder 怎麼辦?

我們引入了實驗性的 Vite builder 支援。只需安裝實驗性框架套件 @storybook/experimental-nextjs-vite,並將所有 @storybook/nextjs 的實例替換為 @storybook/experimental-nextjs-vite

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

npm install sharp
yarn add sharp
pnpm add sharp

您可以參考 Next.js 文件中的 安裝 sharp 以使用內建圖片最佳化 以獲取更多資訊。

API

模組

@storybook/nextjs 套件導出 幾個模組,使您能夠 mock Next.js 的內部行為。

@storybook/nextjs/export-mocks

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

getPackageAliases 是一個用於產生設定 portable stories 所需別名的 helper。

// 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')

此模組導出 next/cache 模組導出的 mock 實作。您可以使用它來建立自己的 mock 實作,或在 story 的 play function 中斷言 mock 呼叫。

MyForm.stories.ts
import type { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
// 👇 Must include the `.mock` portion of filename to have mocks typed correctly
import { revalidatePath } from '@storybook/nextjs/cache.mock';
 
import MyForm from './my-form';
 
const meta: Meta<typeof MyForm> = {
  component: MyForm,
};
 
export default meta;
 
type Story = StoryObj<typeof MyForm>;
 
export const Submitted: Story = {
  async play({ canvasElement }) {
    const canvas = within(canvasElement);
 
    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

此模組導出 next/headers 模組導出的可寫入 mock 實作。您可以使用它來設定在您的 story 中讀取的 cookies 或 headers,並在稍後斷言它們已被呼叫。

Next.js 的預設 headers() 導出是唯讀的,但此模組公開了允許您寫入 headers 的方法

  • headers().append(name: string, value: string):如果 header 已經存在,則將值附加到 header。
  • headers().delete(name: string):刪除 header
  • headers().set(name: string, value: string):將 header 設定為提供的值。

對於 cookies,您可以使用現有的 API 來寫入它們。例如,cookies().set('firstName', 'Jane')

因為 headers()cookies() 及其子函數都是 mocks,所以您可以在您的 stories 中使用任何 mock utilities,例如 headers().getAll.mock.calls

MyForm.stories.ts
import type { Meta, StoryObj } from '@storybook/react';
import { expect, fireEvent, userEvent, within } from '@storybook/test';
// 👇 Must include the `.mock` portion of filename to have mocks typed correctly
import { cookies, headers } from '@storybook/nextjs/headers.mock';
 
import MyForm from './my-form';
 
const meta: Meta<typeof MyForm> = {
  component: MyForm,
};
 
export default meta;
 
type Story = StoryObj<typeof MyForm>;
 
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']>

此模組導出 next/navigation 模組導出的 mock 實作。它還導出一個 getRouter 函數,該函數傳回 Next.js 的 router 物件,來自 useRouter 的 mock 版本,允許操作和斷言屬性。您可以使用它在 story 的 play function 中 mock 實作或斷言 mock 呼叫。

MyForm.stories.ts
import type { Meta, StoryObj } from '@storybook/react';
import { expect, fireEvent, userEvent, within } from '@storybook/test';
// 👇 Must include the `.mock` portion of filename to have mocks typed correctly
import { redirect, getRouter } from '@storybook/nextjs/navigation.mock';
 
import MyForm from './my-form';
 
const meta: Meta<typeof MyForm> = {
  component: MyForm,
  parameters: {
    nextjs: {
      // 👇 As in the Next.js application, next/navigation only works using App Router
      appDirectory: true,
    },
  },
};
 
export default meta;
 
type Story = StoryObj<typeof MyForm>;
 
export const Unauthenticated: Story = {
  async play() {
    // 👇 Assert that your component called redirect()
    await expect(redirect).toHaveBeenCalledWith('/login', 'replace');
  },
};
 
export const GoBack: Story = {
  async play({ canvasElement }) {
    const canvas = within(canvasElement);
    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']>

此模組匯出 next/router 模組輸出的模擬實作。它也匯出一個 getRouter 函數,該函數會回傳 Next.js 的 router 物件(來自 useRouter)的模擬版本,允許屬性被操作和斷言。您可以使用它來模擬實作,或在 story 的 play 函數中斷言模擬呼叫。

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

選項

如果需要,您可以傳遞選項物件以進行額外配置

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

可用的選項如下

builder

類型:Record<string, any>

設定 framework 的 builder 的選項。對於 Next.js,可用的選項可以在 Webpack builder 文件中找到。

image

類型:object

要傳遞給每個 next/image 實例的 Props。有關更多詳細信息,請參閱 next/image 文件

nextConfigPath

類型:string

next.config.js 檔案的絕對路徑。如果您有自訂的 next.config.js 檔案,且該檔案不在專案的根目錄中,則這是必要的。

參數

此框架在 nextjs 命名空間下,將以下參數貢獻給 Storybook

appDirectory

類型:boolean

預設值:false

如果您的 story 導入使用 next/navigation 的元件,您需要將參數 nextjs.appDirectory 設定為 true。由於這是一個參數,您可以將其應用於單個 story元件的所有 storyStorybook 中的每個 story。有關更多詳細信息,請參閱 Next.js Navigation

類型

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

預設值

{
  segments: [];
}

傳遞到 next/navigation 內容的 router 物件。有關更多詳細信息,請參閱 Next.js 的 navigation 文件

router

類型

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

傳遞到 next/router 內容的 router 物件。有關更多詳細信息,請參閱 Next.js 的 router 文件