在 Storybook 中以 Provider 形式注入真实容器组件:Container/Context 分离模式的页面层 Mock 实战

发布时间:2026/9/8 17:19:15

在 Storybook 中以 Provider 形式注入真实容器组件:Container/Context 分离模式的页面层 Mock 实战 在 Storybook 中以 Provider 形式注入真实容器组件Container/Context 分离模式的页面层 Mock 实战【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook导读当组件从原子组件上升到页面Page层级时往往伴随着数据请求、浏览器 API、全局状态等已连接connected依赖直接放进 Storybook 渲染就必须挨个 Mock成本极高。Storybook 官方文档给出的一种解法是用 React/Solid 的 Context 作为容器组件的中转站——Storybook 里用故事的 Mock 版本替换容器应用里则通过ProfilePageContext.Provider注入真实实现。本文以docs/writing-stories/build-pages-with-storybook.mdx第 9 节为主线完整演示创建 Context → 展示组件消费容器 → Storybook 中 Mock → 应用侧 Provider 注入真实容器的端到端流程并补充对应的仓库代码片段作为可运行参考。为什么要在页面层避开直接 Mock 容器组件Storybook 支持构建从原子组件到组合页面的任何组件见 docs/writing-stories/build-pages-with-storybook.mdx。页面通常不会单独请求数据而是由内部各区块container完成。要渲染一个页面级 Story常见做法是对其依赖逐层 MockMocking modulesMock 组件文件中import进来的模块Mocking API services拦截 REST/GraphQL 网络请求Mocking providers用 Decorator 包裹并伪造 Context Provider 提供的值如主题、Redux 数据。这些方案各自有效但存在两个现实痛点繁琐容器组件可能内嵌在页面组件树的任意深度逐层传递数据、逐层 Mock import 会让工作量快速膨胀困难带有本地状态的容器组件本身很难被模拟Mock 往往无从下手。因此官方文档推荐一种结构性解法该段落在docs/writing-stories/build-pages-with-storybook.mdx#L62-L70先把页面做容器/展示的严格拆分再通过 Context 把需要使用哪些容器这件事从展示层解耦出来——页面组件以 Props/Context 拿到容器而非直接 import 它们。这样无论页面嵌套多深都可以在渲染前整体替换容器无需关心容器内部的数据请求与状态逻辑。模式总览应用侧一份真实、Storybook 侧一份 Mock整个模式由四个文件组成官方建议为每个页面或视图单独划分目录结构ProfilePage.js # 展示组件只负责布局通过 useContext 拿容器 ProfilePage.stories.js # 页面级 Story ProfilePageContainer.js # 容器组件真实数据逻辑网络请求等 ProfilePageContext.js # React/Solid Context 定义关键思想同一份ProfilePageContext在两侧提供不同实现。应用侧ProfilePageContext.Provider提供真实的UserPostsContainer、UserFriendsContainer见本主题关联片段 mock-context-container-provider.md在应用入口如pages/profile.js中使用Storybook 侧Provider 提供来自各子组件 Story 的Mock 版本组件往往本身就是理想的可替换实现见 mock-context-container.md若某容器几乎出现在每个页面可上升为全局容器 ContextGlobalContainerContext在.storybook/preview.js的全局 Decorator 中注入见 mock-context-container-global.md。第一步定义 Contextmock-context-create.mdContext 文件本身极其简单——它只导出一个空的 Context不包含任何业务逻辑。React 版本// ProfilePageContext.js import { createContext } from react; const ProfilePageContext createContext(); export default ProfilePageContext;Solid 版本仅换用solid-js的createContext// ProfilePageContext.js import { createContext } from solid-js; const ProfilePageContext createContext(); export default ProfilePageContext;该文件对应片段 mock-context-create.md。第二步展示组件通过 useContext 消费容器mock-context-in-use.mdProfilePage是纯展示组件它不做任何数据请求而是通过useContext从ProfilePageContext中取出两个容器组件再把来自上层 Props 的userId传交给它们// ProfilePage.jsReact import { useContext } from react; import ProfilePageContext from ./ProfilePageContext; export const ProfilePage ({ name, userId }) { const { UserPostsContainer, UserFriendsContainer } useContext(ProfilePageContext); return ( div h1{name}/h1 UserPostsContainer userId{userId} / UserFriendsContainer userId{userId} / /div ); };Solid 版本几乎一致仅 Props 读取方式遵循 Solid 的props风格// ProfilePage.jsSolid import { useContext } from solid-js; import ProfilePageContext from ./ProfilePageContext; export const ProfilePage (props) { const { UserPostsContainer, UserFriendsContainer } useContext(ProfilePageContext); return ( div h1{props.name}/h1 UserPostsContainer userId{props.userId} / UserFriendsContainer userId{props.userId} / /div ); };可以看到只要 Provider 提供了容器页面组件就能像普通组件一样自由渲染它们。展示层完全不感知容器的真实实现是网络请求、Redux 连接还是本地状态这正是后续可以整容器替换的前提。对应片段见 mock-context-in-use.md。第三步在 Storybook 中提供 Mock 容器mock-context-container.md在 Story 文件中我们不再构造真实容器而是为ProfilePageContext.Provider传入Mock 实现。其中最常见的技巧Mock 版本直接复用子组件自身 Stories 里的导出。既然 Story 本身就是给定 Props 渲染该组件的场景它天然就是一个可复用的 React 元素工厂多数情况下足以充当容器的替身。// ProfilePage.stories.js import React from react; import { ProfilePage } from ./ProfilePage; import { UserPosts } from ./UserPosts; // 从故事文件中引入某个具体 story import { Normal as UserFriendsNormal } from ./UserFriends.stories; export default { component: ProfilePage, }; const ProfilePageProps { name: Jimi Hendrix, userId: 1, }; const context { // 如果需要在这里可以访问到 userId prop UserPostsContainer({ userId }) { return UserPosts {...ProfilePageProps} /; }, // 大多数情况下直接把 story 传进来即可。 // 这里传入的是 UserFriends 组件故事中的 normal story 导出。 UserFriendsContainer: UserFriendsNormal, }; export const Normal { render: () ( ProfilePageContext.Provider value{context} ProfilePage {...ProfilePageProps} / /ProfilePageContext.Provider ), };两种 Mock 形态各有适用场景Mock 方式写法适用情况直接传入 story 导出UserFriendsContainer: UserFriendsNormal容器 Mock 不需要感知页面级 Props最简单、最常用函数形式包裹UserPostsContainer({ userId }) { return UserPosts .../ }需要根据页面 Props如userId动态拼接数据或注入其他 Props 时完整片段见 mock-context-container.md。如果同一个 Provider 值适用于ProfilePage的所有 Story则不必在每个 Story 里重复包裹 Provider官方建议改用Decorator收敛逻辑参见 docs/writing-stories/decorators.mdx。第四步在应用中通过 Provider 注入真实容器本主题核心片段页面在 Storybook 中可渲染之后还需要保证应用真实运行时能拿到真实的容器组件。做法与第三步对称在应用渲染ProfilePageContainer的入口处用ProfilePageContext.Provider把真实容器放入 Context。本主题关联的代码片段即此场景见 docs/_snippets/mock-context-container-provider.md。以 Next.js 为例这通常位于pages/profile.js。React 版本// pages/profile.js import React from react; import ProfilePageContext from ./ProfilePageContext; import { ProfilePageContainer } from ./ProfilePageContainer; import { UserPostsContainer } from ./UserPostsContainer; import { UserFriendsContainer } from ./UserFriendsContainer; // 确保你的 context 值在每次渲染之间保持引用相等referentially equal。 const context { UserPostsContainer, UserFriendsContainer, }; export const AppProfilePage () { return ( ProfilePageContext.Provider value{context} ProfilePageContainer / /ProfilePageContext.Provider ); };Solid 版本// pages/profile.js import ProfilePageContext from ./ProfilePageContext; import { ProfilePageContainer } from ./ProfilePageContainer; import { UserPostsContainer } from ./UserPostsContainer; import { UserFriendsContainer } from ./UserFriendsContainer; // 确保你的 context 值在每次渲染之间保持引用相等referentially equal。 const context { UserPostsContainer, UserFriendsContainer, }; export const AppProfilePage () { return ( ProfilePageContext.Provider value{context} ProfilePageContainer / /ProfilePageContext.Provider ); };关键细节context 值为何必须模块级定义片段中特别用注释强调了一行容易忽略的最佳实践// Ensure that your context value remains referentially equal between each render. const context { UserPostsContainer, UserFriendsContainer, };context对象被定义在组件函数之外模块作用域而不是AppProfilePage内部。原因在于 React/Solid 中 Context 的性能语义若在组件体内创建对象{ UserPostsContainer, UserFriendsContainer }每一次渲染都会产生一个新的对象引用Provider 的value引用一旦变化所有订阅该 Context 的消费者组件都会触发重渲染页面级 Provider 包裹着整棵子树这将导致每次父组件重渲染都连带整棵页面子树不必要的重渲染。把对象提升到模块顶层就能保证每次渲染之间引用相等referentially equal从而避免上述连锁重渲染。若某容器集合需要在渲染期内变动则应当用useMemoReact等机制在合适的依赖前提下缓存该值而不是随手内联。第五步可选全局容器 Context 与 preview 的 Decorator如果某些容器几乎出现在应用的每个页面上如NavigationContainer为其逐一在页面入口创建 Provider 会很啰嗦。官方建议另建一个全局容器 Context如GlobalContainerContext放到应用顶层只提供全局必需的容器具体做法参见 mock-context-container-global.md。在 Storybook 侧则在.storybook/preview.js中导出一个全局 Decorator把来自Navigation.stories.js的normalstory 作为NavigationContainer注入所有 Story// .storybook/preview.jsReactCSF 3 import * as React from react; import { normal as NavigationNormal } from ../components/Navigation.stories; import GlobalContainerContext from ../components/lib/GlobalContainerContext; const context { NavigationContainer: NavigationNormal, }; const AppDecorator (storyFn) { return ( GlobalContainerContext.Provider value{context}{storyFn()}/GlobalContainerContext.Provider ); }; export default { decorators: [AppDecorator] };TypeScript ReactCSF 3版本会额外标注组件类型// .storybook/preview.tsReactCSF 3 import * as React from react; // 将 your-framework 替换为你所用的框架如 react-vite、nextjs、nextjs-vite 等。 import type { Meta, StoryObj } from storybook/your-framework; import { normal as NavigationNormal } from ../components/Navigation.stories; import GlobalContainerContext from ../components/lib/GlobalContainerContext; const context { NavigationContainer: NavigationNormal, }; const AppDecorator (storyFn) { return ( GlobalContainerContext.Provider value{context}{storyFn()}/GlobalContainerContext.Provider ); }; const preview: Preview { decorators: [AppDecorator], }; export default preview;其中storybook/your-framework为文档中的占位写法实际项目中应替换为具体框架入口如storybook/react-vite、storybook/nextjs。使用实验性 CSF Next 语法时用definePreview包装相同的 Decorator 配置见 mock-context-container-global.md。与Mocking Providers路径的对比与取舍本模式本质上是对 Mocking providers 的一种结构性延伸。常规 Provider Mock如主题 Provider只需用 Decorator 包裹组件并伪造一个静态值若需要按 Story 差异化取值可借助 Decorator 的第二个context参数读取该 Story 的parameters从而一次定义、按 Story 调参见 mock-provider-in-preview.md 与 mocking-providers.mdx。而 Container Context 模式更进一步Context 中存放的不是值而是组件本身。它要解决的不是给组件喂一份假数据而是让页面在任意深度都能无痛地换掉数据与状态逻辑所在的那一层组件。二者相辅相成Container Context 负责把真实/虚假实现的选择权上移Provider Mock 负责在选定实现后控制其内部数据。小结这套模式带来的实际收益回到 docs/writing-stories/build-pages-with-storybook.mdx 的上下文Container/Context 模式的核心收益可归纳为页面级 Story 无需关心数据依赖——只需在 Story 中把容器替换为各自的故事即可渲染真实页面布局复用而非重写——Mock 版本大多直接取自子组件 Stories遵循 DRYStory 维护成本低深度嵌入无压力——容器以 Props/Context 传递无论在页面组件树多深处都可整体替换应用侧对称注入——真实入口pages/profile.js用 Provider 包住真实容器即可展示组件代码零改动配合 Decorator 进一步收敛——重复性 Provider 包裹可提升为 Story 级或全局级 Decorator。从 mock-context-create.mdContext 定义、mock-context-in-use.md展示组件消费、mock-context-container.mdStorybook 侧 Mock、mock-context-container-provider.md应用侧真实 Provider到 mock-context-container-global.md全局容器五段片段即构成该模式的最小可运行闭环可直接对照实现到自己的 React 或 Solid 项目中。【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/9/8 17:19:14

