1 什么是 Storybook

Storybook is an open source tool for developing UI components and pages in isolation. It simplifies building, documenting, and testing UIs.

Storybook 是一个开源工具,它能有组织和高效地构建 UI 组件,文档编制和测试,包括 React、Vue 和 Angular 。

特点:

分开展示各个组件不同属性下的状态;

能追踪组件的行为并且具有属性调试的功能;

可以为组件自动生成文档和属性列表;

2 安装

根据官网,本人使用的 react 项目,所以,直接控制台运行如下命令,集成 Storybook:本人安装当前最新版本为 "@storybook/react": "^6.2.9",

# Add Storybook:

npx sb init

安装成功后,直接在控制台运行如下命令,就可以看到启动页面:

# Starts Storybook in development mode

npm run storybook

成功启动运行在 6006 端口

3 说明

根目录生成的 .storybook为 storybook 默认配置目录;

src/stories 目录为 storybook 页面组件目录;

本人项目是 ts,安装完成 storybook 后, storybook 页面组件默认就是 tsx,无需再额外配置;

4 decorators

decorators 的作用主要是统一修饰组件展示区域的样式,例如:设置组件展示都居中,或者是 margin、padding 的距离等等。

在对应的组件配置如下:例如(xxx.stories.tsx,组件展示区域都距离 1em 边距)

export default {

title: 'components/Button',

component: Button,

decorators: [(storyFn) =>

{storyFn()}
],

};

详细配置,参考相关的官网说明文档。

5 parameters

parameters 通常是用于控制 Storybook 功能和插件的行为。详细配置,参考相关的官网说明文档。

简单给个 Story parameters 例子:

export default {

title: 'components/Button',

component: Button,

decorators: [(storyFn) =>

{storyFn()}
],

parameters: {

docs: {

source: {

code: 'Some custom string here',

state: true,

}

}

}

};

6 注释

storybook 解析的组件,只要注释符合 JSDoc 标准,通过 docs 插件,目前安装的版本,应该已经集成了,组件就会被自动解析。

7 实例

说明:这只是个例子,样式文件本人只是测试相关的 less 引用是否有问题,官网 demo 给的示例,组件样式是使用 css,使用 less 或者 scss 需要额外的配置,上面有说明。

src/components/Button/Button.tsx

/*

* Author: lin.zehong

* Date: 2021-04-30 10:38:00

* Desc: Button 组件

*/

import React from 'react';

import classnames from 'classnames';

import './Button.less';

export type ButtonType = 'default' | 'primary' | 'danger';

export type ButtonSize = 'lg' | 'sm';

interface IButtonProps {

/**

* 按钮类型

*/

btnType?: ButtonType;

/**

* 按钮大小

*/

size?: ButtonSize;

/**

* 按钮自定义 className

*/

className?: string;

/**

* 超链接按钮

*/

link?: string;

/**

* 按钮是否不可以操作

*/

disabled?: boolean;

/**

* 按钮内容

*/

children?: React.ReactNode;

/**

* Optional click handler

*/

onClick?: () => void;

}

// & 联合属性,并关系; | 或者关系

type NativeButtonProps = IButtonProps & React.ButtonHTMLAttributes;

type AnchorButtonProps = IButtonProps & React.AnchorHTMLAttributes;

// Partial,把属性都设置为可选

export type ButtonProps = Partial;

/**

* 我的 Button 组件

*/

const Button: React.FC = (props) => {

const { btnType, size, className, link, disabled, children, ...restProps } = props;

const classes = classnames('btn', className, {

[`btn-${btnType}`]: btnType,

[`btn-${size}`]: size,

[`btn-link`]: link,

disabled: disabled && link,

});

if (link) {

return (

{children}

);

}

return (

);

};

Button.defaultProps = {

disabled: false,

btnType: 'default',

children: '按钮'

};

export default Button;

src/components/Button/Button.less

@import '../../mixin.less';

@import '../../vartest.less';

.btn{

.button-size(@btn-padding-y, @btn-padding-x, @btn-font-size, @btn-border-radius);

position: relative;;

display: inline-block;

cursor: pointer;

text-align: center;

vertical-align: middle;

white-space: nowrap;

outline: none;

font-weight: @btn-font-weight;

font-family: @btn-font-family;

line-height: @btn-line-height;

border: @btn-border-width solid @border-color;

background-image: none;

background: transparent;

box-shadow: @btn-box-shadow;

transition: @btn-transition;

&.disabled,

&[disabled] {

pointer-events: none;

box-shadow: none;

opacity: @btn-disabled-opacity;

cursor: not-allowed;

}

}

