ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

为组合组件编写首个 Story:Storybook 的 List 空状态起步模板(CSF 3 / CSF Next 与 Svelte CSF 多框架详解)

为组合组件编写首个 Story:Storybook 的 List 空状态起步模板(CSF 3 / CSF Next 与 Svelte CSF 多框架详解) 为组合组件编写首个 StoryStorybook 的 List 空状态起步模板CSF 3 / CSF Next 与 Svelte CSF 多框架详解本篇指南围绕 Storybook 官方写作指南中“为两个或更多组件编写 Stories”的起步模板list-story-starter展开。当父组件List依赖子组件ListItem时如何编写一个最简单的“空列表”Story决定了后续组合渲染、args 复用与子组件文档化的基础。读完本文你将掌握在 React、Angular、Vue 3、Solid、HTML、Web ComponentsLit、Svelte 中写出可运行的List.stories.*起步文件区分 CSF 3 经典写法与实验性 CSF Next 工厂式写法并理解为什么 React/Solid 的空 Story 可以是一个{}而 Vue/HTML/Angular 却需要显式render。这份模板解决什么问题在 Storybook 写作指南 docs/writing-stories/index.mdx 的 “Stories for two or more components” 一节L352-L370中官方给出的场景是某些组件天生需要协作工作例如父级List组件会使用子级ListItem组件。这类组合组件的故事不能只在文档里口述必须落到真实的 story 文件中。该模板源文件 docs/_snippets/list-story-starter.md与文档同目录下的list-story-expanded、list-story-reuse-data、list-story-with-subcomponents等代码块共同构成一条完整的学习链起步starter只声明List组件与一个什么都不渲染的Emptystory先验证故事文件能正常被 Storybook 索引扩展expanded通过自定义render输出包含 0、1、多个ListItem的List复用reuse-data直接复用子组件 story 的 args 数据子组件文档化subcomponents在 meta 中声明父子关系。本文只深入第 1 步——因为它把“meta 声明组件 命名导出 story 各框架渲染约定”的最小骨架完整呈现了出来是所有后续步骤的地基。阅读多框架模板的前提CSF 基本结构模板跨了多个渲染器但本质都遵循Component Story FormatCSF用default exportmeta描述组件加named exports描述各 story来组织故事。文档首页在 docs/writing-stories/index.mdx 中对此作了更完整的阐述并提示自 Storybook 7.0 起 story 标题在构建期被静态分析default export 必须包含可静态读取的title或包含能从其计算自动标题的component。模板代码注释也印证了这一点HTML 变体中特意写明title属性是可选的因为只要给出componentStorybook 就会在构建期生成自动标题。若需了解标题生成可继续阅读 docs/configure 目录下的配置指南。模板每一段代码块顶部都带有renderer渲染器、language语言与tabTitle标签页标题如CSF 3、CSF Next 标记。其中CSF Next 表示该仓库正在实验的下一代工厂式 API官方代码中亦有说明JS 与 TS 两版需同时保留以便文档站点同时呈现两种写法。React空 Story 只需要一个{}对于 React 组件只要 meta 声明了componentStorybook 就能使用该组件的默认渲染。因此起步阶段的Emptystory 是一个不带任何字段的空对象Storybook 会自动用默认 args 渲染组件import { List } from ./List; export default { component: List, }; // Always an empty list, not super interesting export const Empty {};TS 版本则用satisfies把对象字面量约束为Metatypeof List并借助typeof meta让Story类型与 meta 的 args 类型保持联动// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc. import type { Meta, StoryObj } from storybook/your-framework; import { List } from ./List; const meta { component: List, } satisfies Metatypeof List; export default meta; type Story StoryObjtypeof meta; // Always an empty list, not super interesting export const Empty: Story {};在 CSF Next 中不再直接书写 default export而是从项目级.storybook/preview导入preview实例用preview.meta()描述组件用meta.story()定义 story。meta.story()不传任何配置即为默认渲染的空 storyimport preview from ../.storybook/preview; import { List } from ./List; const meta preview.meta({ component: List, }); // Always an empty list, not super interesting export const Empty meta.story();import preview from ../.storybook/preview; import { List } from ./List; const meta preview.meta({ component: List, }); // Always an empty list, not super interesting export const Empty meta.story();Solid与 React 相同的空对象约定Solid 的默认渲染同样由component推断而来因此起步 story 同样是空对象import { List } from ./List; export default { component: List, }; // Always an empty list, not super interesting export const Empty {};TS 变体直接从storybook-solidjs-vite引入类型import type { Meta, StoryObj } from storybook-solidjs-vite; import { List } from ./List; const meta { component: List, } satisfies Metatypeof List; export default meta; type Story StoryObjtypeof meta; // Always an empty list, not super interesting export const Empty: Story {};Vue 3没有默认实例需要 render 返回组件与模板Vue SFC 不存在 React/Solid 那种“仅凭组件即可隐式渲染”的机制story 必须提供render函数返回一个携带components注册表与模板字符串的对象import List from ./ListComponent.vue; export default { component: List, }; // Always an empty list, not super interesting export const Empty { render: () ({ components: { List }, template: List/, }), };import type { Meta, StoryObj } from storybook/vue3-vite; import List from ./ListComponent.vue; const meta { component: List, } satisfies Metatypeof List; export default meta; type Story StoryObjtypeof meta; // Always an empty list, not super interesting export const Empty: Story { render: () ({ components: { List }, template: List/, }), };CSF Next 变体把相同的 render 逻辑挂到meta.story()上JS 与 TS 两版内容一致import preview from ../.storybook/preview; import List from ./ListComponent.vue; const meta preview.meta({ component: List, }); // Always an empty list, not super interesting export const Empty meta.story({ render: () ({ components: { List }, template: List/, }), });import preview from ../.storybook/preview; import List from ./ListComponent.vue; const meta preview.meta({ component: List, }); // Always an empty list, not super interesting export const Empty meta.story({ render: () ({ components: { List }, template: List/, }), });注意template中的标签同时受组件注册名List与 Vue 模板大小写规则影响写成List/是模板里的自闭合写法后续为List追加ListItem子节点时也需要在components中同时注册ListItem。Angular用 moduleMetadata 声明模块依赖Angular 组件的故事必须运行在真实的 Angular 模块/组件上下文中。模板用storybook/angular提供的moduleMetadata装饰器声明List组件并引入CommonModule模板指令如*ngFor等依赖它随后在render里给出模板字符串与透传的 propsimport { type Meta, type StoryObj, moduleMetadata } from storybook/angular; import { CommonModule } from angular/common; import { List } from ./list.component; const meta: MetaList { component: List, decorators: [ moduleMetadata({ declarations: [List], imports: [CommonModule], }), ], }; export default meta; type Story StoryObjList; // Always an empty list, not super interesting export const Empty: Story { render: (args) ({ props: args, template: app-list/app-list, }), };CSF Next 写法把同样的decorators配置传给preview.meta()import { CommonModule } from angular/common; import { moduleMetadata } from storybook/angular; import preview from ../.storybook/preview; import { List } from ./list.component; const meta preview.meta({ component: List, decorators: [ moduleMetadata({ declarations: [List], imports: [CommonModule], }), ], }); // Always an empty list, not super interesting export const Empty meta.story({ render: (args) ({ props: args, template: app-list/app-list, }), });render返回对象中的props: args把 story 的 args 绑定到组件输入属性template中使用的app-list是 Angular 组件的选择器名。HTML用渲染器提供的工厂函数构建 DOMHTML 渲染器没有“组件实例”概念story 依赖 render 函数返回真实 DOM。模板假设项目里存在./List导出的createList工厂函数render 时把 args 交给它import { createList } from ./List; export default { /* The title prop is optional. * See the configure docs on automatic titles. */ title: List, }; // Always an empty list, not super interesting export const Empty { render: (args) createList(args), };import type { Meta, StoryObj } from storybook/html; import { createList, ListArgs } from ./List; const meta: MetaListArgs { /* The title prop is optional. * See the configure docs on automatic titles. */ title: List, }; export default meta; type Story StoryObjListArgs; // Always an empty list, not super interesting export const Empty: Story { render: (args) createList(args), };值得注意HTML 变体是模板中唯一显式给出title: List的写法其注释说明了title可选、可依赖自动标题的前提。这里的 TS 版本把 args 的类型建模为ListArgs并传给Meta与StoryObj泛型是纯 JS/HTML 场景下获得类型提示的常用手段。Web Componentscomponent 是标签名字符串Web Components 渲染器基于 Lit中component不是构造器而是自定义元素标签名如demo-list。模板直接用 Lit 的html标签模板渲染空列表import { html } from lit; export default { component: demo-list, }; // Always an empty list, not super interesting export const Empty { render: () htmldemo-list/demo-list, };import type { Meta, StoryObj } from storybook/web-components-vite; const meta: Meta { component: demo-list, }; export default meta; type Story StoryObj; // Always an empty list, not super interesting export const Empty: Story { render: () htmldemo-list/demo-list, };CSF Next 中preview.meta({ component: demo-list })之后同样的 Lit 渲染被放入meta.story()import { html } from lit; import preview from ../.storybook/preview; const meta preview.meta({ component: demo-list, }); // Always an empty list, not super interesting export const Empty meta.story({ render: () htmldemo-list/demo-list, });import { html } from lit; import preview from ../.storybook/preview; const meta preview.meta({ component: demo-list, }); // Always an empty list, not super interesting export const Empty meta.story({ render: () htmldemo-list/demo-list, });一旦进入“多个ListItem子节点”阶段list-story-expanded只需在demo-list内部嵌套demo-list-item标签即可这正是把组件封装成自定义元素的优势。SvelteSvelte CSF 与标准 CSF 两种范式Svelte 是模板中唯一提供两套完全不同语法的渲染器官方社区主导的Svelte CSF基于storybook/addon-svelte-csf以及通用的CSF 3。Svelte CSF 使用script module中的defineMeta描述组件通过解构出的Story组件以标签形式声明 storyscript module import { defineMeta } from storybook/addon-svelte-csf; import List from ./List.svelte; const { Story } defineMeta({ component: List, }); /script !-- Always an empty list, not super interesting -- Story nameEmpty /TS 版本与 JS 版本在语法上完全一致Svelte CSF 的类型推断来自defineMeta内部script module import { defineMeta } from storybook/addon-svelte-csf; import List from ./List.svelte; const { Story } defineMeta({ component: List, }); /script !-- Always an empty list, not super interesting -- Story nameEmpty /而标准 CSF 3 的 Svelte 写法与 React/Solid 类似空 story 就是空对象渲染交给默认逻辑import List from ./List.svelte; export default { component: List, }; // Always an empty list, not super interesting export const Empty {};// Replace your-framework with svelte-vite or sveltekit import type { Meta, StoryObj } from storybook/your-framework; import List from ./List.svelte; const meta { component: List, } satisfies Metatypeof List; export default meta; type Story StoryObjtypeof meta; // Always an empty list, not super interesting export const Empty: Story {};Svelte 的Story nameEmpty /中name等价于 CSF 的命名导出名两种范式可以共存于同一文档站点。Svelte CSF 与 CSF 的能力边界差异例如部分 args/Controls 特性在写作指南中有专门说明选择范式前应查阅对应文档。CSF Next 工厂式 API 的源码依据preview.meta()与meta.story()并非魔法从源码结构看仓库在 code/core/src/csf/csf-factories.ts 定义了Preview接口与工厂实现。该接口同时声明了_tag: Preview标识字段、.meta()方法把ComponentAnnotations转换为类型安全的Meta以及definePreview()工厂函数——后者会把项目级注解与addons合并成规范化的项目注解源码中通过normalizeProjectAnnotations、composeConfigs等预览 API 完成。各渲染器基于该核心实现对外暴露工厂仓库中存在 code/renderers/react/src/csf-factories.test.tsx、code/renderers/vue3/src/csf-factories.test.ts、code/frameworks/angular/src/client/csf-factories.test.ts 等测试文件而 CSF 处理管线如 processCSFFile.test.ts的测试也直接使用preview.meta({ title: Component })来验证工厂产物能进入正常的故事索引与归一化流程。可以推断meta.story()返回的对象在运行时会被转换成语义等价的 CSF story 注解继续走原有的processCSFFile/normalizeStory管线因此 CSF Next 是在不改变底层存储模型的前提下改进了作者端的类型体验。从 Empty 起步下一步是组合渲染模板注释反复强调 “Always an empty list, not super interesting”始终是空列表没什么意思这正是设计意图起步模板只验证“meta story 骨架”成立。紧接着 docs/writing-stories/index.mdx 的指引是在组合场景下应customize the rendering 来输出带不同数量ListItem子节点的List即 list-story-expanded 中的OneItem/ManyItems随后可复用子组件 story 的数据list-story-reuse-data或在 meta 上声明subcomponents让ListItem的属性出现在 ArgTypes 文档表中list-story-with-subcomponents配套流程文档见 stories-for-multiple-components.mdx。这些扩展与本模板一脉相承Vue/HTML/Web Components 的下一步只是把额外子节点放进render模板或 DOM 构建逻辑Angular 需要把ListItem也加进moduleMetadata的declarations而 React/Solid/Svelte CSF 则需要为OneItem、ManyItems增加显式 render 来嵌套子元素——因为空对象 story 只适用于“默认渲染即可”的起步场景。注意事项与小结从本模板可以提炼出几条写多框架故事时的通用准则能省则省不能省则显式 renderReact/Solid/Svelte标准 CSF由component推导默认渲染空 story 写成{}Vue/HTML/Web Components/Angular 必须给出render。meta 声明优先优先提供component让 Storybook 计算自动标题手动title在 HTML 变体里作为可选示例保留。框架类型入口不同TS 代码中的storybook/your-framework是占位符需替换为实际安装的框架包模板中vue3-vite、storybook-solidjs-vite、web-components-vite是已经具名的真实导入路径。satisfies Meta...与StoryObjtypeof meta的配合能把 args 类型错误提前到编译期暴露。CSF Next 改变的是写法而非机制preview.meta()/meta.story()最终仍产出可被标准 CSF 处理管线消费的注解适合希望获得更强类型约束与组合能力的团队试验。起步文件应随组件文件存放story 文件只用于开发不会进入生产打包产物。理解这份 22 段代码构成的起步模板等于同时拿到了七类主流框架下“为一个组合组件声明第一个故事”的对照词典。它本身信息量不大一个空列表却精准暴露了各框架渲染约定的差异也是通往组合 story、子组件文档化与 args 复用的必经入口。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表