適用於 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 7+ 版本搭配使用。如果您尚未使用 v7,請使用此命令升級
npx storybook@latest upgrade
自動遷移
執行上述 upgrade
命令時,您應該會收到提示,詢問您是否要遷移至 @storybook/nextjs
,這應該會為您處理一切。如果自動遷移不適用於您的專案,請參閱下方的手動遷移。
手動遷移
首先,安裝框架
npm install --save-dev @storybook/nextjs
然後,更新您的 .storybook/main.js|ts
以變更 framework 屬性
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 整合,則在使用此框架時不再需要這些外掛程式,可以移除
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 屬性
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 整合,則在使用此框架時不再需要這些外掛程式,可以移除
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 測試您的元件對各種輸入的回應。
如果您跳過了精靈,您可以隨時透過將 ?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/font。next/font/google
和 next/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 的執行環境。很可能它是您專案的根目錄。
import { StorybookConfig } from '@storybook/nextjs';
const config: StorybookConfig = {
// ...
staticDirs: [
{
from: '../src/components/fonts',
to: 'src/components/fonts',
},
],
};
export default config;
next/font
的不支援功能
以下功能尚不支援(但)。未來可能會計劃支援這些功能
- 在 next.config.js 中支援字型載入器設定
- fallback 選項
- adjustFontFallback 選項
- preload 選項會被忽略。Storybook 會以自己的方式處理字型載入。
- display 選項會被忽略。所有字型都會以 display 設定為 "block" 的方式載入,以使 Storybook 正確載入字型。
在測試期間模擬字型
有時,從 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 來完成。框架會將您在此處放置的任何內容淺層合併到路由器中。
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 操作和斷言的模擬函式。
若要覆寫這些預設值,您可以使用 parameters 和 beforeEach
// .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
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。
import { Preview } from '@storybook/react';
const preview: Preview = {
// ...
parameters: {
// ...
nextjs: {
appDirectory: true,
},
},
};
export default preview;
覆寫預設值
每個 story 的覆寫可以透過將 nextjs.navigation
屬性新增至 story parameters 來完成。框架會將您在此處放置的任何內容淺層合併到路由器中。
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。標準的參數繼承規則適用。
useSelectedLayoutSegment
、useSelectedLayoutSegments
和 useParams
hooks
Storybook 支援 useSelectedLayoutSegment
、useSelectedLayoutSegments
和 useParams
hooks。您必須設定 nextjs.navigation.segments
參數,以傳回您想要使用的區段或 params。
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 值。
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 操作和斷言的模擬函式。
若要覆寫這些預設值,您可以使用 parameters 和 beforeEach
// .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
@storybook/nextjs/cache.mock
@storybook/nextjs/headers.mock
@storybook/nextjs/navigation.mock
@storybook/nextjs/router.mock
Mock 其他模組
在 Storybook 中 mock 其他模組的方式取決於您如何將模組匯入到元件中。
無論使用哪種方法,第一步都是 建立 mock 檔案。以下是一個名為 session
的模組的 mock 檔案範例
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
{
"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 檔案。
// 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 的範例。
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.json
的 baseUrl 和 paths。因此,像下面這樣的 tsconfig.json
將可直接使用。
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"]
}
}
}
React 伺服器元件 (RSC)
(⚠️ 實驗性功能)
如果您的應用程式使用 React 伺服器元件 (RSC),Storybook 可以在瀏覽器中的 stories 中渲染它們。
要啟用此功能,請在您的 .storybook/main.js|ts
設定中設定 experimentalRSC
功能標誌
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
參數 選擇性地停用它
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-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 不同的套件解析規則。如果您的情況是這樣,請直接安裝套件。
常見問題
頁面/元件的 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 呼叫。
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 的 cookies
、headers
和 draftMode
此模組導出 next/headers
模組導出的可寫入 mock 實作。您可以使用它來設定在您的 story 中讀取的 cookies 或 headers,並在稍後斷言它們已被呼叫。
Next.js 的預設 headers()
導出是唯讀的,但此模組公開了允許您寫入 headers 的方法
headers().append(name: string, value: string)
:如果 header 已經存在,則將值附加到 header。headers().delete(name: string)
:刪除 headerheaders().set(name: string, value: string)
:將 header 設定為提供的值。
對於 cookies,您可以使用現有的 API 來寫入它們。例如,cookies().set('firstName', 'Jane')
。
因為 headers()
、cookies()
及其子函數都是 mocks,所以您可以在您的 stories 中使用任何 mock utilities,例如 headers().getAll.mock.calls
。
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 呼叫。
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 函數中斷言模擬呼叫。
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、元件的所有 story 或 Storybook 中的每個 story。有關更多詳細信息,請參閱 Next.js Navigation。
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 文件。