.btn-lg {

.button-size(@btn-padding-y-lg, @btn-padding-x-lg, @btn-font-size-lg, @btn-border-radius-lg);

}

.btn-sm {

.button-size(@btn-padding-y-sm, @btn-padding-x-sm, @btn-font-size-sm, @btn-border-radius-sm);

}

.btn-default {

.button-style(@body-color, transparent, @border-color, @primary, transparent, @primary);

}

.btn-primary {

.button-style(@white, @primary, @primary);

}

.btn-danger {

.button-style(@white, @danger, @danger);

}

.btn-link{

border: none;

box-shadow: none;

color: @btn-link-color;

text-decoration: @link-decoration;

padding: 0;

&:hover,

&.hover,

&:focus,

&.focus{

color: @btn-link-hover-color;

border: none;

}

&.disabled{

color: @btn-link-disabled-color;

&:hover{

text-decoration: none;

}

}

}

mixin.less

// 按钮

.button-size(@padding-y, @padding-x, @font-size, @border-raduis) {

padding: @padding-y @padding-x;

font-size: @font-size;

border-radius: @border-raduis;

}

.button-style(

@color,

@background,

@border,

@hover-color: lighten(@color, 10%),

@hover-background: lighten(@background, 10%),

@hover-border: lighten(@border, 10%),

) {

color: @color;

background: @background;

border: @border-width solid @border;

&:hover,

&.hover {

color: @hover-color;

background: @hover-background;

border: @border-width solid @hover-border;

}

// &:focus,

// &.focus{

// color: @hover-color;

// background: @hover-background;

// border: @border-width solid @hover-border;

// }

&:active,

&.active {

color: @color;

background: @background;

border: @border-width solid @border;

}

}

// 按钮 end

// 动画

.animation-zoom(

@direction: 'top',

@scaleStart: scaleY(0),

@scaleEnd: scaleY(1),

@ransform-origin: center top,

) {

.zoom-in-@{direction}-enter {

opacity: 0;

transform: @scaleStart;

}

.zoom-in-@{direction}-enter-active {

opacity: 1;

transform: @scaleEnd;

transition: opacity 500ms cubic-bezier(0.23, 1, 0.32, 1),

transform 500ms cubic-bezier(0.23, 1, 0.32, 1);

transform-origin: @ransform-origin;

}

.zoom-in-@{direction}-exit {

opacity: 1;

transform: @scaleEnd;

}

.zoom-in-@{direction}-exit-active {

opacity: 0;

transform: @scaleStart;

transition: opacity 500ms cubic-bezier(0.23, 1, 0.32, 1) 100ms,

transform 500ms cubic-bezier(0.23, 1, 0.32, 1) 100ms;

transform-origin: @ransform-origin;

}

}

// 动画 end

vartest.less

// 自定义颜色

@white: #fff;

@gray-100: #f8f9fa;

@gray-200: #e9ecef;

@gray-300: #dee2e6;

@gray-400: #ced4da;

@gray-500: #adb5bd;

@gray-600: #6c757d;

@gray-700: #495057;

@gray-800: #343a40;

@gray-900: #212529;

@black: #000;

@blue: #0d6efd;

@indigo: #6610f2;

@purple: #6f42c1;

@pink: #d63384;

@red: #dc3545;

@orange: #fd7e14;

@yellow: #fadb14;

@green: #52c41a;

@teal: #20c997;

@cyan: #17a2b8;

@primary: @blue;

@secondary: @gray-600;

@success: @green;

@info: @cyan;

@warning: @yellow;

@danger: @red;

@light: @gray-100;

@dark: @gray-800;

// @theme-colors: @primary; @secondary; @success; @info; @warning; @danger; @light; @dark;

// 字体

@font-family-sans-serif:

'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';

@font-family-monospace:

'SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';

@font-family-base: @font-family-sans-serif;

// 字体大小

@font-size-base: 1rem; // Assumes the browse;

@font-size-lg: @font-size-base * 1.25;

@font-size-sm: @font-size-base * .875;

@font-size-root: null;

// // 字重

@font-weight-lighter: lighter;

@font-weight-light: 300;

@font-weight-normal: 400;

@font-weight-bold: 700;

@font-weight-bolder: bolder;

@font-weight-base: @font-weight-normal;

// // 行高

@line-height-base: 1.5;

@line-height-lg: 2;

@line-height-sm: 1.25;

// // 标题大小

