news 2026/9/8 21:27:56

Storybook 文件级 `beforeEach` 钩子实战:用 MockDate 统一控制每个 Story 的渲染时间

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Storybook 文件级 `beforeEach` 钩子实战:用 MockDate 统一控制每个 Story 的渲染时间

Storybook 文件级beforeEach钩子实战:用 MockDate 统一控制每个 Story 的渲染时间

【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook

在 Storybook 交互测试与组件开发中,很多组件(日历、倒计时、报表、聊天时间戳等)的行为依赖系统当前时间,导致每次渲染或运行play函数时结果都不稳定。本篇文章基于 Storybook 官方交互测试文档提供的beforeEach代码片段,讲解如何在组件 meta(默认导出)上声明异步beforeEach钩子,用mockdateDate固定为某个确定值,再通过钩子返回的清理函数在每个 Story 结束后自动还原——最终让文件内所有 Story 与交互测试都运行在可预期的“假想时间”里。读完你将掌握 Storybook 生命周期钩子的声明位置、执行顺序、清理函数约定,以及它在各渲染器(Angular、React、Vue、Svelte、Web Components)下的完整写法。

这段代码解决了什么问题

Storybook 中,一个.stories文件可以同时导出多个 Story,而每个 Story 的play函数是进行组件交互测试的入口。文档 interaction-testing.mdx 指出:当组件包含时间相关逻辑时,直接渲染会让画面随真实时间“漂移”,快照与断言都难以稳定。

代码片段给出的方案是在组件 meta 上声明:

async beforeEach() { MockDate.set('2024-02-14'); return () => { MockDate.reset(); }; }

这一段干了两件事:在每个 Story 渲染之前把全局Date固定到2024-02-14;在 Story 结束(重挂载或导航离开)时自动执行返回的清理函数还原时间。两者配合,即实现了“文件级时间隔离”。

beforeEach生命周期钩子的定位与执行顺序

在 Storybook 的注解类型定义 story.ts 中,beforeEach被描述为:

在每个 Story 之前调用的函数,若为异步则会被 await。beforeEach可以添加到 preview、默认导出(meta)以及具体的某个 Story 上,三者按preview → 默认导出 → story的顺序依次运行(并依次 await),且可以返回一个清理函数。

同一位置还定义了配套的afterEach(运行于每个play函数结束之后,用于后置断言,不应用于清理状态)以及仅允许声明在全局(preview 文件)的beforeAll钩子。

由此可以提炼出beforeEach的三个合法声明位置与语义:

