首页
/ Storybook CSF 3 显式 render 函数全指南:各框架下从 CSF 2 故事函数迁移的权威示例与底层机制解析

Storybook CSF 3 显式 render 函数全指南:各框架下从 CSF 2 故事函数迁移的权威示例与底层机制解析

2026-09-07 09:57:43作者:何将鹤

本指南围绕 Storybook 官方 CSF(Component Story Format)文档中关于 CSF 3 显式 render 函数的核心示例展开,系统讲解如何把 CSF 2 中"故事即函数"的写法迁移为 CSF 3 中"故事对象 + render 属性"的结构,并覆盖 Angular、React、Solid、Svelte、Vue 3、Web Components 六种主流渲染器。读完本文,你将掌握各框架下 render 函数的正确写法、何时可以省略 render(默认渲染函数机制)、如何复用与覆写 render,以及 render 函数与 Args、Controls 在源码层面的协作原理。

render 函数示例在 CSF 文档体系中的位置

在 Storybook 仓库中,本指南对应的原文代码片段位于 csf-3-example-render.md,它被 docs/api/csf/index.mdxDefault render functions(默认 render 函数) 一节直接引用。该文档以 CodeSnippets path="csf-3-example-render.md" 的方式嵌入,服务于一个明确的迁移叙事:

  • CSF 2 中,每个命名导出的故事是一个渲染函数,如 csf-2-example-story.md 所示;
  • CSF 3 中,命名导出改为对象,通过 render 属性显式告诉 Storybook "这个故事应该如何渲染自己"。

这个示例与 csf-3-example-starter.md(CSF 3 基础入门)、csf-3-example-default-render.md(空对象即默认渲染)共同构成一条完整的 CSF 2 → CSF 3 迁移知识链。

迁移对照:故事从"函数"变为"带 render 的对象"

先看 CSF 2 的写法(以下节选自 csf-2-example-story.md):

export const Basic: ComponentStory<typeof Button> = (args) => <Button {...args} />;
export const Basic: StoryFn<typeof Button> = (args) => ({
  components: { Button },
  setup() {
    return { args };
  },
  template: '<Button v-bind="args" />',
});

而在 CSF 3 中,同一故事改写为故事对象并附上 render 函数(即本指南核心文档 csf-3-example-render.md 的全部内容):

export const Basic: Story = {
  render: (args) => <Button {...args} />,
};
export const Basic: Story = {
  render: (args) => ({
    components: { Button },
    setup() {
      return { args };
    },
    template: '<Button v-bind="args" />',
  }),
};

两者的能力边界完全一致——render 给了你"完全控制故事如何渲染组件、甚至渲染一组组件"的能力。区别在于:CSF 2 故事本身就是函数,CSF 3 故事是携带元数据(argsparametersdecorators 等)的对象render 只是其中一个字段。

注意:render 在 CSF 3 里并非必需。它只在你想控制渲染输出时使用。关于"何时可以省略"见下文「默认渲染函数」一节。

六种渲染器的显式 render 函数写法

以下代码块完整继承自核心文档,覆盖了 Storybook 官方支持的各渲染器。示例均基于同一个假设:Button 组件已经导入,文件顶部还有"其他 import 与故事实现"(如 actionMetaStory 类型等,不同渲染器的导入包路径略有差异)。

React(JS / TSX)

React 的 render 函数接收 args 并返回 JSX,直接把 args 展开到组件上是标准姿势:

// Other imports and story implementation
export const Basic = {
  render: (args) => <Button {...args} />,
};
// Other imports and story implementation
export const Basic: Story = {
  render: (args) => <Button {...args} />,
};

其中 Story 类型通常由 type Story = StoryObj<typeof meta> 推导而来(参考 csf-3-example-starter.md),文件顶部需根据框架导入 MetaStoryObj,例如 @storybook/react-vite@storybook/nextjs 等。

Angular(TS)

Angular 渲染器要求 render 返回带 props 的对象。若要在模板中按 args 动态绑定组件输入,应使用框架导出的 argsToTemplate 工具(详见下文「向 DOM 输出展开 args」):

// Other imports and story implementation
export const Basic: Story = {
  render: (args) => ({
    props: args,
  }),
};

Solid(JS / TSX)

与 React 语法高度一致,render 直接返回组件调用结果:

