Storybook Vue插槽代码不显示?两种方法搞定
2025-04-11 04:27:33
搞定 Storybook Vue 插槽代码预览:不再只显示
写 Vue 组件库文档的时候,Storybook 是个挺方便的工具。但有时候会遇到个小麻烦:如果你的组件用了 Vue 的插槽 (Slots) 特性,在 Storybook 里看这个故事 (Story) 的代码预览,可能只会显示一个光秃秃的组件标签,比如 <Button />
。那些你在模板里精心安排的 <template v-slot:xxx>
部分,它就不给你显示出来。
就像下面这个 Story 例子:
// MyButton.stories.js
import Button from './Button.vue';
import IconAdd from './IconAdd.vue'; // 假设有个图标组件
export default {
title: 'Components/Button',
component: Button,
};
// 一个使用了具名插槽的故事
export const IconText = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { Button, IconAdd },
template: `
<Button>
<template v-slot:text>
按钮文字
</template>
<template v-slot:icon>
<IconAdd />
</template>
</Button>
`
});
理论上,我们希望代码预览能展示 template
里的完整用法,包括插槽。可现实往往是骨感的,预览区可能只显示:
<Button />
这显然不是我们想要的,它没告诉使用者怎么去用这两个插槽 (text
和 icon
)。这咋整呢?
问题出在哪儿?
这事儿通常跟 Storybook 处理故事定义的方式,特别是使用 template
字符串时,如何生成代码片段有关。
当用一个包含多行、嵌套模板 (尤其是 <template>
标签) 的字符串来定义 Story 时,Storybook 的源代码生成插件 (@storybook/addon-docs
的一部分) 可能在解析和“重构”这个源码用于展示时遇到了点困难。它可能只识别了最外层的组件调用 (<Button>...</Button>
),并将其简化成了自闭合标签 <Button />
,或者干脆丢掉了内部的插槽内容。
这种行为,具体取决于 Storybook 版本、Vue 插件版本以及故事的复杂性。但结果就是,重要的插槽使用方法没能在代码预览里体现出来。
怎么解决?
别担心,有几种方法可以绕过或者修正这个问题,让你的插槽用法大大方方地展示出来。
方法一:拥抱 render
函数
不用 template
字符串,改用 Vue 的 render
函数 (通常是 h
函数) 来编写你的故事。render
函数是编程式地创建 VNode (虚拟节点),对于 Storybook 来说,它可能更容易精确地反推出原始的结构,或者说,基于 render
函数的逻辑来生成更靠谱的代码示例。
原理和作用:
render
函数让你用 JavaScript 来组件结构。插槽在 render
函数里通常作为第三个参数 (一个对象) 传递给 h
函数,对象的键是插槽名,值是一个返回 VNode 或 VNode 数组的函数。这种方式更底层,能更精确地控制组件的渲染逻辑,Storybook 分析起来可能更直接。
代码示例:
把上面的 IconText
故事用 render
函数重写:
// MyButton.stories.js
import { h } from 'vue';
import Button from './Button.vue';
import IconAdd from './IconAdd.vue';
export default {
title: 'Components/Button',
component: Button,
};
export const IconText = (args, { argTypes }) => ({
props: Object.keys(argTypes), // 依然可以传递 props
// components 字段在 render 函数中不再需要显式声明
// 但要确保 Button 和 IconAdd 能在作用域内被访问到 (通常 import 就行)
setup() {
// 如果需要访问 args 或者 setup 上下文,可以在这里处理
// 这里简单示例,直接返回 render 函数
return () => h(
Button, // 要渲染的组件
{ ...args }, // Props,可以把 args 传进去
// 第三个参数是 Slots 对象
{
// text 插槽
// 值是一个函数,返回该插槽的内容 (VNode)
text: () => '按钮文字', // 返回文本节点
// icon 插槽
// 值是一个函数,返回该插槽的内容
icon: () => h(IconAdd) // 返回 IconAdd 组件的 VNode
}
);
}
});
// 如果你的 Button 组件不需要 setup,可以直接返回 render 函数
// export const IconText = (args, { argTypes }) => ({
// props: Object.keys(argTypes),
// render: () => h(Button, args, {
// text: () => '按钮文字',
// icon: () => h(IconAdd)
// })
// });
// 注意:根据你的 Vue 版本和 Storybook 配置,可能 setup 写法兼容性更好
效果:
用了 render
函数后,Storybook 生成的代码预览通常就能更好地反映出组件是如何被调用的,有时它甚至能生成类似原始模板的结构(虽然不一定 100% 一致,但会包含插槽信息)。
进阶使用技巧:JSX
如果你配置了 Babel 支持 JSX in Vue,render
函数可以写得更像模板语言,可读性也许会更高:
// MyButton.stories.js
// 确保你的项目配置了 @vue/babel-plugin-jsx
import Button from './Button.vue';
import IconAdd from './IconAdd.vue';
export default {
title: 'Components/Button',
component: Button,
};
export const IconText = (args, { argTypes }) => ({
props: Object.keys(argTypes),
setup() {
return () => (
<Button {...args}>
{{
text: () => <span>按钮文字</span>, // 使用 JSX 语法
icon: () => <IconAdd />, // 使用 JSX 语法
}}
</Button>
);
}
});
// 或者更简洁的写法 (取决于具体配置)
// export const IconText = (args) => (
// <Button {...args}>
// {{
// text: () => <span>按钮文字</span>,
// icon: () => <IconAdd />
// }}
// </Button>
// );
// IconText.argTypes = { /* ... argTypes 定义 ... */ }; // argTypes 可以分开定义
JSX 写法更接近模板的视觉结构,维护起来可能更直观些。
方法二:手动指定 source.code
如果不想改用 render
函数,或者 render
函数生成的预览还是不满意,可以走“手动挡”——直接告诉 Storybook 在文档的代码预览区该显示什么代码。
原理和作用:
Storybook 提供了一个参数 parameters.docs.source.code
,允许你为某个故事硬编码一个代码片段字符串。设置了这个参数后,文档页面的代码预览就会显示你提供的字符串,而不是 Storybook 自动生成的内容。
操作步骤:
在你的故事定义后面,给它加上 parameters
属性:
// MyButton.stories.js
import Button from './Button.vue';
import IconAdd from './IconAdd.vue';
export default {
title: 'Components/Button',
component: Button,
// ... 其他全局参数 ...
};
export const IconText = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { Button, IconAdd },
template: `
<Button>
<template v-slot:text>
按钮文字
</template>
<template v-slot:icon>
<IconAdd />
</template>
</Button>
`
});
// --- 添加这个部分 ---
IconText.parameters = {
docs: {
source: {
// 这里放入你希望展示的代码字符串
// 使用反引号 `` 可以方便地写多行字符串
code: `
<Button>
<template v-slot:text>
按钮文字
</template>
<template v-slot:icon>
<IconAdd />
</template>
</Button>
`,
language: 'html', // 可以指定语言类型,高亮用
type: 'auto', // 或者 'code' | 'dynamic',一般 'auto' 就行
}
}
};
// --------------------
效果:
现在,打开 Storybook 里 IconText
故事的文档页(通常是 "Docs" Tab),代码预览区域就会老老实实地显示你写在 code
字段里的那段 HTML 代码,插槽用法一目了然。
安全建议 / 注意事项:
- 保持同步! 最大的缺点是,如果你修改了故事的
template
逻辑,必须记得手动更新parameters.docs.source.code
里的字符串,否则文档里的代码示例就和实际行为脱节了,这会误导使用者的。 - 代码整洁: 使用模板字符串 (反引号) 可以让多行代码更易读。注意缩进,保持代码整洁。
选哪个好呢?
render
函数 / JSX:- 优点: 代码与逻辑更紧密,理论上更“自动化”(如果 Storybook 解析顺利),改动组件逻辑时通常不用额外维护一份示例代码。更符合 Vue 底层逻辑。
- 缺点: 对不熟悉
render
函数或 JSX 的同学可能有学习成本。有时生成的代码预览可能不是最理想的格式(但通常会包含插槽信息)。
- 手动指定
source.code
:- 优点: 简单直接,完全掌控最终显示的代码样子,对任何复杂的
template
都有效。 - 缺点: 需要手动维护,容易忘记同步更新,增加了额外工作量。
- 优点: 简单直接,完全掌控最终显示的代码样子,对任何复杂的
怎么选?
- 如果你的团队习惯用
render
函数,或者不介意学习它,可以优先尝试 方法一 。 - 如果你的故事模板特别复杂,或者
render
函数生成的预览效果不好,或者你就是想精确控制显示的每一行代码,那么 方法二 是个可靠的备选方案,就是得多留心保持同步。 - 对于简单的插槽用法,方法一 (
render
函数)通常表现不错,值得一试。
还有几点要注意
- 确保
@storybook/addon-docs
已安装和配置 :代码预览功能主要由这个插件提供。检查你的.storybook/main.js
文件,确保addons
数组里包含了它。 - 版本兼容性 :偶尔,特定版本的 Storybook、Vue 相关插件 (
@storybook/vue3
或@storybook/vue
)、Vue 自身版本之间可能存在一些小问题。如果遇到奇怪的行为,查阅官方文档,看看是否有已知的兼容性 issue 或更新的版本。 - 插槽命名 :确保你的
v-slot
指令用得规范,比如具名插槽用v-slot:slotName
或简写#slotName
。规范的写法有助于 Storybook 或相关工具的解析。
通过上面介绍的方法,应该能解决 Storybook 中 Vue 插槽代码预览不显示的问题,让你的组件文档更清晰、更有用。