声明位置作用范围运行时机
.storybook/preview.*(全局)项目内所有 Story每个 Story 渲染前
组件 meta(export default该文件内所有 Story每个 Story 渲染前
单个 Story 定义该 Story每个 Story 渲染前

本文片段使用的正是第二个位置——组件 meta。当某些需求是某个组件特有的(例如该文件下的页面组件强依赖固定的“今天”),把它放在 meta 级而非全局 preview 中,职责更内聚,不会影响项目中其他组件的 Story。

在组件 meta 中统一 Mock Date:各渲染器完整写法

官方代码片段为同一逻辑提供了跨渲染器、跨 CSF 语法(CSF 3 与 CSF Next)的多种写法。其核心钩子体完全一致,差异仅在 meta 的组装方式与组件导入语句上,下面按渲染器分组给出可运行的完整示例。

Angular + CSF 3

Angular 渲染器下,meta 是Meta<Page>类型,beforeEach直接写进对象:

import type { Meta, StoryObj } from '@storybook/angular'; import MockDate from 'mockdate'; import { Page } from './Page.component'; const meta: Meta<Page> = { component: Page, // 👇 为文件内每个 Story 固定 Date async beforeEach() { MockDate.set('2024-02-14'); // 👇 在每个 Story 结束后还原 Date return () => { MockDate.reset(); }; }, }; export default meta; type Story = StoryObj<Page>; export const Basic: Story = { async play({ canvas }) { // ... 此处运行在 Mock 的 Date 下 }, };

在 Angular 场景下,官方文档特意强调:Angular 组件在渲染前执行代码的方式就是为 Story 定义异步beforeEach函数(其他渲染器还可通过play中调用mount来完成渲染前准备),这使该片段在 Angular 项目中尤为重要。

React / Vue / Web Components + CSF Next(preview.meta

使用实验性 CSF Next 语法时,meta 通过从.storybook/preview导入的preview.meta({ ... })创建,story 则通过meta.story({ ... })声明。React 的写法如下:

import MockDate from 'mockdate'; import preview from '../.storybook/preview'; import { Page } from './Page'; const meta = preview.meta({ component: Page, // 👇 为文件内每个 Story 固定 Date async beforeEach() { MockDate.set('2024-02-14'); // 👇 在每个 Story 结束后还原 Date return () => { MockDate.reset(); }; }, }); export const Basic = meta.story({ async play({ canvas }) { // ... 此处运行在 Mock 的 Date 下 }, });

Vue 渲染器的 CSF Next 版本结构完全一致,仅需将组件导入改为import Page from './Page.vue'。Web Components 渲染器的 CSF Next 版本也遵循同一模式,不同点是 meta 通过component: 'my-page'指定自定义元素标签名:

import MockDate from 'mockdate'; import preview from '../.storybook/preview'; const meta = preview.meta({ component: 'my-page', // 👇 为文件内每个 Story 固定 Date async beforeEach() { MockDate.set('2024-02-14'); return () => { MockDate.reset(); }; }, }); export const Basic = meta.story({ async play({ canvas }) { // ... }, });

React / Vue / Web Components + CSF 3

不启用 CSF Next 的项目沿用 CSF 3:meta 就是export default的对象,Story 为具名导出。React 的典型写法(配合satisfies获得类型收窄):

import type { Meta, StoryObj } from '@storybook/your-framework'; import MockDate from 'mockdate'; import { Page } from './Page'; const meta = { component: Page, // 👇 为文件内每个 Story 固定 Date async beforeEach() { MockDate.set('2024-02-14'); return () => { MockDate.reset(); }; }, } satisfies Meta<typeof Page>; export default meta; type Story = StoryObj<typeof meta>; export const Basic: Story = { async play({ canvas }) { // ... 此处运行在 Mock 的 Date 下 }, };

其中@storybook/your-framework只是一个占位写法,实际项目需替换为你所用的框架包,例如react-vitenextjsvue3-vite等。若不使用 TypeScript,可以去掉类型标注与satisfies,直接导出纯对象,钩子体保持完全一致。

Web Components + CSF 3(无组件导入)

Web Components 的 CSF 3 版本无需导入组件文件,直接以字符串标签声明组件:

import type { Meta, StoryObj } from '@storybook/web-components-vite'; import MockDate from 'mockdate'; const meta: Meta = { component: 'my-page', // 👇 为文件内每个 Story 固定 Date async beforeEach() { MockDate.set('2024-02-14'); return () => { MockDate.reset(); }; }, }; export default meta; type Story = StoryObj; export const Basic: Story = { async play({ canvas }) { // ... }, };

Svelte + Svelte CSF(defineMeta

Svelte 使用 Storybook 官方的@storybook/addon-svelte-csf扩展,需要在<script module>(模块级脚本)中调用defineMeta并在其中声明钩子,然后用解构出的<Story>组件声明 story:

<script module> import { defineMeta } from '@storybook/addon-svelte-csf'; import MockDate from 'mockdate'; import Page from './Page.svelte'; const { Story } = defineMeta({ component: Page, // 👇 为文件内每个 Story 固定 Date async beforeEach() { MockDate.set('2024-02-14'); return () => { MockDate.reset(); }; }, }); </script> <Story name="Default" play={async ({ canvas }) => { // ... 此处运行在 Mock 的 Date 下 }} />

若 Svelte 项目坚持使用标准 CSF 3(.stories.js/.stories.ts),则只需像其他框架一样把beforeEach写入默认导出即可,见 Svelte CSF 相关文档 之外的 writing-stories 目录 说明。

钩子体详解:MockDate.set与清理函数的配合

先安装依赖:

npm install mockdate

代码片段中的关键点可拆成三层:

  1. MockDate.set('2024-02-14')mockdate会接管全局Date构造函数,使此后新建的new Date()Date.now()new Date(Date.now())等一律返回所设置的时刻(其余时间字段如时分秒归零)。字符串会被解析为本地时区的当天零点,若需要更精确的时间可传入'2024-02-14T12:00:00'这样的完整时间串或时间戳。
  2. return () => { MockDate.reset(); }:这是beforeEach的清理函数约定——钩子返回的函数会在 Story 被重新挂载或导航离开时执行。将它用于还原被篡改的全局状态,正好补上了“测试后残留污染”的缺口。
  3. 作用域:由于钩子声明在 meta 上,setreset的配对逻辑对文件内每一个 Story生效,无需在每个 Story 里重复书写。

官方文档建议不要把清理工作放在afterEach中,理由是afterEach运行在 Story 渲染与交互结束之后,此时清理会破坏你观察 Story 最终状态的能力;正确的还原时机就是beforeEach返回的清理函数,它只在 Story 之间切换时执行,从而保留单次 Story 的终态(见 after-each-in-meta 代码片段相关章节)。

与全局钩子的分工:什么时候用 meta,什么时候用 preview

如果项目中有大量 Story 都依赖固定时间,把还原逻辑放在.storybook/preview.*的全局beforeEach中更省事:

// .storybook/preview.ts import type { Preview } from '@storybook/your-framework'; import MockDate from 'mockdate'; const preview: Preview = { async beforeEach() { MockDate.reset(); }, }; export default preview;

官方代码片段文件 before-each-in-preview.md 给出的正是这个“全局兜底”思路,并保留了 CSF Next 下definePreview({ ... })的等价写法。官方文档(interaction-testing.mdx 中 “Set up or reset state for all tests” 一节)的建议是:

  • 组件或模块状态的复位应优先放在 preview 的全局钩子里,保证覆盖整个项目;meta 级beforeEach的清理函数适合处理某个组件特别定制、不便全局化的状态。
  • 项目级的beforeAll(仅全局可声明,见 story.ts 的类型注释)只在测试会话开始时执行一次,适合做启动引导类操作(其完整示例见 before-all-in-preview.md),不应依赖它在不同 Story 间反复重置时间。
  • 事件监听器等fn()产生的 mock无需手动还原,Storybook 会在渲染前自动恢复,相关细节见parameters.test.restoreMocks文档。

因此对本片段而言,一个稳妥的组合是:meta 级beforeEachMockDate.set+ 清理函数reset,保证文件内确定性;同时也可以在 preview 全局beforeEach中冗余一次MockDate.reset(),为“漏网”的 Story 兜底。

底层实现:清理函数如何被收集与触发

从源码看,Storybook 在正式渲染 Story 前会执行各层beforeEach。在 StoryRender.ts 中可以看到如下调用:

const cleanupCallbacks = await applyBeforeEach(context); this.store.addCleanupCallbacks(story, ...cleanupCallbacks);

即 Storybook 会先对 StoryContext 应用所有beforeEach(并等待异步完成),把返回的清理函数统一登记到 store 中;随后在 Story 重挂载、切换或测试收尾时通过cleanupStory依次执行这些清理回调。这也解释了为什么钩子的返回函数必须在 Story 离开时才运行——因为它是被 Storybook 生命周期统一调度的,而非在play内手动触发。

正因如此,beforeEach中任何异步准备工作(例如等待某个模块初始化完成后再固定时间)都是安全的:它是async且会被await,只有钩子 resolve 之后 Story 才会进入渲染阶段。

实践要点小结

  • 值域一致性:为每个使用时间快照的 Story 固定同一个Date,能同时稳定浏览器渲染、视觉回归(如 Chromatic 快照)与play函数中的断言,是最常见的用法。
  • 匹配真实业务MockDate.set的值应与组件业务场景吻合(如“今天”附近的时间),否则会出现月末、闰年等边界展示与真实日历不一致的假阳性。
  • 不要忘记 reset:任何对全局Date的篡改若不还原,都会顺着 Story 顺序“传染”给后续 Story;务必利用beforeEach的清理函数或全局 preview 钩子成对复位。
  • 渲染器差异只影响包装层:从本文的多个示例可以看出,Angular、React、Vue、Svelte、Web Components 之间的区别仅在 meta 组装语法(CSF 3export default、CSF Nextpreview.meta、SveltedefineMeta)与组件导入方式,async beforeEach() { MockDate.set(...); return () => MockDate.reset(); }这一核心结构在所有渲染器中通用,可直接复制迁移。

若你的测试运行环境切换到 Vitest/Storybook Test,也可以考虑用 Vitest 内置的vi.useFakeTimersvi.setSystemTime实现等价效果;而本文的方案因完全位于 Storybook 生命周期内,对于在浏览器预览面板中手工浏览 Story 的场景同样生效,适用范围更广。

【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 21:25:34

RPCS3 汉化补丁:让 PS3 游戏界面说中文

RPCS3 汉化补丁&#xff1a;让 PS3 游戏界面说中文 【免费下载链接】rpcs3 PlayStation 3 emulator and debugger 项目地址: https://gitcode.com/GitHub_Trending/rp/rpcs3 启动 RPCS3 后游戏菜单、对话与提示全是英文&#xff0c;你找不到任何中文选项。汉化补丁正是解…

作者头像 李华
网站建设 2026/9/8 21:25:07

{AIP Title} — Playbook

{AIP Title} — Playbook 【免费下载链接】airflow Apache Airflow - A platform to programmatically author, schedule, and monitor workflows 项目地址: https://gitcode.com/GitHub_Trending/ai/airflow Airflow version: {version} Required packages: {packages, …

作者头像 李华
网站建设 2026/9/8 21:23:46

基于SIFT的影像拼接算法Matlab实现全流程解析

简介&#xff1a;基于SIFT的影像拼接Matlab实现&#xff0c;面向计算机视觉初学者与图像拼接任务开发者&#xff0c;解决多视角影像自动对齐与融合问题&#xff0c;提供从特征点提取、特征描述与匹配、RANSAC误匹配剔除、单应性矩阵估计到图像融合的完整可运行代码流程。压缩包…

作者头像 李华