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
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) => <div style={{ margin: '1em' }}>{storyFn()}</div>],
};
详细配置,参考相关的官网说明文档。
5 parameters
parameters
通常是用于控制 Storybook 功能和插件的行为。详细配置,参考相关的官网说明文档。
简单给个 Story parameters 例子:
export default {
title: 'components/Button',
component: Button,
decorators: [(storyFn) => <div style={{ margin: '1em' }}>{storyFn()}</div>],
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<HTMLElement>;
type AnchorButtonProps = IButtonProps & React.AnchorHTMLAttributes<HTMLElement>;
// Partial,把属性都设置为可选
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>;
/**
* 我的 Button 组件
*/
const Button: React.FC<ButtonProps> = (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 (
<a href={link} className={classes} {...restProps}>
{children}
</a>
);
}
return (
<button className={classes} disabled={disabled} {...restProps}>
{children}
</button>
);
};
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) => <div style={{ margin: '1em' }}>{storyFn()}</div>],
// 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<ButtonProps> = (args) => <Button onClick={action('12222')} {...args} />;
// Template.parameters = {
// docs: { previewSource: 'open' },
// }
export const FirstStory = Template.bind({});
FirstStory.args = {
/*👇 The args you need here will depend on your component */
btnType: 'primary',
};
// export const DisabledButton = Template.bind({});
// DisabledButton.storyName = 'So simple!1';
// DisabledButton.args = {
// /*👇 The args you need here will depend on your component */
// disabled: true,
// };
8 填坑
8.1 less 不支持,需要配置
官网查询,storybook 扩展 webpack 配置:https://storybook.js.org/docs/react/configure/webpack#extending-storybooks-webpack-config
github issues: Does not support less #691
安装 less 相关的依赖
先不要急着安装,往下看,不然,启动会有相关的报错!!!
yarn add style-loader css-loader less-loader
由于上面安装的是最新的 less-loader 版本
,本人装完过后是 8.1.1
的版本,启动项目后,出现了各种错误,例如:
Cannot find module 'less'
Module build failed less-loader this.getOptions is not a function
但是,本人确定 less-loader 是安装成功,最后,发现问题是由于 less-loader
版本过高,所以,安装了较低的版本后 yarn add less-loader@7.0.0
,启动成功
- .storybook / mian.js 配置
const path = require('path');
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials"
],
webpackFinal: async (config, { configType }) => {
// `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
// You can change the configuration based on that.
// 'PRODUCTION' is used when building the static version of storybook.
// Make whatever fine-grained changes you need
config.module.rules.push({
test: /\.less$/,
loaders: ['style-loader', 'css-loader', 'less-loader'],
include: path.resolve(__dirname, '../src/')
});
// Return the altered config
return config;
},
}
扩展:less 模块化
由于项目都是使用 less 模块化
,所以,这里需要新增模块化的配置,上面的配置更改为:
const path = require('path');
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials"
],
webpackFinal: async (config, { configType }) => {
// `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
// You can change the configuration based on that.
// 'PRODUCTION' is used when building the static version of storybook.
// Make whatever fine-grained changes you need
config.module.rules.push({
test: /\.less$/,
exclude: /node_modules/,
// loaders: ['style-loader', 'css-loader', 'less-loader'],
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[local]_[hash:base64:5]'
}
}
},
{
loader: 'less-loader'
}
],
include: path.resolve(__dirname, '../src/')
});
// Return the altered config
return config;
},
}
8.2 使用 yarn 安装
本人使用 cnpm 安装完依赖后,一直启动不成功,要么就是项目启动有问题,要么就是 Storybook 启动有问题,使用 yarn
安装完成之后,问题都解决,所以,这里推荐使用 yarn
安装。
8.3 样式问题
本人使用的框架是 umi
,组件使用的主题色和变量相关的配置是在 theme.ts
配置文件,项目启动没有问题,但是,使用 Storybook 配置相关的组件,就找不到在 umi 配置文件 theme.ts
的相关变量,导致样式相关错误;
所以,变量和 mixin
等相关的样式变量,要放在单独的 less 文件,方便 Storybook 配置对应的组件引入样式。
8.4 npx sb init 一直无法安装相关的 react 依赖
根据 Storybook 官网 说明,使用如下命令 npx
进行安装
# Add Storybook:
npx sb init
命令安装下载默认的配置文件 .storybook
和示例 src/stories
,如下图:
接着检查到为 react
项目,下载 storybook react
相关依赖,一直有问题,报各种文件已存在,不受 npm 控制
等如下错误:如下图
解决方案:
通过手动安装 storybook react
相关依赖包,报错后,不使用 npx sb init
storybook cli 进行安装,storybook react
相关依赖包为:
cnpm i @storybook/react@6.2.9 -D
cnpm i @storybook/addon-links@6.2.9 -D
cnpm i @storybook/addon-essentials@6.2.9 -D
cnpm i @storybook/addon-actions@6.2.9 -D
最后,在 package.json scripts
中,添加对应的命令 "storybook": "start-storybook -p 6006", "build-storybook": "build-storybook"
,如下:
"scripts": {
"start": "koi dev",
"build": "koi build",
"publish": "koi publish",
"eslint": "eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx ./src",
"lint-staged": "lint-staged",
"test": "umi-test",
"test:coverage": "umi-test --coverage",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
添加完成后,控制台运行命令 yarn run storybook
,就可以看到成功的界面了。