@h1-font-size: @font-size-base * 2.5;

@h2-font-size: @font-size-base * 2;

@h3-font-size: @font-size-base * 1.75;

@h4-font-size: @font-size-base * 1.5;

@h5-font-size: @font-size-base * 1.25;

@h6-font-size: @font-size-base;

// // 链接

@link-color: @primary;

@link-decoration: none;

@link-hover-color: lighten(@link-color; 15%);

@link-hover-decoration: underline;

// body

@body-bg: @white;

@body-color: @gray-900;

@body-text-align: null;

// Spacing

@spacer: 1rem;

// Paragraphs

@paragraph-margin-bottom: 1rem;

// 字体其他部分 heading list hr 等等

@headings-margin-bottom: @spacer / 2;

@headings-font-family: null;

@headings-font-style: null;

@headings-font-weight: 500;

@headings-line-height: 1.2;

@headings-color: null;

@display1-size: 6rem;

@display2-size: 5.5rem;

@display3-size: 4.5rem;

@display4-size: 3.5rem;

@display1-weight: 300;

@display2-weight: 300;

@display3-weight: 300;

@display4-weight: 300;

@display-line-height: @headings-line-height;

@lead-font-size: @font-size-base * 1.25;

@lead-font-weight: 300;

@small-font-size: .875em;

@sub-sup-font-size: .75em;

@text-muted: @gray-600;

@initialism-font-size: @small-font-size;

@blockquote-small-color: @gray-600;

@blockquote-small-font-size: @small-font-size;

@blockquote-font-size: @font-size-base * 1.25;

@hr-color: inherit;

@hr-height: 1px;

@hr-opacity: .25;

@legend-margin-bottom: .5rem;

@legend-font-size: 1.5rem;

@legend-font-weight: null;

@mark-padding: .2em;

@dt-font-weight: @font-weight-bold;

@nested-kbd-font-weight: @font-weight-bold;

@list-inline-padding: .5rem;

@mark-bg: #fcf8e3;

@hr-margin-y: @spacer;

// Code

@code-font-size: @small-font-size;

@code-color: @pink;

@pre-color: null;

// options 可配置选项

@enable-pointer-cursor-for-buttons: true;

// 边框 和 border radius

@border-width: 1px;

@border-color: @gray-300;

@border-radius: .25rem;

@border-radius-lg: .3rem;

@border-radius-sm: .2rem;

// 不同类型的 box shadow

@box-shadow-sm: 0 .125rem .25rem rgba(@black; .075);

@box-shadow: 0 .5rem 1rem rgba(@black; .15);

@box-shadow-lg: 0 1rem 3rem rgba(@black; .175);

@box-shadow-inset: inset 0 1px 2px rgba(@black; .075);

// 按钮

// 按钮基本属性

@btn-font-weight: 400;

@btn-padding-y: .375rem;

@btn-padding-x: .75rem;

@btn-font-family: @font-family-base;

@btn-font-size: @font-size-base;

@btn-line-height: @line-height-base;

//不同大小按钮的 padding 和 font size

@btn-padding-y-sm: .25rem;

@btn-padding-x-sm: .5rem;

@btn-font-size-sm: @font-size-sm;

@btn-padding-y-lg: .5rem;

@btn-padding-x-lg: 1rem;

@btn-font-size-lg: @font-size-lg;

// 按钮边框

@btn-border-width: @border-width;

// 按钮其他

@btn-box-shadow: inset 0 1px 0 rgba(@white; .15) 0 1px 1px rgba(@black; .075);

@btn-disabled-opacity: .65;

// 链接按钮

@btn-link-color: @link-color;

@btn-link-hover-color: @link-hover-color;

@btn-link-disabled-color: @gray-600;

// 按钮 radius

@btn-border-radius: @border-radius;

@btn-border-radius-lg: @border-radius-lg;

@btn-border-radius-sm: @border-radius-sm;

@btn-transition:

color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;

src/components/Button/Button.stories.tsx

import React from 'react';

import { Story } from '@storybook/react';

import Button, { ButtonProps } from './Button';

import { action } from '@storybook/addon-actions'

//👇 This default export determines where your story goes in the story list

export default {

title: 'components/Button',

component: Button,

decorators: [(storyFn) =>

{storyFn()}
],

// parameters: {docs: { previewSource: 'open' } }

parameters: {

docs: {

source: {

// code: 'Some custom string here',

state: true,

}

}

}

};

//👇 We create a “template” of how args map to rendering

const Template: Story = (args) =>