首页
/ Storybook Docs 多框架适配开发指南:如何为新框架优化 Docs 体验

Storybook Docs 多框架适配开发指南:如何为新框架优化 Docs 体验

2026-09-06 12:29:10作者:蔡怀权

本文以 @storybook/addon-docs 的多框架开发指南为主体,系统讲解当你把 Docs 接入一个非 React 视图层(如 Vue、Angular、Web Components、Ember)时,如何分别优化“框架专属配置、Args 表格自动提取、组件描述抽取、故事内联渲染、源码动态渲染”这五大能力。读完你将能对照 addons/docs 的 preset 机制、argTypesEnhancers/extractArgTypes 数据流与 SNIPPET_RENDERED 事件通道,为新框架补齐 Docs 所需的框架级代码,并理解每条参数在源码中的落点。

框架专属配置

Storybook Docs 开箱即支持除 React Native 外的所有视图层,但部分框架(React、Vue 3 等)额外做了 Docs 优化,例如自动 props 表格、内联故事渲染等。要为某个框架补齐这类优化,首先需要处理“框架专属配置”——比如追加 webpack loader,或注入全局 decorator / story parameters。

Docs addon 通过“文件命名约定”来承载这种定制:它的通用 preset 会按 ../<framework>/{preset,config}.[tj]sx? 的规则去查找框架文件,其中 <framework> 是框架标识符(如 vue3angularreact)。这一机制的目的在于让各框架把“构建配置”和“Docs 抽取配置”分离在两个文件里,互不干扰。

以 Vue 为例,它的 webpack 配置需要引入 vue-docgen-loader,同时还有用于 props 表格组件描述 的自定义抽取函数。Docs for Vue 定义了一个 preset.ts,遵循 preset 文件结构:

export function webpack(webpackConfig: any = {}, options: any = {}) {
  webpackConfig.module.rules.push({
    test: /\.vue$/,
    loader: 'vue-docgen-loader',
    enforce: 'post',
  });
  return webpackConfig;
}

这段代码只是追加vue-docgen-loader,此刻的 webpackConfig 已包含通用 preset 所做的修改,因此顺序上“后追加”是安全的。对 props 表格与描述这两个能力,则定义在 config.jsx 中。

从当前仓库源码结构看,这套“框架配置分离”的思路仍然成立,只是入口位置发生了迁移:@storybook/addon-docs 自身的 preset 负责通用能力(如 MDX loader、react/react-dom 别名、CSF enrichment),而各框架的 client 配置被抽离到独立的 framework 包里。例如 Angular 的框架级入口 code/frameworks/angular/src/client/config.ts 同时声明了 parameters(含 docs.extractArgTypesdocs.extractComponentDescription)与 argTypesEnhancers,并从中转出 renderapplyDecorators 等视图层能力:

// code/frameworks/angular/src/client/config.ts
export { render, renderToCanvas } from './render.ts';
export { decorateStory as applyDecorators } from './decorateStory.ts';

export const parameters: Parameters = {
  renderer: 'angular',
  docs: {
    story: { inline: true },
    extractArgTypes,
    extractComponentDescription,
  },
};

export const argTypesEnhancers: ArgTypesEnhancer[] = [enhanceArgTypes];

addon-docs 内仍保留了针对特定框架的轻量入口文件,如 Angular 入口 暴露了 setCompodocJson,它会把 Compodoc 生成的 documentation.json 挂到全局变量上,供 Controls 与 Docs 读取(在开启 experimentalDocgenServer 特性时该调用会被忽略并告警)。

Arg tables(参数表格自动生成)

每个框架都可以自动生成 ArgTable,方式是导出一个或多个 ArgType enhancer,它们把组件的属性抽取成一个通用数据结构。在框架专属的 preview.js 中通常这样写:

import { enhanceArgTypes } from './enhanceArgTypes';

export const argTypesEnhancers = [enhanceArgTypes];

enhanceArgTypes 函数接收一个 StoryContext(含 story id、parameters、args、argTypes 等),并返回一个更新后的 ArgTypes 对象:

export interface ArgType {
  name?: string;
  description?: string;
  defaultValue?: any;
  [key: string]: any;
}

export interface ArgTypes {
  [key: string]: ArgType;
}

不同框架的元数据来源不同,抽取路径也不同:

  • React / Vue:preset 往用户配置里加一个 webpack loader;该 loader 给组件标注一个 __docgenInfo 字段,内含若干元数据;视图层专属的 enhanceArgTypes 再把这份元数据翻译成 ArgTypes
  • Angular / Web components / Ember:读取用户 .storybook/preview.json 里的 JSON 文件并注入一个全局变量;视图层专属的 enhanceArgTypes 再把元数据翻译成 ArgTypes
  • 对于你自己的框架,也可以采用完全不同的实现方式。

当前仓库源码印证了这条数据流。核心 enhancer 实现在 enhanceArgTypes.ts:它从 context 中取出 componentparameters.docs.extractArgTypes,若二者都存在则调用 extractArgTypes(component),再用 combineParameters 把抽取结果与用户手工写的 argTypes 合并(用户显式值优先):

export const enhanceArgTypes = <TRenderer extends Renderer>(
  context: StoryContextForEnhancers<TRenderer>
) => {
  const {
    component,
    argTypes: userArgTypes,
    parameters: { docs = {} },
  } = context;
  const { extractArgTypes } = docs;

  if (!extractArgTypes || !component) {
    return userArgTypes;
  }

  const extractedArgTypes = extractArgTypes(component);
  return extractedArgTypes ? combineParameters(extractedArgTypes, userArgTypes) : userArgTypes;
};