// Other imports and story implementation
export const Basic = {
  render: (args) => <Button {...args} />,
};
// Other imports and story implementation
export const Basic: Story = {
  render: (args) => <Button {...args} />,
};

Svelte(JS / TS)

Svelte 的 render 返回 { Component, props } 结构对象,Storybook 据此实例化 Svelte 组件:

// Other imports and story implementation
export const Basic = {
  render: (args) => ({
    Component: Button,
    props: args,
  });
};
// Other imports and story implementation
export const Basic: Story = {
  render: (args) => ({
    Component: Button,
    props: args,
  }),
};

注意:若你使用 Svelte 专用 CSF(.stories.svelte),则走 defineMeta + <Story> 组件 + template snippet 的体系,而不是本文的普通对象导出(可参考 docs/api/csf/index.mdx 中对 Svelte 的特殊说明)。

Vue 3(JS / TS)

Vue 的 render 返回一个"组件选项对象",需要把 args 暴露给 setup(),再在 template 中通过 v-bind 绑定:

// Other imports and story implementation
export const Basic = {
  render: (args) => ({
    components: { Button },
    setup() {
      return { args };
    },
    template: '<Button v-bind="args" />',
  }),
};
// Other imports and story implementation
export const Basic: Story = {
  render: (args) => ({
    components: { Button },
    setup() {
      return { args };
    },
    template: '<Button v-bind="args" />',
  }),
};

Web Components(JS / TS)

Web Components 渲染器使用 lit 的 html 模板标签,把 args 映射为自定义元素的 attribute/property 绑定:

// Other imports and story implementation

export const Basic = {
  render: (args) => html`<demo-button label="Hello" @click=${action('clicked')}></demo-button>`,
};
// Other imports and story implementation

export const Basic: Story = {
  render: (args) => html`<demo-button label="Hello" @click=${action('clicked')}></demo-button>`,
};

上例的 htmllit 导入;action 需要从 @storybook/addon-actions 导入(示例文件注释里省略了该 import)。事件监听写法 @click=${...} 即 lit 的 @event 简写语法。

什么时候必须写 render:真实的实战场景

故事对象的默认行为是"渲染 meta 中声明的组件并把 args 传给它"(见 docs/writing-stories/index.mdx 中 Custom render functions 一节的说明)。因此,当你需要渲染非默认内容时才需要自定义 render。典型场景:

  1. 组合多个组件——例如把一个 Button 渲染进一个 Alert 容器里,形成"按钮位于警示条中"的复合场景;
  2. 固定页面骨架/布局——例如自定义渲染函数把组件放进 Layout > header > article 的页面结构中(Angular/React/Vue/Web Components 均有此类示例,见 component-story-with-custom-render-function.md);
  3. 精确控制组件实例与周边 DOM

以 React 为例,将 Button 放进 Alert 渲染:

export const PrimaryInAlert: Story = {
  args: { primary: true, label: 'Button' },
  render: (args) => (
    <Alert>
      Alert text
      <Button {...args} />
    </Alert>
  ),
};

对应的 Vue 版本则需要模板返回(完整对照见 render-custom-in-story.md):

export const PrimaryInAlert: Story = {
  render: (args) => ({
    components: { Alert, Button },
    setup() { return { args }; },
    template: '<Alert><Button v-bind="args" /></Alert>',
  }),
  args: { primary: true, label: 'Button' },
};

render 中展开 args 的原因:让 Controls 继续生效

官方文档特别强调(见 docs/writing-stories/index.mdx 的 Callout):render 函数中必须把 args 展开(spread)到目标组件上。这正是上面 React 里 {...args}、Vue 里 v-bind="args"、Angular 里 props: args / argsToTemplate(args) 存在的原因——只有 args 真正流入了组件,Controls 面板才能在运行时动态改写组件属性并即时生效。

meta 级 render:一次定义,多故事复用

同一个 render 常常适用于多个故事。此时可以把 render 从故事对象提升到 meta(default export)层:

const meta = {
  component: Button,
  render: (args) => (
    <Alert>
      Alert text
      <Button {...args} />
    </Alert>
  ),
} satisfies Meta<typeof Button>;

export const DefaultInAlert: Story = { args: { label: 'Button' } };
export const PrimaryInAlert: Story = { args: { label: 'Button', primary: true } };

