Storybook Core-Client 架构解析:框架如何接入浏览器端预览系统(renderToCanvas / render / decorateStory)
2026/9/7 1:50:50 网站建设 项目流程

Storybook Core-Client 架构解析:框架如何接入浏览器端预览系统(renderToCanvas / render / decorateStory)

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

本文围绕 Storybook 仓库中 Core-Client 包的官方说明文档 展开,讲解这个浏览器端共享层的历史定位、核心契约(start(renderToCanvas, { render, decorateStory })三要素)以及configure()返回值的设计意图,并结合当前仓库中 React 渲染器与 preview-api 的源码实现,说明这套契约在今天的代码里以什么形态存在、如何工作。读完后,你将理解框架适配层与核心预览系统之间的职责边界,以及renderToCanvasdecorateStory等关键概念在源码中的真实调用关系。

Core-Client 是什么:v6 时代遗留的浏览器端共享层

README-core-client.md 开篇即点明了这个包的身份:

This package contains browser-side functionality shared amongst all the frameworks (React, RN, Vue 3, Ember, Angular, etc) in the old "v6" story store back-compatibility layer.

也就是说,Core-Client(原独立包@storybook/core-client)承担的是所有框架(React、React Native、Vue 3、Ember、Angular 等)在浏览器端共享的功能层。它存在的前提是 v6 时代的故事存储(story store)向后兼容需求——不同 UI 框架对"把故事渲染到画布"这件事的实现方式完全不同,但 Storybook 核心需要一套统一的接口来屏蔽这种差异。

需要注意的是,这个包在仓库演进中已经被合并。preview-api 包的 README 明确记录了这一变迁:

This package used to be multiple packages (they have been combined into this one):

  • @storybook/addons
  • @storybook/core-client
  • @storybook/preview-web
  • @storybook/store

因此,README-core-client.md现在作为 preview-api 包下的历史子包文档保留(同级还有 README-addons.md、README-preview-web.md、README-store.md),对应源码分别落在code/core/src/preview-api/modules/下的addons/preview-web/store/三个目录中。理解这一点是读懂后续内容的关键:文档描述的是历史契约,而当前源码展示的是同一契约的现代化形态

核心契约:start() 与三个由框架提供的函数

文档的核心内容定义了框架接入 Storybook 的调用约定:

A framework calls thestart(renderToCanvas, { render, decorateStory })function and provides:

  • TherenderToCanvasfunction, which tells Storybook how to render the result of a story function to the DOM
  • Therenderfunction, which is a default mapping ofargsto a story result in CSFv3
  • ThedecorateStoryfunction, which tells Storybook how to combine decorators in the framework.

这条调用约定确立了框架适配层的三个职责,每个都对应一个清晰的边界:

函数职责解决的问题
renderToCanvas把 story 函数的结果渲染到 DOM 画布不同框架(React / Vue / Svelte / Angular / Ember)挂载、卸载、更新组件的方式完全不同
renderCSFv3 中args到 story 结果的默认映射用户不再手写function() { return <Button /> },而是声明args,框架负责默认把args展开成组件调用
decorateStory在框架内部组合(compose)decorators装饰器最终要变成该框架的"嵌套组件"(如 React 的<Wrapper><Story/></Wrapper>),核心层无法代劳

其中renderToCanvas是最关键的抽象:Storybook 核心只关心"故事函数被调用后产出了一个可渲染对象",至于这个对象如何落到 canvas 元素上(是否要 ErrorBoundary、是否要act()包裹、如何卸载旧组件),完全交给框架实现。

用 React 渲染器看 renderToCanvas 的真实实现

当前仓库中 React 渲染器的入口 entry-preview.tsx 保留了与文档契约完全对应的导出:

export { render } from './render.tsx'; export { renderToCanvas } from './renderToCanvas.tsx'; export { mount } from './mount.ts'; export { applyDecorators } from './applyDecorators.ts';

