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

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

2026-09-07 21:33:00作者:柏廷章Berta

在 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 3 export default、CSF Next preview.meta、Svelte defineMeta)与组件导入方式,async beforeEach() { MockDate.set(...); return () => MockDate.reset(); } 这一核心结构在所有渲染器中通用,可直接复制迁移。

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

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388