Storybook Core-Client 架构解析:框架如何接入浏览器端预览系统(renderToCanvas / render / decorateStory)
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 的源码实现说明这套契约在今天的代码里以什么形态存在、如何工作。读完后你将理解框架适配层与核心预览系统之间的职责边界以及renderToCanvas、decorateStory等关键概念在源码中的真实调用关系。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/addonsstorybook/core-clientstorybook/preview-webstorybook/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 DOMTherenderfunction, which is a default mapping ofargsto a story result in CSFv3ThedecorateStoryfunction, 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在框架内部组合composedecorators装饰器最终要变成该框架的嵌套组件如 React 的WrapperStory//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;同时它导出项目级注解decorators、parameters、beforeAll例如parameters: { renderer: react }声明了渲染器身份beforeAll中配置了与storybook/test集成的asyncWrapper/eventWrapper基于 Reactact。从源码结构看这正是文档所述框架向 Storybook 提供渲染能力契约在当前代码中的落点框架不再通过start()注册而是直接以模块导出的形式暴露这三类函数。具体到 renderToCanvas.tsx 的实现可以看到一个框架适配层需要承担的完整细节接收统一的RenderContext签名是renderToCanvas({ storyContext, unboundStoryFn, showMain, showException, forceRemount }, canvasElement)。核心层把未绑定的故事函数和渲染上下文交出来框架决定怎么用。ErrorBoundary 包裹非 portable story 会被包进ErrorBoundary组件componentDidCatch时调用showException(err)正常挂载时调用showMain()——这就是核心层出错时如何在界面上展示的回调约定。StrictMode 支持const Wrapper FRAMEWORK_OPTIONS?.strictMode ? StrictMode : Fragment;说明框架选项FRAMEWORK_OPTIONS会直接影响渲染行为。act 队列串行化actQueueprocessActQueue保证多个并发的act()调用被串行处理渲染本身在act(async () renderElement(element, canvasElement, ...))中执行注释中说明 docs 视图下会禁用 act对应 issue 30356 的行为。forceRemount 语义切换故事时需要先unmountElement(canvasElement)再挂载否则React 不会为每次故事运行重建实例但改变 args/globals 时则走更新路径而不重挂载。返回 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 prepareStoryTRenderer extends Renderer( storyAnnotations: NormalizedStoryAnnotationsTRenderer, componentAnnotations: NormalizedComponentAnnotationsTRenderer, projectAnnotations: NormalizedProjectAnnotationsTRenderer ): PreparedStoryTRenderer { ... }prepareStory把故事级、组件级、项目级三层注解合并并在其中调用 loaders、beforeEach等钩子最终产出一个可渲染的故事函数。注释明确说明这个函数是无状态的——它不跟踪 args 或 globals而是期望这些值在每次调用时从外部传入。装饰器在这一阶段被组合进故事函数而真正把组合结果翻译成框架语法的就是框架侧的decorateStory/applyDecoratorsReact 侧对应 applyDecorators.ts。这解释了为什么文档把组合 decorators列为框架职责而非核心职责核心做语义层组合框架做语法层落地。renderCSFv3 中 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 filesdeprecatedmake calls to thestoriesOfAPI.这里包含三层信息configure()曾是 preview 入口早期v6 之前用户需要在preview.js中手动调用configure(require.context(../stories, true, /\.stories\.(js|tsx?)$/))来注册故事文件被main.js的stories字段取代后来改为在main.js中声明 stories glob构建管线自动完成注册preview.js中手写configure()变为弃用路径storiesOfAPI 整体弃用storiesOf().add()的旧式 API 与 CSF 文件模式并存后被淘汰文档中直接标注了deprecated。从当前源码结构看start()这一入口签名已不再出现在代码树中检索不到start(renderToCanvas, ...)的调用点框架改为通过 entry-preview.tsx 这类模块直接导出render/renderToCanvas/ 装饰器等浏览器端预览实例则演化为 PreviewWeb.tsx 中的PreviewWeb类继承自PreviewWithSelection构造时接收importFn与getProjectAnnotations并挂到global.__STORYBOOK_PREVIEW__。可以推断start()→configure()是 v6 及更早版本的引导bootstrap机制如今其职责被框架模块导出 PreviewWeb 实例化取代但三要素契约渲染、args 映射、装饰器组合本身被完整继承了下来。当前代码中的对应关系速查文档概念v6 契约当前仓库中的形态关键文件renderToCanvas各渲染器直接导出的同名函数react、vue3、angularrenderargs 默认映射各渲染器导出的rendercode/renderers/react/src/render.tsxdecorateStory装饰器组合store 层prepareStory做语义组合 框架侧applyDecorators做语法落地prepareStory.ts、applyDecorators.tsstart()返回值 /configure()PreviewWeb实例 main.js:stories自动注册PreviewWeb.tsxv6 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:storiesPreviewWeb实例化取代但三要素契约在 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),仅供参考