首页
/ Storybook Web Components 文档指南:基于 custom-elements.json 自动生成 Props 表格与组件文档

Storybook Web Components 文档指南:基于 custom-elements.json 自动生成 Props 表格与组件文档

2026-09-06 21:35:07作者:魏献源Searcher

在 Storybook 中为 Web Components(Lit、Stencil、Polymer 等框架或纯 Vanilla 自定义元素)编写文档时,与普通 React/Vue 组件最大的差异在于:没有 PropTypes、TypeScript 装饰器这类“就地可查”的类型信息,组件元数据必须依赖 custom-elements.json 这份外部清单文件。本文基于 Storybook 官方文档 WEB_COMPONENTS.md 完整展开,讲解如何为 Web Components 接入 Storybook Docs:安装与配置 setCustomElementsManifest、通过清单文件自动生成 Props 表格、以及用 docs.story.iframeHeight / docs.stories.inline 参数控制故事的 iframe 渲染方式,并结合开源仓库源码说明元数据是如何被解析、映射为 Docs 页面上 argTypes 表格的。

读完本文,你将能够:

  1. 按文档要求完成 @storybook/addon-docs + Web Components 渲染器的接入;
  2. 选择或生成符合 v1.0.0 规范的 custom-elements.json 文件并理解其结构;
  3. 让 DocsPage 自动渲染出 Properties / Attributes / Events / Slots / CSS Shadow Parts 等分类表格;
  4. 从源码层面理解 setCustomElementsManifestgetCustomElementsextractArgTypes 这条数据链路的实际实现。

前置条件:先完成 Docs 通用安装

官方文档明确要求,在配置 Web Components 专属能力之前,必须先在 README.md 中描述的通用流程下安装 Docs 插件。核心步骤为:

yarn add -D @storybook/addon-docs

然后在 .storybook/main.js 中注册插件并让 stories 可被索引:

export default {
  stories: [
    '../src/**/*.mdx',
    '../src/**/*.stories.@(js|jsx|ts|tsx)',
  ],
  addons: [
    '@storybook/addon-docs',
  ],
};

同时确认项目中已经存在一份可用的 custom-elements.json 文件(后文详述如何生成与校验),否则 Props 表格将无数据可渲染。

安装:向 preview 注入 custom-elements 清单

Web Components 接入 Docs 的第一步,是把 custom-elements.json 的内容注入到预览运行时。按 WEB_COMPONENTS.md 的 Install 一节,在 .storybook/preview.js 中添加:

import { setCustomElementsManifest } from '@storybook/web-components';
import customElements from '../custom-elements.json';

setCustomElementsManifest(customElements);

然后在故事文件中,用标签名字符串作为 component 字段:

export default {
  title: 'Demo Card',
  component: 'your-component-name', // 该名字必须能在 custom-elements.json 中找到
};

源码视角:清单存到哪里、何时被读取

setCustomElementsManifest 的实现位于 framework-api.ts,它把清单对象挂到全局变量上:

export function setCustomElementsManifest(customElements: any) {
  global.__STORYBOOK_CUSTOM_ELEMENTS_MANIFEST__ = customElements;
}

同文件中的 getCustomElementsframework-api.ts)在 Docs 提取 argTypes 时读取该全局变量,并带有历史兼容逻辑——旧版 setCustomElements(对应 __STORYBOOK_CUSTOM_ELEMENTS__ 全局变量)注入的数据同样可用:

export function getCustomElements() {
  return global.__STORYBOOK_CUSTOM_ELEMENTS__ || global.__STORYBOOK_CUSTOM_ELEMENTS_MANIFEST__;
}

这意味着如果你维护的是较老版本 Storybook 时代通过 setCustomElements 注入的清单(实验性 tags 结构),迁移到新 API 前功能仍然保持兼容。同时 isValidMetaDataframework-api.ts)会对清单做结构校验:数据中必须包含 tags 数组(实验性格式)或 modules 数组(v1.0.0 格式),否则抛出明确指向 addon-docs 文档的错误提示,帮助定位配置缺失。

Props 表格:custom-elements.json 的结构与生成

要让 Props tables 在 Web Components 上生效,必须提供 custom-elements.json。官方文档给出两条路径:手写,或使用分析器自动生成——后者通常更可靠,且“不同 Web Components 技术栈(sugar)下效果不一(your mileage may vary)”。

文档中列出的已知分析器:

输出 custom-elements.json v1.0.0 的分析器:

  • @custom-elements-manifest/analyzer(OpenWC 出品)
    • 支持 Vanilla、LitElement、FAST Element、Stencil、Catalyst、Atomico

输出旧版格式的分析器:

  • web-component-analyzer
    • 支持 LitElement、Polymer、Vanilla、(Stencil)
  • stenciljs 自带生成
    • 支持 Stencil(但元数据不全)

用 Stencil 生成清单文件

如果使用 Stencil,只需在 stencil.config.tsoutputTargets 中追加 docs-vscode 输出:

{
  type: 'docs-vscode',
  file: 'custom-elements.json'
},

清单文件长什么样

一份 v1.0.0 格式的最小结构如下(摘自 WEB_COMPONENTS.md 原文示例,完整示例可参考仓库中的 web-components-kitchen-sink 项目):