同时它导出项目级注解(decoratorsparametersbeforeAll),例如parameters: { renderer: 'react' }声明了渲染器身份,beforeAll中配置了与storybook/test集成的asyncWrapper/eventWrapper(基于 Reactact)。从源码结构看,这正是文档所述"框架向 Storybook 提供渲染能力"契约在当前代码中的落点:框架不再通过start()注册,而是直接以模块导出的形式暴露这三类函数。

具体到 renderToCanvas.tsx 的实现,可以看到一个框架适配层需要承担的完整细节:

  1. 接收统一的RenderContext:签名是renderToCanvas({ storyContext, unboundStoryFn, showMain, showException, forceRemount }, canvasElement)。核心层把"未绑定的故事函数"和渲染上下文交出来,框架决定怎么用。
  2. ErrorBoundary 包裹:非 portable story 会被包进ErrorBoundary组件,componentDidCatch时调用showException(err),正常挂载时调用showMain()——这就是核心层"出错时如何在界面上展示"的回调约定。
  3. StrictMode 支持const Wrapper = FRAMEWORK_OPTIONS?.strictMode ? StrictMode : Fragment;,说明框架选项(FRAMEWORK_OPTIONS)会直接影响渲染行为。
  4. act 队列串行化actQueue+processActQueue保证多个并发的act()调用被串行处理,渲染本身在act(async () => renderElement(element, canvasElement, ...))中执行;注释中说明 docs 视图下会禁用 act(对应 issue 30356 的行为)。
  5. forceRemount 语义:切换故事时需要先unmountElement(canvasElement)再挂载,否则"React 不会为每次故事运行重建实例";但改变 args/globals 时则走更新路径而不重挂载。
  6. 返回 cleanup 函数return async () => { await act(() => { unmountElement(canvasElement); }); }——核心层拿到的是一个卸载回调,用于下一次渲染前的清理。这个"渲染函数返回清理函数"的模式是框架契约的隐含约定。

同样的契约在 Vue 3、Svelte、Preact、Web Components、Angular、Ember 等渲染器中都有对应实现,例如 vue3 的 render.ts、Angular 客户端的 render.ts 与 config.ts、Ember 的 render.ts。多框架各自实现renderToCanvas,是这套契约存在的根本原因。

decorateStory:装饰器组合为什么必须交给框架

文档对decorateStory的定义是"告诉 Storybook 如何在该框架内组合 decorators"。这句话的深意在于:装饰器在语义上是"包裹",但包裹的语法是框架相关的——在 React 里是嵌套 JSX,在 Vue 里是组件包裹,在 Angular 里可能是模板嵌套。核心层只能传递"装饰器列表 + 上下文",无法生成最终 AST。

在当前的 store 实现中,这一职责体现在 prepareStory.ts:

// Combine all the metadata about a story (both direct and inherited from the // component/global scope) into a "render-able" story function, with all // decorators applied, parameters passed as context etc export function prepareStory<TRenderer extends Renderer>( storyAnnotations: NormalizedStoryAnnotations<TRenderer>, componentAnnotations: NormalizedComponentAnnotations<TRenderer>, projectAnnotations: NormalizedProjectAnnotations<TRenderer> ): PreparedStory<TRenderer> { ... }

prepareStory把故事级、组件级、项目级三层注解合并,并在其中调用 loaders、beforeEach等钩子,最终产出一个"可渲染的故事函数"。注释明确说明这个函数是无状态的——它不跟踪 args 或 globals,而是期望这些值在每次调用时从外部传入。装饰器在这一阶段被组合进故事函数,而真正"把组合结果翻译成框架语法"的,就是框架侧的decorateStory/applyDecorators(React 侧对应 applyDecorators.ts)。这解释了为什么文档把"组合 decorators"列为框架职责而非核心职责:核心做语义层组合,框架做语法层落地

render:CSFv3 中 args 到 story 结果的默认映射