跳点搜索JPS原理与实现:从A*到几十毫秒的栅格寻路优化

前阵子在做园区低速物流车的全局路径模块,甲方给过来的需求很直接:在几百米见方的栅格地图上,从库房门口到充电桩要能快速出一条不绕远、不震荡的路径,同时嵌入式板卡上的CPU预算不高,不能把核都吃满。最早用A*跑&…

2026/9/8 17:19:14

2026知网AIGC检测通关!AI率狂降至5%,亲测百试百灵

真的要崩溃了!熬夜改到凌晨三四点,眼睛都快睁不开,逐字逐句润色、语序换了又换,结果一检测还是满屏红。 别再傻傻硬改、纯手动死磕了!不仅浪费时间,还越改越乱、AI率越改越高。我花了整整一周,…

2026/9/8 19:24:35

SDD+AI协作:从规格文档到npm排版包的开发实践

1. 起因:一个重复了无数次的排版痛点,和一次方法论转向这个月我把一个积压了很久的零散脚本,正式做成了一款排版类的 npm 包。整个过程最让我意外的不是包本身,而是它的生产方式:我没有像以前那样打开编辑器就开写&…

2026/9/8 19:24:35

Linux学习总结-元一软件

前言介绍: 根目录下的所有文件(文件夹):bin etc lib64 opt run sys var boot home media proc sbin tmp dev lib mnt root srv usr1.重启network网卡 service NetworkManager stop chkconfig NetworkManager off 永久关闭Manager…