多框架的“多个 enhancer 叠加”由 CSF 组合逻辑保证。在 composeConfigs.ts 中,argTypesEnhancers 是通过 getArrayField 从多个模块导出列表里聚合(concat)起来的,因此 addon、preview、框架包各自导出的 enhancer 会按序执行;对应的行为在 composeConfigs.test.ts 的 “concats argTypesEnhancers in two passes” 用例中有覆盖。

__docgenInfo 这条 React/Vue 路径在源码里依然可见:判断组件是否带 docgen 元数据、以及读取其 description/displayName,分别落在 docgenInfo.tsutils.ts。Angular 的 documentation.json 注入路径则由 setCompodocJson 写入 __STORYBOOK_COMPODOC_JSON__ 全局变量,再由 extractArgTypesFromData(见 code/frameworks/angular/src/client/compodoc.ts)把 JSON 转成 ArgTypes

关于各框架 Controls 的自动生成细节,可参考 props 表格文档

组件描述(Component descriptions)

组件描述由 docs.extractComponentDescription 参数启用,它把组件描述(通常来自源码注释)抽取成一个 Markdown 字符串。它沿用了上一节 Arg tables 的模式,只是更简单——函数输出只是一个字符串(若无描述则返回 null)。

当前实现中,该参数在 Description.tsx 中被调用:当 parameters.docs.extractComponentDescription 存在时,以组件与上下文为入参求得描述并渲染。默认实现会从组件源码注释中抽取 JSDoc;你也可以像 recipes 文档 里演示的那样,用 notes/自定义逻辑覆盖它。

内联故事渲染(Inline story rendering)

内联故事渲染是另一个框架级优化,由 docs.prepareForInline 参数实现。仍以 Vue 的框架专属 preview.js 为例:

import toReact from '@egoist/vue-to-react';

addParameters({
  docs: {
    // `container`、`page` 等
    prepareForInline: (storyFn, { args }) => {
      const Story = toReact(storyFn());
      return <Story {...args} />;
    },
  },
});

输入是 story 函数与 story 上下文(id、parameters、args 等),输出是一个 React element——因为 Docs 页面本身是用 React 渲染的。对 Vue 来说,所有转换工作都由 @egoist/vue-to-react 库完成;如果你的框架没有类似库,就得自己想办法把该框架的渲染结果转成 React 可渲染的元素。

参数在文档侧的组合方式在 DocsPage 参考 中有说明:把 inlineStories 设为 true 后,story 不再被放进 iframe,而 prepareForInline 则负责把非 React 的 story 内容转换为 React 可渲染的形式。两者配合,即可让非 React 视图层的故事“无缝”内联进 DocsPage。

动态源码渲染(Dynamic source rendering)

自 Storybook 6.0 起,Source doc block 对 story 的源码渲染做了增强,其中之一就是 dynamic 源码类型——它基于 story 函数的输出渲染一段代码片段。这种动态渲染是框架相关的,因此每个框架都需要单独实现。

以 React 的 dynamic 片段实现作为参考(供其他框架实现该特性时对照):

import { StoryContext, addons } from '@storybook/preview-api';
import { SNIPPET_RENDERED } from '../../shared';

export const jsxDecorator = (storyFn: any, context: StoryContext) => {
  const story = storyFn();

  // 只有当 Source block 真正会消费它时才渲染 JSX,否则只是拖慢性能
  if (skipJsxRender(context)) {
    return story;
  }

  const channel = addons.getChannel();

  const options = {}; // 从 story parameters 中读取
  const jsx = renderJsx(story, options);

  const { id, args } = context;
  channel.emit(SNIPPET_RENDERED, { id, args, source: jsx });

  return story;
};

上面片段有两个关键点:

  • renderJsx 负责把 story 函数的输出转换成框架专属(这里是 React)的字符串;
  • 转换出的片段字符串通过 channel.emit() 在 Storybook 通道上发出,随后被该 story 对应的 Source block 消费(若存在)。

配置如何展示时,则通过导出 decorators 让该 decorator 作用于每个 story:

import { jsxDecorator } from './jsxDecorator';

export const decorators = [jsxDecorator];

当前仓库中,这条“事件驱动”的源码渲染链路仍然完整存在,且常量与消费方都已收敛到 internal/docs-tools

  • 事件常量 SNIPPET_RENDERED 定义在 shared.ts(形如 ${ADDON_ID}/snippet-rendered);
  • 发送侧封装在 emitTransformCode.ts:它先读取 parameters.docs.source.transform 对原始 source 做可选转换,再通过 addons.getChannel().emit(SNIPPET_RENDERED, { id, source, args, warning }) 发出;
  • 接收侧在 SourceContainer.tsx 中通过 channel.on(SNIPPET_RENDERED, handleSnippetRendered) 监听,并在卸载时 channel.off 解除监听。

可以看出,原文档里“decorator 里手工 channel.emit(SNIPPET_RENDERED, ...)”的写法,在当前版本被抽象成了 emitTransformCode + docs.source.transform 的统一入口;框架专属的差异从“整个 decorator 实现”收敛为“transform 转换器”这一可注入点。理解这一点,对实现新框架的动态源码渲染尤为关键:你只需提供把 story 输出转换为该框架源码字符串的转换逻辑,事件通道的收发由核心统一处理。

更多资源

围绕 Docs 的框架适配,仓库内可供深入的资料包括:

需要说明的是,本文中的 Vue preset/config、jsxDecorator 等示例代码继承自 多框架开发指南原文,用于讲解“框架级优化由谁提供、放在哪”的设计约定;而参数在运行时如何流转(enhancer 合并、__docgenInfo 读取、SNIPPET_RENDERED 事件),则以当前仓库源码为准。为某个具体框架补齐 Docs 能力时,建议先通读对应 framework 包的 client/config.ts,再对照上述核心实现逐条对齐。

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