
在 Storybook 中使用 React Server Components 和 Mock Service Worker 建構 Next.js 應用程式
使用 MSW 模擬網路請求,在隔離環境中開發、記錄和測試 RSC 應用程式

Storybook 8(我們的下一個主要版本)首次為 Storybook 帶來 React Server Component (RSC) 相容性,讓您在隔離環境中建構、測試和記錄 Next.js 伺服器應用程式。
在我們的第一個示範中,我們使用 Storybook 開發了一個聯絡人卡片 RSC,它非同步地存取聯絡人資料,並從檔案系統存取,同時透過模組模擬來模擬伺服器程式碼。

接下來,我們將探索如何在隔離環境中使用 Next.js App Router 建構整個應用程式,方法是在 Storybook 中重建 Hacker Next 範例,並借助 Mock Service Worker。


為什麼要在隔離環境中建構頁面?
令人驚訝的是,僅僅兩個頁面就能容納如此多的 UI。考慮您的頁面需要的資料狀態。然後,將它們乘以響應式佈局、登入視圖、主題、瀏覽器、地區設定和輔助功能。少數幾個頁面很容易變成數百個變體。
Storybook 透過讓您將任何 UI 狀態隔離為一個故事來解決這種複雜性!如果您是 Storybook 的新手,這裡說明了故事的運作方式。
為 Hacker Next 撰寫故事
首先,在您的 Next.js 專案中安裝 Storybook
npx storybook@next init
然後,將 experimentalRSC
標誌新增到 Storybook 的 main.ts,並將其指向我們即將撰寫的新故事
// main.ts
const config: StorybookConfig = {
- stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
+ stories: ['../app/**/*.stories.tsx'],
// ... existing config
+ features: { experimentalRSC: true }
}
現在,讓我們為 Hacker Next 的兩個元件撰寫故事:news
首頁和 item
頁面!以下是 news
頁面的簡單故事範例
// app/news/[page]/index.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import News from './page';
const meta = {
title: 'app/News',
component: News,
} satisfies Meta<typeof News>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Home: Story = {
args: { params: { page: 1 } },
}

雖然這可以運作,但您會注意到它缺少樣式。我們可以透過將 裝飾器 新增到我們的 .storybook/preview.tsx
來修正此問題
// .storybook/preview.tsx
import Layout from '../app/layout.tsx';
export default {
// other settings
decorators: [(Story) => <Layout><Story /></Layout>],
}

這樣就更像樣了!現在,嘗試對 app/item/[id]/(comments)/page.tsx
執行此操作。如果您遇到困難,請查看我們的 repo。
使用 Mock Service Worker 進行模擬
我們希望能夠控制資料,而不是使用真實資料。這讓我們可以測試不同的狀態並產生一致的結果。
Hacker Next 從網路 API 獲取資料,因此我們將使用 Mock Service Worker (MSW) 來模擬其請求。
首先,讓我們將 Storybook 的 MSW 擴充套件 新增到我們的專案中。我們將使用支援 MSW 2.0 顯著改進的 API 的 canary 版本。
pnpx storybook add msw-storybook-addon@2.0.0--canary.122.06f0c92.0
pnpx msw init public
接下來,更新 .storybook/preview.tsx
以使用 onUnhandledRequest
選項初始化 MSW。這確保我們現有的故事繼續運作。
// .storybook/preview.tsx
// ... existing imports
+ import { initialize, mswLoader } from 'msw-storybook-addon';
+ initialize({ onUnhandledRequest: 'warn' });
const preview: Preview = {
+ loaders: [mswLoader],
decorators: [(Story) => <Layout><Story /></Layout>],
}
現在,讓我們為 Hacker Next 的首頁建立一個故事,其中包含單一貼文
// app/news/[page]/index.stories.tsx
import { http, HttpResponse } from 'msw'
// ...existing meta/story
export const Mocked = {
...Home,
parameters: {
msw: {
handlers: [
http.get('https://hacker-news.firebaseio.com/v0/topstories.json', () => {
return HttpResponse.json([1]);
}),
http.get('https://hacker-news.firebaseio.com/v0/item/1.json', () => {
return HttpResponse.json({
id: 1,
time: Date.now(),
user: 'shilman',
url: 'https://storybook.dev.org.tw',
title: 'Storybook + Next.js = ❤️',
score: 999,
});
}),
],
},
},
};
透過模擬來自前端的兩個 REST API 請求並硬式編碼回應,我們得到以下故事