2026/9/8 19:24:35

嵌入式硬件开发全流程:从原理图设计到PCB制造与调试

1. 别急着画图:先把需求转成能落地的设计输入很多刚入行的工程师拿到一个项目,第一反应就是打开EDA工具开始拖元件库、拉网络。我在前几年带新人时反复强调过同一句话:原理图只是一个表达载体,真正的设计工作发生在画图之前。嵌入…

2026/9/8 19:24:35

2026 AI编程助手横评:效率优先,我只留下这两个

先亮明身份:一个每天跟需求、bug、重构打交道的后端开发者,不搞评测自媒体,也不替任何厂商站台。2026年刚开始,我给自己定了一个KPI:把手上的AI编程助手从“装了好几个但实际都在吃灰”收敛到“真正打开就能干活的那两…

2026/9/8 19:19:35

opencode 完全指南:终端 AI 编程智能体的安装、配置与实战

最近这两个月,我几乎每天都在用 opencode 写代码。本来只是看它上了 GitHub 热榜,想装来试试水,结果它直接成了我终端里最常用的工具之一。如果你还没用过 opencode,简单说,它是一个开源的、跑在终端里的 AI 编程智能体…

2026/9/8 7:15:10

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/8 7:15:15

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/8 7:15:10

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/8 0:01:49

踩多轮坑才跑通|OpenClaw 3.1.0 双平台本地 AI 自动化搭建实操实录

🔹 工具简述 OpenClaw 是一款备受开发者与办公人群青睐的开源本地智能工具,凭借离线本地运行、可视化图形面板、全流程自主任务处理三大核心特点,积累了众多忠实用户。与普通对话类 AI 产品不同,它能够直接调用电脑的软硬件操作权…

2026/9/8 0:01:50

拒绝复杂命令行,Hermes Agent 一键包快速解锁智能办公能力

🔍前言 不少想要体验 Hermes Agent 办公能力的使用者,往往会被复杂的环境配置拦住使用脚步。手动下载匹配依赖、反复调整系统目录、处理命令行持续报错、修复权限异常、补全丢失核心文件等一系列操作,对普通使用者而言门槛较高,很…

2026/9/7 16:23:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/7 22:46:00

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/7 22:45:59

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码