
React Server Components 的 Storybook
通过升级到 Storybook 8.0 alpha 在 Storybook 中使用 RSC

React Server Components (RSC) 是一种用于基于 React 的 Web UI 的新编程模型。与传统的 React“客户端”组件相比,它们仅在服务器上渲染。这带来了各种性能和安全优势,但同时也与我们今天使用的 React 工具和库有很大的不同。
受影响最显著的领域之一是组件驱动的开发和测试。Storybook、Testing Library 和 Playwright/Cypress Component Testing 等工具都假定用户的组件是在浏览器(或 JSDom)中渲染的。但对于服务器组件,情况已不再如此。
这引出了一个问题:对服务器组件进行隔离的开发和测试意味着什么?
今天,我很高兴能在 Storybook 的 Next.js 框架中发布对 RSC 的支持,作为这个问题的实验性解答。这是一个纯客户端实现,使其与 Storybook 插件和集成的整个生态系统兼容。
继续阅读,了解它的工作原理、如何使用它以及如何今天就尝试它!
服务器来自火星,客户端来自金星
RSC 与传统客户端组件有两个主要区别,这两个区别在以下示例中都有体现
// ApiCard.tsx
import { ComponentProps } from 'react';
import { Card } from './Card';
import { findById } from './db';
export async function DbCard({ id }: {id: number}) {
let props;
try {
const contact = await findById(id);
props = { state: 'success', contact };
} catch (e) {
props = { state: 'error' };
}
return <Card {...props} />;
}
- 第一个区别是我们的组件是
async
,这在客户端是不支持的。 - 第二个区别是我们的组件可以直接访问 Node 代码,在本例中是包装了经过身份验证的数据库连接的
findById
函数。
RSC 在底层做了很多工作来实现这两个区别。这段代码只在服务器上运行,并生成一个类似 JSON 的静态结构,然后流式传输到客户端。
Storybook 是一个纯客户端应用。它生成的是纯粹的 HTML/CSS/JS 的静态构建,完全看不到 Node!因此,支持 RSC 需要弄清楚如何让 RSC 在客户端渲染,或者对 Storybook 进行重构以适应服务器。
我们首先专注于客户端方法。我们希望最大限度地减少对用户的影响,他们已经编写了数百万个 stories 和数百个 addons,所有这些都基于当前的架构。
那么,这到底是怎么工作的呢?
实现异步
将 RSC 在客户端渲染的第一个挑战是配置如何支持异步组件。事实证明,Next.js 的 canary React 版本已经(非官方地)支持了这一点。特别感谢 JamesManningR 和 julRuss,他们贡献了这个简单的解决方案!
import { Suspense } from 'react';
export const ClientContact = ({ id }) => (
<Suspense><DbCard id={id} /></Suspense>
);
从 Storybook 8 开始,@storybook/nextjs
可以使用 .storybook/main.js
中的 experimentalRSC
功能标志将你的 stories 包装在 Suspense
中
// .storybook/main.js
export default {
features: {
experimentalRSC: true,
}
};
你也可以在 7.x 版本的 @storybook/nextjs
中手动完成此操作,方法是将你的 RSC stories 包装在 decorator 中。
注意:此解决方案尚不适用于其他 Storybook React 框架(例如 react-vite
、react-webpack5
),因为它们不使用 Next.js 的 canary React 版本。希望 React 的下一个版本能解除此限制。
模拟和加载
解决异步问题只完成了一半。我们的 DbCard
组件还引用了用于获取数据以填充组件的 Node 代码。这在浏览器中是个问题,因为浏览器无法执行 Node 代码!
为了解决这个问题,我们建议建立一个清晰的数据访问层。RSC 的架构师也推荐这样做,将其视为最佳实践。
一旦你有了数据访问层,你就可以模拟它,使其能够在浏览器中运行,并且你可以精确控制它返回的数据,以便测试不同的 UI 状态(加载中、错误、成功等)。
你可以使用 模块模拟 或 网络模拟 来模拟数据访问层,Storybook 都支持这两种方式。
模块:有一个社区插件 storybook-addon-module-mock,它提供了 jest.mock
风格的模拟(仅适用于 Webpack 项目)。你也可以使用 webpack/vite 的别名(aliases)来实现一个更简单但功能有限的解决方案。我们计划在未来版本的 Storybook 中提供更符合人体工程学的模块模拟功能。
网络 API:要模拟网络请求,我们推荐 Mock Service Worker (msw)。Storybook 还支持许多其他网络和 GraphQL 模拟插件。
回到我们的例子,使用 storybook-addon-module-mock
的 story 可能看起来像这样
// DbCard.stories.js
import { StoryObj, Meta } from '@storybook/react';
import { createMock } from 'storybook-addon-module-mock';
import { DbCard } from './DbCard';
import * as db from './db';
export default { component: DbCard };
export const Success {
args: { id: 1 },
parameters: {
moduleMock: {
mock: () => {
const mock = createMock(db, 'findById');
mock.mockReturnValue(Promise.resolve({
name: 'Beyonce',
img: 'https://blackhistorywall.files.wordpress.com/2010/02/picture-device-independent-bitmap-119.jpg',
tel: '+123 456 789',
email: 'b@beyonce.com'
}))
return [mock];
},
},
},
}
完整演示:API + 模块模拟
对于上面包含模块模拟数据库版本和 MSW2 模拟 API 版本的完整示例,请查看 我们的完整 RSC 演示 Storybook 或 其 GitHub 仓库。

有什么不足之处?
在这篇文章中,我们成功地为 Storybook 中的第一个 RSC 编写了一个 story,并展示了其底层实现原理。
这都相当直接,但这种方法存在局限性
- 保真度。纯客户端实现与在你的应用程序中运行的服务器端流式 RSC 实现存在显著差异。
- 便利性。这里的模拟解决方案绝对可以改进。我们当前的模块模拟解决方案不仅冗长,而且与 Storybook 的 args/controls 配合得不好。
我们计划在后续迭代中解决这两个局限性,这也是为什么我们将此解决方案标记为实验性的原因。
立即使用 Storybook 进行 RSC 开发 🎊
要使用 Storybook 进行 RSC 开发,请将 Storybook 升级到 8.0-alpha
npx storybook@next upgrade --prerelease
然后,在你的 .storybook/main.ts
中启用实验性功能
// .storybook/main.js
export default {
features: {
experimentalRSC: true,
},
};
更多信息,请参阅 @storybook/nextjs
的文档。
这是我们详细介绍 Storybook 8.0(我们的下一个主要版本)内容的第一篇文章,未来几个月还将发布更多内容。通过关注我们的社交媒体或订阅 Storybook 时事通讯来了解下一版本的所有新闻!
Storybook 现在支持 React Server Components 了!
— Storybook (@storybookjs) December 13, 2023
了解它是如何工作的,并在 Storybook 8.0 alpha 中立即尝试 🔥https://#/lOuql2kKPq