MSW 資料工廠
硬式編碼的 API 回應難以擴展。因此,讓我們撰寫一個故事,使用更高等級的參數來控制頁面內容!我們需要
- 建構簡化的記憶體資料庫
- 建立從資料庫讀取並產生所需網路回應的 MSW 處理程序
- 撰寫故事以使用測試案例填充資料庫
步驟 1:建構資料庫
首先,讓我們使用 @mswjs/data(MSW 的資料工廠程式庫)和 Faker.js 建立資料庫。
// data.mock.ts
import { faker } from '@faker-js/faker'
import { drop, factory, primaryKey } from '@mswjs/data
let _id;
const db = factory({
item: {
id: primaryKey(() => _id++),
time: () => faker.date.recent({ days: 2 }).getTime() / 1000,
user: faker.internet.userName,
title: faker.lorem.words,
url: faker.internet.url,
score: () => faker.number.int(100),
}
})
/** Reset the database */
export const reset = (seed?: number) => {
_id = 1
faker.seed(seed ?? 123)
return drop(db)
}
/** Create a post. Faker will fill in any missing data */
export const createPost = (item = {}) => db.item.create(item);
/** Utility function */
export const range = (n: number) => Array.from({length: n}, (x, i) => i);
/** Return all the post IDs */
export const postIds = () => db.item.findMany({}).map((p) => p.id);
/** Return the content of a single post by ID */
export const getItem = (id: number) => db.item.findFirst({ where: { id: { equals: id }}});
這讓您可以精確地指定您希望貼文顯示的樣子。當我們不指定任何資料時,Faker 會填補空白。這樣,您可以用最少的程式碼建立數十甚至數百個貼文!
步驟 2:建立 MSW 處理程序
接下來,我們將使用從資料庫讀取的 MSW 處理程序更新 .storybook/preview.tsx
。這些處理程序在您的所有故事中都可用,並讀取資料庫中的任何內容。這表示故事的唯一工作是用有用的資料填充資料庫!
// .storybook/preview.tsx
import { postIds, getItem } from '../lib/data.mock.ts';
import { http, HttpResponse } from 'msw'
const preview: Preview = {
// ...existing configuration
parameters: { msw: { handlers: [
http.get(
'https://hacker-news.firebaseio.com/v0/topstories.json',
() => HttpResponse.json(postIds())
),
http.get<{ id: string }>(
'https://hacker-news.firebaseio.com/v0/item/:id.json',
({ params }) => HttpResponse.json(getItem(parseInt(params.id, 10)))
)
] } },
};
步驟 3:撰寫故事
最後,我們將為我們的新設定撰寫故事。
首先,使用 loader(在故事呈現之前執行的函式)替換您現有的 Mocked
故事。此 loader 呼叫我們的 createPost
輔助函式,該函式 1) 實例化一個貼文,以及 2) 將其新增到記憶體資料庫。
// app/news/[page]/index.stories.tsx
import { createPost } from '../../../lib/data.mock';
// ...existing meta/story
export const MockedNew = {
...Home,
loaders: [() => {
createPost({
id: -1,
user: 'shilman',
url: 'https://storybook.dev.org.tw',
title: 'Storybook + Next.js = ❤️',
score: 999,
});
}],
};
當您需要一次建立大量資料時,此方案真的會發光發熱。為了示範這一點,讓我們建立一個顯示 30 個貼文的首頁。為了使其更強大,我們可以允許在 Storybook 的 UI 中互動式地控制貼文數量
// app/news/[page]/index.stories.tsx
import { createPost, range, reset } from '../../../lib/data.mock'
export const FullPage = {
args: {
postCount: 30,
},
loaders: [({ args: { postCount } }) => {
reset();
range(postCount).forEach(() => createPost());
}];
}

現在是測試的時候了
恭喜!您已經在 Storybook 中建構了 Hacker Next,其資料可以針對不同的測試進行自訂。或者,查看 示範 Storybook(透過 Chromatic 分享)或 我們的 repo。
除了將您的 UI 集中到一個位置之外,您還可以以前所未有的方式測試 Hacker Next。
例如,您可以使用 Storybook 的 play function 為 Hacker Next 的投票和摺疊評論狀態撰寫故事。這是一段程式碼片段,用於模擬使用者互動,並在故事呈現後立即執行。它可以使用 Testing-Library 與 DOM 互動,並使用 Vitest 的 expect 和 spies 進行斷言。
這是一個使用 play function 來為首頁上的第一個貼文投票的故事
// app/news/[page]/index.stories.tsx
import { within, userEvent } from '@storybook/test';
export const Upvoted: Story = {
...FullPage,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)
const votes = await canvas.findAllByText('▲')
await userEvent.click(votes[0])
}
}
整合在一起
在本練習中,我們編catalog了 Next.js 應用程式的關鍵 UI 狀態。一旦我們將所有這些都放入 Storybook 中,我們就可以
- 即使我們的後端正在開發中,也可以針對模擬資料進行開發
- 開發難以觸及的 UI 狀態,例如「信用卡已過期」畫面
- 在每個畫面上立即執行視覺迴歸和 a11y 測試,跨瀏覽器和不同解析度進行測試
- 將生產故事與其設計檔案一起檢視,以確保順利交接
- 使用整個前端架構的即時和全面的文件來引導新開發人員
- 瞭解更多關於將 Next.js 與 Storybook 搭配使用的資訊
Storybook 徹底改變了可重複使用元件的開發。現在,您可以將相同的優勢應用於應用程式的頁面。
在我們的下一篇 RSC 文章中,我們將探索模組模擬,以處理模擬網路請求不可能或不切實際的真實案例。
Storybook 8(我們的下一個主要版本)為 Storybook 帶來了 React Server Components 支援!
— Storybook (@storybookjs) 2024 年 1 月 18 日
在我們的新教程中,瞭解如何使用 @nextjs、Storybook 和 @ApiMocking 在隔離環境中建構、記錄和測試 RSC 應用程式 ≫https://#/SVZ3TNJw1I
致謝
感謝 Artem Zakharchenko(MSW 的核心維護者)和 Next.js 團隊的審閱和指導!