Storybook Core-Client 架构解析:框架如何接入浏览器端预览系统(renderToCanvas / render / decorateStory)
本文围绕 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/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 the
start(renderToCanvas, { render, decorateStory })function and provides:
- The
renderToCanvasfunction, which tells Storybook how to render the result of a story function to the DOM- The
renderfunction, which is a default mapping ofargsto a story result in CSFv3- The
decorateStoryfunction, which tells Storybook how to combine decorators in the framework.
这条调用约定确立了框架适配层的三个职责,每个都对应一个清晰的边界:
| 函数 | 职责 | 解决的问题 |
|---|---|---|
renderToCanvas |
把 story 函数的结果渲染到 DOM 画布 | 不同框架(React / Vue / Svelte / Angular / Ember)挂载、卸载、更新组件的方式完全不同 |
render |
CSFv3 中 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';
同时它导出项目级注解(decorators、parameters、beforeAll),例如 parameters: { renderer: 'react' } 声明了渲染器身份,beforeAll 中配置了与 storybook/test 集成的 asyncWrapper / eventWrapper(基于 React act)。从源码结构看,这正是文档所述"框架向 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 队列串行化:
actQueue+processActQueue保证多个并发的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 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 of args to a story result in CSFv3"。CSFv3 的核心简化是:用户只声明 args,不必手写 render 函数;只有需要自定义时才显式提供 render。因此框架提供的 render 是默认兜底实现——React 渲染器导出 render(见 render.tsx),其典型行为是把 args 展开为组件 props。当用户未提供自定义 render 时,核心预览流程就会落到这个框架默认映射上;当用户提供了 render(例如渲染插槽、组合多个组件或返回非组件值),用户版本会覆盖默认映射。
这一点与 renderToCanvas 形成两级抽象的分工:
render:args→ 故事结果(框架相关,React 中通常是 JSX 元素);renderToCanvas:故事结果 → DOM 画布(框架相关,涉及挂载/卸载/错误处理)。
核心层两者都不实现,只定义调用时机与数据形状。
start() 的返回值:configure() 与 storiesOf 历史
文档最后一段描述了 start 的返回值:
The
startfunction 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.
这里包含三层信息:
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、angular |
render(args 默认映射) |
各渲染器导出的 render |
code/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,就是最直接的参考实现。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00