{
  "schemaVersion": "1.0.0",
  "readme": "",
  "modules": [
    {
      "kind": "javascript-module",
      "path": "src/my-element.js",
      "declarations": [
        {
          "kind": "class",
          "description": "",
          "name": "MyElement",
          "members": [
            { "kind": "field", "name": "disabled" },
            { "kind": "method", "name": "fire" }
          ],
          "events": [
            { "name": "disabled-changed", "type": { "text": "Event" } }
          ],
          "superclass": { "name": "HTMLElement" },
          "tagName": "my-element"
        }
      ],
      "exports": [
        {
          "kind": "custom-element-definition",
          "name": "my-element",
          "declaration": {
            "name": "MyElement",
            "module": "src/my-element.js"
          }
        }
      ]
    }
  ]
}

关键字段说明:

字段 作用
schemaVersion 清单版本,Docs 解析器按 v1.0.0 处理
modules[].path 组件源文件路径,用于代码链接等元信息
modules[].declarations[].tagName 自定义元素标签名,Docs 通过它与故事中的 component 字段精确匹配(大小写不敏感)
members / properties 属性(Properties)来源,映射到 Props 表格
events 事件(Events)来源,映射为 onXxx action 参数
superclass 继承关系,文档中展示
exports 导出声明,标识元素定义与类的对应关系

源码视角:Docs 如何把清单变成表格数据

清单到表格的转换核心在 custom-elements.tsgetMetaDatacustom-elements.ts)根据清单版本分派到两条解析路径:

const getMetaData = (tagName: string, manifest: any) => {
  if (manifest?.version === 'experimental') {
    return getMetaDataExperimental(tagName, manifest);
  }
  return getMetaDataV1(tagName, manifest);
};
  • getMetaDataExperimentalcustom-elements.ts):面向旧版 tags 数组结构,按 tag.name 大小写不敏感匹配;
  • getMetaDataV1custom-elements.ts):遍历 modules[].declarations[],以 declaration.tagName === tagName 精确匹配 v1.0.0 格式。

两条路径在找不到组件时都会输出警告日志 Component not found in custom-elements.json: <tagName>,这是排查“表格为空”时最重要的控制台线索。

匹配成功后,extractArgTypesFromElementscustom-elements.ts)把各字段映射为 Storybook 的 ArgTypes

metaData && {
  ...mapData(metaData.members ?? [], 'properties'),
  ...mapData(metaData.properties ?? [], 'properties'),
  ...mapData(metaData.attributes ?? [], 'attributes'),
  ...mapData(metaData.events ?? [], 'events'),
  ...mapData(metaData.slots ?? [], 'slots'),
  ...mapData(metaData.cssProperties ?? [], 'css custom properties'),
  ...mapData(metaData.cssParts ?? [], 'css shadow parts'),
}

可以推断,这些 table.category 分类就是 DocsPage 上 Properties / Attributes / Events / Slots / CSS Custom Properties / CSS Shadow Parts 各表格的分组依据。映射细节还包括:

  • mapItemcustom-elements.ts):为属性/事件/插槽生成 namedescriptiontypetable.defaultValue 等字段;slots 类型固定为 string
  • mapEventcustom-elements.ts):把 disabled-changed 这类 kebab-case 事件名转换为 onDisabledChanged,并附加 action: { name: ... },使 Controls 面板能直接触发该事件、Actions 面板能记录调用——这是 Web Components 文档与框架文档在“事件可控性”上对齐的关键机制;
  • mapDatacustom-elements.ts)中显式过滤 kind === 'method' 的成员,即 custom-elements.json 中的 methods 不会出现在 Props 表格里,方法与事件、属性在表格体系里是区分的。

相关测试 custom-elements.test.ts 验证了将清单写入 window.__STORYBOOK_CUSTOM_ELEMENTS_MANIFEST__ 后提取逻辑生效,覆盖了这条全局变量数据链路。

渲染方式:故事默认内联,可切换为 iframe

Web Components 在 DocsPage 中的故事默认以 inline 方式渲染——直接把自定义元素插入文档流。当组件内部使用了 Shadow DOM 隔离的复杂样式、或者内联渲染出现布局问题时,可以改用 iframe 渲染。

WEB_COMPONENTS.md 的 “Stories not inline” 一节,做法是在 .storybook/preview.js 中设置:

export const parameters = { docs: { story: { inline: false } } };

iframe 模式下:

  • 默认 iframe 高度为 60px
  • 可用故事参数 docs.story.iframeHeight 调整高度,避免内容被裁切。

这一配置对全局生效;如需只对部分故事生效,可以在单个故事的 parameters 中覆盖同样的 docs.story 结构。

进一步阅读

官方文档末尾的 “More resources” 指向了 Docs 插件的核心参考资料,均位于本仓库中:

小结

为 Web Components 编写 Storybook 文档的关键在于三点:一是按 README.md 完成 @storybook/addon-docs 的通用安装;二是准备并注入符合 v1.0.0 规范的 custom-elements.json(推荐 @custom-elements-manifest/analyzer 或 Stencil 的 docs-vscode 输出目标生成),再通过 setCustomElementsManifest 交给运行时;三是善用 docs.story.iframeHeightdocs.stories.inline 参数处理 Shadow DOM 组件的内联渲染问题。从源码看,framework-api.ts 负责清单的全局注入与校验,custom-elements.ts 负责把 modules[].declarations 解析为带分类的 ArgTypes,最终由 DocsPage 呈现为可交互的 Props 表格。

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