1 什么是 Storybook
Storybook is an open source tool for developing UI components and pages in isolation. It simplifies building, documenting, and testing UIs.
Storybook 是一個(gè)開源工具,它能有組織和高效地構(gòu)建 UI 組件测蹲,文檔編制和測(cè)試莹捡,包括 React、Vue 和 Angular 扣甲。
特點(diǎn):
分開展示各個(gè)組件不同屬性下的狀態(tài)篮赢;
能追蹤組件的行為并且具有屬性調(diào)試的功能;
可以為組件自動(dòng)生成文檔和屬性列表琉挖;
2 安裝
根據(jù)官網(wǎng)启泣,本人使用的 react 項(xiàng)目,所以示辈,直接控制臺(tái)運(yùn)行如下命令寥茫,集成 Storybook:本人安裝當(dāng)前最新版本為 "@storybook/react": "^6.2.9",
# Add Storybook:
npx sb init
安裝成功后,直接在控制臺(tái)運(yùn)行如下命令矾麻,就可以看到啟動(dòng)頁(yè)面:
# Starts Storybook in development mode
npm run storybook
3 說明
根目錄生成的
.storybook
為 storybook 默認(rèn)配置目錄纱耻;src/stories
目錄為 storybook 頁(yè)面組件目錄;本人項(xiàng)目是 ts险耀,安裝完成 storybook 后弄喘, storybook 頁(yè)面組件默認(rèn)就是 tsx,無需再額外配置甩牺;
4 decorators
decorators
的作用主要是統(tǒng)一修飾組件展示區(qū)域的樣式蘑志,例如:設(shè)置組件展示都居中,或者是 margin贬派、padding 的距離等等急但。
在對(duì)應(yīng)的組件配置如下:例如(xxx.stories.tsx,組件展示區(qū)域都距離 1em 邊距)
export default {
title: 'components/Button',
component: Button,
decorators: [(storyFn) => <div style={{ margin: '1em' }}>{storyFn()}</div>],
};
詳細(xì)配置赠群,參考相關(guān)的官網(wǎng)說明文檔羊始。
5 parameters
parameters
通常是用于控制 Storybook 功能和插件的行為。詳細(xì)配置查描,參考相關(guān)的官網(wǎng)說明文檔突委。
簡(jiǎn)單給個(gè) 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 標(biāo)準(zhǔn),通過 docs 插件匀油,目前安裝的版本缘缚,應(yīng)該已經(jīng)集成了,組件就會(huì)被自動(dòng)解析敌蚜。
7 實(shí)例
說明:這只是個(gè)例子桥滨,樣式文件本人只是測(cè)試相關(guān)的 less
引用是否有問題,官網(wǎng) 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;
/**
* 按鈕內(nèi)容
*/
children?: React.ReactNode;
/**
* Optional click handler
*/
onClick?: () => void;
}
// & 聯(lián)合屬性喻括,并關(guān)系; | 或者關(guān)系
type NativeButtonProps = IButtonProps & React.ButtonHTMLAttributes<HTMLElement>;
type AnchorButtonProps = IButtonProps & React.AnchorHTMLAttributes<HTMLElement>;
// Partial,把屬性都設(shè)置為可選
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
// 動(dòng)畫
.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;
}
}
// 動(dòng)畫 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;
// // 標(biāo)題大小
@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 可配置選項(xiàng)
@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 不支持,需要配置
官網(wǎng)查詢贫奠,storybook 擴(kuò)展 webpack 配置:https://storybook.js.org/docs/react/configure/webpack#extending-storybooks-webpack-config
github issues: Does not support less #691
安裝 less 相關(guān)的依賴
先不要急著安裝唬血,往下看,不然唤崭,啟動(dòng)會(huì)有相關(guān)的報(bào)錯(cuò)?胶蕖!谢肾!
yarn add style-loader css-loader less-loader
由于上面安裝的是最新的 less-loader 版本
腕侄,本人裝完過后是 8.1.1
的版本,啟動(dòng)項(xiàng)目后芦疏,出現(xiàn)了各種錯(cuò)誤兜挨,例如:
Cannot find module 'less'
Module build failed less-loader this.getOptions is not a function
但是,本人確定 less-loader 是安裝成功眯分,最后,發(fā)現(xiàn)問題是由于 less-loader
版本過高柒桑,所以弊决,安裝了較低的版本后 yarn add less-loader@7.0.0
,啟動(dòng)成功
- .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;
},
}
擴(kuò)展:less 模塊化
由于項(xiàng)目都是使用 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 安裝完依賴后界逛,一直啟動(dòng)不成功昆稿,要么就是項(xiàng)目啟動(dòng)有問題,要么就是 Storybook 啟動(dòng)有問題息拜,使用 yarn
安裝完成之后溉潭,問題都解決净响,所以,這里推薦使用 yarn
安裝喳瓣。
8.3 樣式問題
本人使用的框架是 umi
馋贤,組件使用的主題色和變量相關(guān)的配置是在 theme.ts
配置文件,項(xiàng)目啟動(dòng)沒有問題畏陕,但是配乓,使用 Storybook 配置相關(guān)的組件,就找不到在 umi 配置文件 theme.ts
的相關(guān)變量惠毁,導(dǎo)致樣式相關(guān)錯(cuò)誤犹芹;
所以,變量和 mixin
等相關(guān)的樣式變量鞠绰,要放在單獨(dú)的 less 文件腰埂,方便 Storybook 配置對(duì)應(yīng)的組件引入樣式。
8.4 npx sb init 一直無法安裝相關(guān)的 react 依賴
根據(jù) Storybook 官網(wǎng) 說明洞豁,使用如下命令 npx
進(jìn)行安裝
# Add Storybook:
npx sb init
命令安裝下載默認(rèn)的配置文件 .storybook
和示例 src/stories
盐固,如下圖:
接著檢查到為 react
項(xiàng)目,下載 storybook react
相關(guān)依賴丈挟,一直有問題刁卜,報(bào)各種文件已存在,不受 npm 控制
等如下錯(cuò)誤:如下圖
解決方案:
通過手動(dòng)安裝 storybook react
相關(guān)依賴包曙咽,報(bào)錯(cuò)后蛔趴,不使用 npx sb init
storybook cli 進(jìn)行安裝,storybook react
相關(guān)依賴包為:
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
中孝情,添加對(duì)應(yīng)的命令 "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"
},
添加完成后洒嗤,控制臺(tái)運(yùn)行命令 yarn run storybook
箫荡,就可以看到成功的界面了。