组件库开发—Storybook生成UI文档
简介
Storybook是一个UI组件的开发环境。
使用
初始化StoryBook环境
$ npx -p @storybook/cli sb init
storybook自动检测开发环境,安装依赖。
执行以上命令行会进行以下操作:
1. 自动生成以下目录结构:
├─.storybook // Storybook 全局配置文件
├─ main.js // 入口文件
└─ preview.js // 页面展示、全局资源配置
└─stories // 示例代码
└─assets
2. 更新pkg#run-scripts:
...
"scripts": {
"storybook": "start-storybook -p 6006 -h 0.0.0.0", // 启动本地服务以预览
"build-storybook": "build-storybook" // 构建
},
...
核心文件
main.js
module.exports = {
"stories": [ // 组件Stories目录所在 —— Storybook会载入配置路径下的指定文件渲染展示
"../stories/**/*.stories.mdx",
"../stories/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [ // Storybook所用插件 —— Storybook功能增强
"@storybook/addon-links",
"@storybook/addon-essentials"
],
"framework": "@storybook/vue3" // Storybook所用框架 —— Vue环境支持
}
该文件定义StoryBook与编译相关的配置。
preview.js
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
}
}
该文件引入全局依赖,定义StoryBook渲染相关的配置。
简单示例
入口配置
更新.storybook/main.js,将组件所在目录注册到入口文件声明中:
module.exports = {
"stories": [ // 组件Stories目录所在 —— Storybook会载入配置路径下的指定文件渲染展示
"../packages/**/*.stories.@(js|jsx|ts|tsx)"
],
...
}
组件Story编写
import SubmitForm from "./index"; // 引入组件
import { SchemaType, RuleTrigger } from "./src/schemas/baseSchema";
const caseSchema = [ // 示例数据
{
key: "moduleName",
name: "title",
type: SchemaType.Text,
label: "栏目名称",
placeholder: "请输入栏目名称",
attrs: {
//
},
rules: [
{
required: true,
message: "栏目名称必填~",
trigger: RuleTrigger.Blur,
},
],
},
...
];
export default {
title: "ui组件/SubmitForm", // 展示标题:使用路径定义命名空间 —— 分组、分类
component: SubmitForm,
};
const Template = (args: any) => ({ // 渲染组件
components: { SubmitForm },
setup() {
return {
...args,
};
},
template: '<submit-form :schema="schema"></submit-form>',
});
export const 基本应用 = Template.bind({}); // 组件应用示例
(基本应用 as any).args = {
schema: caseSchema,
ref: "submitFormRef",
};
其中,
默认导出的是组件的元数据,包含归属组件、组件所属StoryBook文档分类、组件参数交互式声明...
更多配置参见:
命名导出的Story ( export const 基本应用 = Template.bind({}); ) 是一个函数,变量名为StoryBook文档展示的标题,另一种导出方式参考下文。
全局依赖配置
因为示例代码中依赖element-plus,通过上述展现的页面没有样式,所以,StoryBook渲染需要额外引入element-plus主题:
// .storybook/preview.js
import "element-plus/lib/theme-chalk/index.css";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
}
}
启动本地服务
启动服务
更新pkg#storybook:
// package.json
"scripts": {
"storybook": "start-storybook -p 6006 -h 0.0.0.0",
"build-storybook": "build-storybook"
},
命令行执行:
$ npm run storybook
效果展示
image默认参数栏只展示两项,如需更多参数信息,修改 preview.js 文件:
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
controls: {
expanded: true // 展开所有参数信息
}
}
image
在Stories中使用第三方UI库
以ElementPlus为例:
全局配置
如果 babel.config
没有配置按需加载,可直接编辑.storybook/preview.js
:
// .storybook/preview.js
import elementPlus from 'element-plus';
import { app } from '@storybook/vue3'
app.use(elementPlus);
export const decorators = [
(story) => ({
components: { story, elementPlus },
template: '<elementPlus><story/></elementPlus>'
})
];
import "element-plus/lib/theme-chalk/index.css";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
}
}
Notes:配置按需加载后,import elementPlus from 'element-plus';
导入elementPlus报错:elementPlus is not defined
—— 全局加载、按需加载不能在同一项目中使用。
按需加载
在需要使用ElementPlus的Stories中直接引入即可:
// SubmitForm.stories.ts
import { ElButton } from 'element-plus';
import SubmitForm from "./index";
import { SchemaType, RuleTrigger } from "./src/schemas/baseSchema";
const caseSchema = [
{
key: "moduleName",
name: "title",
type: SchemaType.Text,
label: "栏目名称",
placeholder: "请输入栏目名称",
attrs: {
//
},
rules: [
{
required: true,
message: "栏目名称必填~",
trigger: RuleTrigger.Blur,
},
],
},
...
];
export default {
title: "ui组件/SubmitForm",
component: SubmitForm,
};
const Template = (args: any) => ({
components: { SubmitForm, ElButton },
setup() {
return {
...args,
};
},
template: '<submit-form :schema="schema" ref="submitFormRef"></submit-form><el-button @click="submit">提交</el-button>',
});
export const 基本应用 = Template.bind({});
(基本应用 as any).args = {
schema: caseSchema,
};
补充已有示例交互
// SubmitForm.stories.ts
import { ElButton } from "element-plus";
import { ref } from "vue";
import SubmitForm from "./index";
import { SchemaType, RuleTrigger } from "./src/schemas/baseSchema";
const caseSchema = [
{
key: "moduleName",
name: "title",
type: SchemaType.Text,
label: "栏目名称",
placeholder: "请输入栏目名称",
attrs: {
//
},
rules: [
{
required: true,
message: "栏目名称必填~",
trigger: RuleTrigger.Blur,
},
],
},
...
];
export default {
title: "ui组件/SubmitForm",
component: SubmitForm,
};
const Template = (args: any) => ({
components: { SubmitForm, ElButton },
setup() {
const { refName } = args;
const submitFormRef = ref();
function submit() {
console.log(submitFormRef.value.values);
}
function onRuntimeChange(name: string, value: any) {
console.log(name, " = ", value);
}
return {
submit,
onRuntimeChange,
[refName]: submitFormRef,
...args,
};
},
template: `
<submit-form :schema="schema" :ref="refName" @runtimeChange="onRuntimeChange"></submit-form>
<el-button @click="submit">提交</el-button>
`,
});
export const 基本应用 = Template.bind({});
(基本应用 as any).args = {
refName: "submitFormRef",
schema: caseSchema,
};
这里做了两件事:
-
增加提交按钮
-
增加数据提交交互
配置参数文档
默认文档展示
默认查看到的文档是两栏展示:
image更新 .storybook/preview.js
文件:
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
controls: {
expanded: true
}
}
参数的所有配置都展示:
image参数配置
通过配置argTypes可以补充参数信息:
// SubmitForm.stories.ts
...
export default {
title: "ui组件/SubmitForm",
component: SubmitForm,
argTypes: {
refName: {
description: '表单组件引用',
type: {
required: true,
},
table: {
defaultValue: {
summary: 'defaultNameRef',
}
},
control: {
type: 'text'
}
},
schema: {
type: {
required: true,
},
table: {
type: {
summary: '渲染表单所需JSON结构',
detail: 'JSON结构包含表单渲染、交互所需要的必要字段,也包含表单的校验规则',
},
defaultValue: {
summary: '[]',
detail: `[
{
key: "moduleName",
name: "title",
type: SchemaType.Text,
label: "栏目名称",
placeholder: "请输入栏目名称",
attrs: {
//
},
rules: [
{
required: true,
message: "栏目名称必填~",
trigger: RuleTrigger.Blur,
},
],
}
]
`
}
}
},
runtimeChange: {
description: '实时监听表单的更新',
table: {
category: 'Events',
},
}
}
};
...
更多相关配置见:
StoryBook功能模块
一个Story是以函数形式描述如何渲染组件的方式。
args
提供动态参数,提供省时的便利:
-
参数可在
Controls
面板实时编辑,可检测组件在不同参数下的状态。 -
事件可在
Actions
面板查看日志输出。- 需要配置actions
自定义Stories展示名称
命名模块
export const 自定义名称 = () => ({
components: { Button },
template: '<Button primary label="Button" />',
});
【推荐】storyName属性设定
export const Primary = () => ({
components: { Button },
template: '<Button primary label="Button" />',
});
Primary.storyName = '自定义名称';
Args
提供交互动态修改Props、Slots、styles、inputs...的方式,允许在Storybook交互界面中实时编辑,不必改动底层组件代码。
通过Storybook交互界面指定Args
界面直接修改
通过URL设定Args
?path=/story/avatar--default&args=style:rounded;size:100
由于安全策略(XSS攻击)和特殊值,传入URL需要处理,见详情。
全局依赖
// .storybook/preview.js
import { app } from '@storybook/vue3';
import Vuex from 'vuex';
//👇 Storybook Vue app being extended and registering the library
app.use(Vuex);
export const decorators = [
(story) => ({
components: { story },
template: '<div style="margin: 3em;"><story /></div>',
}),
];
静态资源访问
// .storybook/main.js
module.exports = {
stories: [],
addons: [],
staticDirs: ['../public'],
};
或
// .storybook/main.js
module.exports = {
staticDirs: [
{
from: '../my-custom-assets/images',
to: '/assets'
}
],
};
查看更多配置
定制化主题
修改Logo
安装依赖:
npm i -D @storybook/addons @storybook/theming
修改pkg#scripts
// pkg#scripts
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook -s public",
新建.storybook/manager.js文件:
import { addons } from "@storybook/addons";
import theme from "./themes/theme";
addons.setConfig({
theme: theme
})
创建./storybook/themes/theme.js:
// .storybook/themes/theme.js
import { create } from '@storybook/theming';
export default create({
base: 'light',
brandTitle: 'Custom StoryBook', // logo不展示时,替代文本alt
brandImage: '/logo.png',
});
Notes:brandImage和brandTitle同时配置的情况下,只有一项起作用,优先级brandImage > brandTitle
Notes:自定义主题时,base配置是必填的。
// package.json
{
...
"scripts": {
"replace": "rimraf storybook-static/favicon.ico && cpr .storybook/themes/favicon.ico storybook-static/favicon.ico",
"storybook": "start-storybook -p 6006 -h 0.0.0.0",
"build-storybook": "build-storybook && npm run replace"
},
}
Notes:打包的话,需要用本地图标替换storybook包内的默认图标。
这里使用了Cli参数-s
指定静态文件访问地址,更推荐在main.js中配置:
// .storybook/main.js
module.exports = {
stories: [],
addons: [],
staticDirs: ['/public'],
};
修改站点Title、favicon
新增.storybook/manager-head.html:
<link rel="shortcut icon" href="/favicon.ico">
<script>
var observer = new MutationObserver(function(mutations) {
if (document.title.match(/Storybook$/)) {
document.title = "M.UI | GameCenter";
}
}).observe(document.querySelector("title"), {
childList: true,
subtree: true,
characterData: true
});
</script>
参见更多配置
组件样式—Scss
组件样式需要storybook-addon的支持
npm i -D @storybook/preset-scss
修改.storybook/main.js
module.exports = {
"stories": [
"../packages/**/*.stories.@(js|jsx|ts|tsx)",
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/preset-scss"
],
"framework": "@storybook/vue3"
}
Rem支持
替换@storybook/preset-scss为@storybook/addon-postcss:
npm uninstall -D @storybook/preset-scss
npm i -D @storybook/addon-postcss
# 修改webpack内核为版本5
# 初始化环境时修改
npx -y sb init --builder webpack5
# 初始化时未设定,后续修改
npm i -D @storybook/builder-webpack5
npm i -D @storybook/manager-webpack5
// 修改.storybook/main.js
module.exports = {
"stories": [
"../packages/**/*.stories.@(js|jsx|ts|tsx)",
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
{
name: '@storybook/addon-postcss',
options: {
postcssLoaderOptions: {
implementation: require('postcss'),
},
sassLoaderOptions: {
implementation: require('sass'),
}
},
}
],
core: {
builder: 'webpack5',
},
webpackFinal: (config) => {
config.module.rules.push({
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
});
return config
},
"framework": "@storybook/vue3"
}
在preview.js中引入flexible.js,并定义视窗:
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
import '../assets/js/flexible.all';
import './assets/stylesheets/sb.scss';
import '../assets/stylesheets/reset.scss';
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
viewport: {
viewports: INITIAL_VIEWPORTS, // newViewports would be an ViewportMap. (see below for examples)
defaultViewport: 'iphone6',
},
controls: {
expanded: true
}
}
详细内容参见文章地址
同类对比
Decorator自定义画布、文档样式
Decorator通过包裹Story来增强其表现形式。
作用域:全局 > Component > Story(按执行顺序排列)
// js示例
export default {
title: 'YourComponent',
component: YourComponent,
decorators: [() => ({ template: '<div style="margin: 3em;"><story/></div>' })],
};
// mdx示例
<Meta
title="YourComponent"
component={YourComponent}
decorators={[
() => ({
template: '<div style="margin: 3em;"><story /></div>',
}),
]}
/>
Decorator可在.storybook/preview.js
、组件声明、Story声明中定义,最终会合并,执行顺序为 Global > Component > Story,可通过定义ClassName的命名空间来定制样式。
更多配置: https://storybook.js.org/docs/vue/writing-stories/decorators#wrap-stories-with-extra-markup
画布、文档的分离
默认文档中包含画布信息,若不想文档中渲染Story,需要写.stories.js和.stories.mdx两个文件
其中,.stories.js中默认导出不添加任何元数据,转移至.stories.mdx中,示例如下:
// *.stories.ts
export default {}
const Template = (args: any) => ({
components: {
Drawer
},
props: Object.keys(args),
methods: {
onClose: action('onClose'),
onStretched: action('onStretched')
},
setup () {
const state = reactive({
...args
})
function toogleVisible () {
state.visible = true
}
return {
state,
toogleVisible
}
},
template: `
<Drawer v-bind="state" v-model:visible="state.visible" @close="onClose" @stretched="onStretched">
</Drawer>
<button @click="toogleVisible">切换可见性</button>
`
})
export const Primary = Template.bind({}) as any
(Primary as any).args = {
visible: false,
title: '示例'
}
Primary.parameters = { docs: { disable: true } };
// *.stories.mdx
import { Meta, Story } from '@storybook/addon-docs'
import { Primary } from './Drawer.stories.ts';
import Drawer from './index'
export const argsType = {
visible: {
description: '是否显示Dialog,支持.sync修饰符',
type: {
required: true
},
table: {
type: {
summary: 'Boolean'
},
defaultValue: {
summary: false
}
// category: 'Boolean' // 参数分组
// subcategory: 'Button colors', // 子分组
},
control: 'boolean'
},
...
}
# Drawer
## 基本用法
<Story
name="Drawer"
decorators={[
() => ({
template: '<div id="custom-root" style="background: red;"><story /></div>',
})
]}
story={Primary} />
## 自定义内容
## 参数文档
<Meta
title="组件/Basic/Drawer"
component={Drawer}
argTypes={{
...argsType
}}
/>
parameters = { docs: { disable: true } }可在docs中禁止渲染Story
https://storybook.js.org/docs/vue/api/mdx#documentation-only-mdx
https://github.com/storybookjs/storybook/blob/master/addons/docs/docs/recipes.md#docspage
https://github.com/storybookjs/storybook/blob/master/addons/docs/docs/mdx.md#documentation-only-mdx
Notes:
- mdx文件中jsx和markdown语法之间要用空行分隔;
- jsx定义对象,尤其是空对象,不能有多余的空行;
export const argTypes = {
}
# 这样会报错
正确写法:
export const argTypes = {}
# 这样才能正确解析
侧边栏忽略子节点
若不想要侧边栏创建子节点,可以定义Story.storyName与export default的组件title保持一致:
// 示例
export default {
title: '组件/Basic/Drawer'
}
export const Primary = Template.bind({}) as any
Primary.storyName = 'Drawer';
调整docs优先展示权
默认优先展示stories,可以通过优先展示docs
parameters = {
docs: {
disable: true
},
viewMode: 'docs'
}
隐藏Canvas
previewTabs: {
canvas: {
hidden: true,
}
},
可以隐藏当前Stories的Canvas面板
修改Logo跳转地址
// .storybook/themes/theme.js
import { create } from '@storybook/theming';
export default create({
base: 'light',
brandTitle: 'StoryBook',
brandUrl: '/?path=/docs/快速开始--primary',
brandImage: '/logo.png',
});
关闭Addons
showPanel无论设置在哪一层级,都是全局的
parameters = {
docs: {
disable: true
},
controls: {
disable: true
},
options: {
showPanel: false
}
}
MDX写法
动机
上述的写法为CSF,component story format,是 Storybook 官方推荐的一种基于 ES module 的 stories 编写方法,由一个 export default
和一个或多个 export 组成。
它是Storybook 提供了一些封装过后的组件供使用,让我们能够较为快速的生成 stories。
代价是灵活度会相对的没有高,当然,如果只是简单的展示组件及其接收参数,那其实已经完全足够了。
可如果在展示组件之余,还想要编写一个额外的文档,比如介绍一下组件封装的背景,用到的技术等,CSF 就不是那么好用了。
基于这样的需求,Storybook 也支持使用 MDX 格式编写 stories。
MDX,如同 TSX,就是一种能够在 Markdown 文档中写 JSX 的格式。使用 MDX 格式编写 stories,文字部分内容的编写会更加灵活,没有了官方预置的内容,真正的所写即所得。
MDX示例
import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs';
import { action } from '@storybook/addon-actions'
import { reactive } from 'vue'
import Dialog from './index'
# Dialog
export const argsType = {
visible: {
description: '是否显示Dialog,支持.sync修饰符',
type: {
required: true
},
table: {
type: {
summary: 'Boolean'
},
defaultValue: {
summary: false
}
// category: 'Boolean' // 参数分组
// subcategory: 'Button colors', // 子分组
},
control: 'boolean'
},
showCancel: {
description: '展示独立的关闭按钮X',
table: {
type: {
summary: 'Boolean'
},
defaultValue: {
summary: true
}
},
control: 'boolean'
},
title: {
description: 'Dialog的标题,也可通过具名slot(见下表)传入',
table: {
defaultValue: {
summary: '示例Dialog'
}
},
control: 'text'
},
rawHtml: {
description: 'dialog主体内容',
table: {
type: {
summary: 'string / htmlString'
},
defaultValue: {
summary: 'DJ小能手',
detail: '<span style="color: red;">DJ</span>小能手'
}
},
control: 'text'
},
confirm: {
description: '确认相关的配置项目',
mapping: {
label: '确定按钮的文本内容',
handler: '确认的回调'
},
options: ['label', 'handler'],
table: {
type: {
summary: 'Object',
detail: `
confirm.label: 确定按钮的文本内容;
confirm.handler: 确认的回调;
`
}
},
control: 'object'
},
cancel: {
description: '取消相关的配置项目',
table: {
type: {
summary: 'Object',
detail: `
cancel.label: 取消按钮的文本内容;
cancel.handler: 取消的回调;`
}
},
control: 'object'
},
header: {
description: 'Dialog标题区的内容',
table: {
type: {
summary: 'Vnode'
},
defaultValue: ''
},
control: 'text',
category: 'Slots'
},
default: {
description: 'Dialog的内容',
table: {
type: {
summary: 'Vnode'
},
defaultValue: ''
},
control: 'text',
category: 'Slots'
},
'update:visible': {
table: {
disable: true
}
}
}
export const actionData = {
updateVisible: action('update:visible')
}
## 参数文档
<Meta
title="组件/Basic/Dialog"
component={Dialog}
argTypes={{
...argsType
}}
/>
<ArgsTable story="基本用法" />
## 基本用法
export const argsData = {
visible: false,
showCancel: true,
confirm: {
label: '确定',
handler () {
console.log('确定')
}
},
cancel: {
handler () {
console.log('X')
}
}
}
export const HTMLTemplate = `
<Dialog v-bind="state" v-model:visible="state.visible" @update:visible="updateVisible">
<template v-slot:header v-if="state.header">
<header v-html="state.header"></header>
</template>
<template v-slot:default v-if="state.default">
<main v-html="state.default"></main>
</template>
</Dialog>
<button @click="toggleVisible">切换可见性</button>
`
export const ConstructorFactory = (args) => ({
components: { Dialog },
props: Object.keys(args),
setup () {
const state = reactive({
...args
})
function toggleVisible () {
state.visible = true
}
return {
state,
toggleVisible
}
},
methods: {
...actionData
},
template: HTMLTemplate
})
<Canvas
mdxSource={HTMLTemplate}
>
<Story
name="基本用法"
args={{...argsData}}
>
{
ConstructorFactory.bind({})
}
</Story>
</Canvas>
内置组件
Meta
声明本 MDX 文件渲染的页面的标题,对应的组件等。作用和 CSF 写法中的 export default
一致;
ArgsTable
自定义arguments类型,用于Props、Slots、Events展示与交互。
属性 | 示例 | 属性说明 | 属性值 | 属性值示例 |
---|---|---|---|---|
of | <ArgsTable of={ComponentObj} /> |
自动解析组件的Props、Slots、Events等声明,依据Storybook内置类型声明输出Storybook文档 | import导入的组件对象 |
import Dialog from './index' 的"Dialog" |
story | <ArgsTable story="StoryNameString" /> |
自定义arguments,需要使用story属性承接arguments类型声明。 | Story标签的name属性值 |
<Story name="storyName"> 的"storyName" |
Canvas
生成一个 Canvas 画板,用于展示我们自己编写的组件。
画布会提供一些便捷的功能,比如展示当前组件的源代码等;
Canvas默认将Stroybook的Template函数作为源码输出,若想自定义源码输出,将源码作为属性mdxSource
的值即可。
Story
生成一个 story。
Notes:并不是一定要把 story 写在 <Canvas />
组件中,Canvas 组件只是能够提供一些其他的功能(展示源码、复制代码之类的)。直接编写 story 的话,组件也能正常渲染。
区别在于,写在<Canvas />
中,Canvas
会提供便捷的功能:工具栏、源代码查看...
vscode语法提示插件
名称: MDX
ID: silvenon.mdx
说明: Provides syntax highlighting and bracket matching for MDX (JSX in Markdown) files.
版本: 0.1.0
发布者: Matija Marohnić
VS Marketplace 链接: https://marketplace.visualstudio.com/items?itemName=silvenon.mdx
名称: MDX Preview
ID: xyc.vscode-mdx-preview
说明: MDX Preview
版本: 0.3.3
发布者: Xiaoyi Chen
VS Marketplace 链接: https://marketplace.visualstudio.com/items?itemName=xyc.vscode-mdx-preview
名称: Vue Storybook Snippets
ID: megrax.vue-storybook-snippets
说明: Storybook Snippets for Vue
版本: 0.0.7
发布者: Megrax
VS Marketplace 链接: https://marketplace.visualstudio.com/items?itemName=Megrax.vue-storybook-snippets