文档中render的定义是"a default mapping ofargsto a story result in CSFv3"。CSFv3 的核心简化是:用户只声明args,不必手写 render 函数;只有需要自定义时才显式提供render。因此框架提供的render默认兜底实现——React 渲染器导出render(见 render.tsx),其典型行为是把args展开为组件 props。当用户未提供自定义render时,核心预览流程就会落到这个框架默认映射上;当用户提供了render(例如渲染插槽、组合多个组件或返回非组件值),用户版本会覆盖默认映射。

这一点与renderToCanvas形成两级抽象的分工:

  • renderargs→ 故事结果(框架相关,React 中通常是 JSX 元素);
  • renderToCanvas:故事结果 → DOM 画布(框架相关,涉及挂载/卸载/错误处理)。

核心层两者都不实现,只定义调用时机与数据形状。

start() 的返回值:configure() 与 storiesOf 历史

文档最后一段描述了start的返回值:

Thestartfunction will return aconfigure()function, which can be re-exported to be used inpreview.js(deprecated), or automatically by themain.js:storiesfield to:

  • return a list of CSF files
  • deprecatedmake calls to thestoriesOfAPI.

这里包含三层信息:

  1. configure()曾是 preview 入口:早期(v6 之前)用户需要在preview.js中手动调用configure(require.context('../stories', true, /\.stories\.(js|tsx?)$/))来注册故事文件;
  2. main.jsstories字段取代:后来改为在main.js中声明 stories glob,构建管线自动完成注册,preview.js中手写configure()变为弃用路径;
  3. storiesOfAPI 整体弃用storiesOf().add()的旧式 API 与 CSF 文件模式并存后被淘汰,文档中直接标注了deprecated

从当前源码结构看,start()这一入口签名已不再出现在代码树中(检索不到start(renderToCanvas, ...)的调用点),框架改为通过 entry-preview.tsx 这类模块直接导出render/renderToCanvas/ 装饰器等,浏览器端预览实例则演化为 PreviewWeb.tsx 中的PreviewWeb类(继承自PreviewWithSelection,构造时接收importFngetProjectAnnotations,并挂到global.__STORYBOOK_PREVIEW__)。可以推断,start()configure()是 v6 及更早版本的引导(bootstrap)机制,如今其职责被"框架模块导出 + PreviewWeb 实例化"取代,但三要素契约(渲染、args 映射、装饰器组合)本身被完整继承了下来。

当前代码中的对应关系速查

文档概念(v6 契约)当前仓库中的形态关键文件
renderToCanvas各渲染器直接导出的同名函数react、vue3、angular
render(args 默认映射)各渲染器导出的rendercode/renderers/react/src/render.tsx
decorateStory(装饰器组合)store 层prepareStory做语义组合 + 框架侧applyDecorators做语法落地prepareStory.ts、applyDecorators.ts
start()返回值 /configure()PreviewWeb实例 +main.js:stories自动注册PreviewWeb.tsx
v6 back-compat 共享层本体合并进 preview-api 的store/addons/preview-web子模块code/core/src/preview-api/README.md

小结

README-core-client.md虽然篇幅不长,但它定义了 Storybook 框架适配层与核心预览系统之间最本质的接口契约:

  • 核心层不碰 DOM:它只做注解归一化、装饰器语义组合(prepareStory)、args/globals 状态管理;
  • 框架层负责三个不可跨框架复用的动作:把args映射成框架对象(render)、把框架对象挂到画布并处理挂载/卸载/错误(renderToCanvas)、把装饰器列表翻译成框架语法(decorateStory);
  • 历史引导机制已退役start()configure()storiesOf的 v6 引导链被"模块导出 +main.js:stories+PreviewWeb实例化"取代,但三要素契约在 code/renderers 各渲染器与 code/core/src/preview-api 源码中仍可一一对应地找到。

如果你在为新框架编写 Storybook 渲染器,这份文档加上当前 React 渲染器的 entry-preview.tsx 与 renderToCanvas.tsx,就是最直接的参考实现。

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询