更完整的框架对照见 render-custom-in-meta.md。两条关键规则:

  • 故事级 render 会覆盖 meta 级 render,因此需要特殊渲染的单个故事仍可自由定制;
  • render 函数接收第二个参数 context,其中包含该故事的其余全部上下文,如 parametersglobalsargs 之外的加载器数据等(参见 docs/api/csf/index.mdxwriting-stories/index.mdx)。

大多数故事不需要 render:默认渲染函数

迁移的实用建议是:CSF 2 中大量故事函数"长得都一样"——取 default export 里的组件,把 args 展开进去渲染。这种故事真正有价值的不是函数体,而是传入的 args

因此 CSF 3 为每个渲染器都内置了默认 render 函数:只要你的需求就是"把 args 展开渲染到组件",完全可以不写任何 rendercsf-3-example-default-render.md 给出的极简形式是:

export const Basic = {};

配合 meta 中的组件声明,一个空对象故事即可完成与显式 render 完全相同的渲染。这也意味着在多数迁移场景下,CSF 2 → CSF 3 的工作量主要是删除样板式的渲染函数、把 args/parameters 从函数属性搬进对象字段,而不是为每个故事新增 render。

源码视角:render 函数如何在 Storybook 内部被执行

理解 render 的运作需要回到 renderer 的实现上。以 React 渲染器为例:

  • code/renderers/react/src/applyDecorators.ts 中,story 被包进默认装饰器,核心是通过 React.createElement(storyFn, context) 将"故事可渲染内容"实例化为 React 元素。这里 storyFn 的底层来源就是当前故事对象的 render 逻辑(无论它是用户显式定义的 render,还是框架按组件 + args 派生的默认渲染);
  • code/renderers/react/src/renderToCanvas.tsx 导出 renderToCanvas,它负责把该元素真正挂载到预览 iframe 的画布节点上;
  • 而在 preview-web 层,PreviewWeb 测试与集成测试 通过 mock renderToCanvas 验证了"渲染上下文 → renderer → canvas"这条链路。

由此可以推断出完整的执行链条:Storybook 收集故事对象 → 组装 args 与 context → 调用故事(或 meta)的 render 得到可渲染内容 → renderer 的 renderToCanvas 把它绘制到画布。这也解释了为何 render 函数的签名是 (args, context)——第一个参数提供动态数据,第二个参数携带渲染所需的全部上下文。

对 Angular 这类模板驱动框架,render 返回的对象需包含模板与组件注册信息,且借助 argsToTemplate 把 args 序列化为模板绑定字符串(见 component-story-with-custom-render-function.md 中的 Angular 示例);对 Web Components 则依赖 lit 的模板系统把 args 绑定为元素的属性/事件。渲染器之间的差异正是默认 render 函数"按框架定制"的原因。

迁移 Checklist 与要点回顾

将 CSF 2 故事迁移到 CSF 3 的显式 render 写法时,对照以下要点检查:

检查项 说明
故事类型 命名导出从函数改为对象,TS 下标注 Story = StoryObj<typeof meta>(各渲染器类型来自对应框架包,如 @storybook/react-vite@storybook/vue3-vite@storybook/web-components-vite@storybook/angularstorybook-solidjs-vite
render 返回值 React/Solid 返回元素;Angular 返回 { props }(可加 template);Svelte 返回 { Component, props };Vue 返回带 components/setup/template 的对象;Web Components 返回 lit 模板
args 展开 在 render 内展开 args({...args} / v-bind="args" / argsToTemplate),保证 Controls 动态改参可用
复用策略 多个故事共享渲染逻辑时把 render 提到 meta 层;故事级 render 可单独覆写
何时省略 仅需"组件 + args"时完全省略 render,交给各渲染器默认渲染函数
第二参数 需要 parameters/globals 等上下文时使用 render: (args, context) => ...

想继续深入可阅读仓库内以下资料:docs/api/csf/index.mdx(CSF 规范总览与迁移章节)、docs/writing-stories/index.mdx(render 函数实战与 Callout 提醒)、component-story-with-custom-render-function.md(复杂页面骨架示例),以及各渲染器的源码实现目录(如 code/renderers/react)。CSF 2 → CSF 3 的批量自动迁移还可借助仓库提供的 codemod 完成(参见 docs/api/csf/index.mdx 中关于 migrate-csf-2-to-3 的命令说明与 migrate-csf-2-to-3.md)。

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