成品展示
可视化UI文档页面:
可视化UI文档地址:
https://guozia007.gitee.io/storybook-ui/?path=/docs/mdx-button--default-story
组件库地址:
https://www.npmjs.com/package/storybook-ui-public
项目地址:
https://gitee.com/guozia007/storybook-ui
开发准备
在gitee或者github创建仓库,然后clone到本地。
这次使用了storybook,很多开发环境下的依赖都不需要装了。
根目录创建.gitignore
,屏蔽这些,不上传,基本操作了
node_modules
lib
dist
初始化生成pkg
文件:
npm init -y
安装react:
npm i react react-dom -D
安装webpack5:
// 这次就不需要安装webpack-dev-server了,因为storybook已经准备好了开发环境的服务
npm i webpack webpack-cli -D
babel那一套,看哪个没有就装哪个,有些是已经通过storybook装好了:
npm i babel-loader @babel/core @babel/preset-env @babel/preset-react -D
根目录下创建.babelrc.js
,配置babel:
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-react'
]
}
根目录下创建jsconfig.json
:
{
"compilerOptions": {
"outDir": "./lib/",
"module": "ESNext",
"target": "ES5",
"moduleResolution": "node",
"esModuleInterop": true,
"jsx": "react",
"allowJs": true,
"allowSyntheticDefaultImports":true
},
"exclude": ["node_modules", "lib"]
}
把stories
目录下的文件全部清空,然后创建Button.stories.mdx
,
用于后面写可视化UI文档。
在根目录下创建src
目录,我们的组件要写这里面。
安装less相关:
npm i less less-loader -D
安装postcss相关,用于做样式兼容配置:
npm i postcss postcss-loader postcss-preset-env -D
安装glob,用于获取入口路径:
npm i glob -D
安装css相关plugin:
npm i mini-css-extract-plugin -D
npm i css-minimizer-webpack-plugin -D
安装storybook管理webpack的工具,默认在使用的版本为4,我们安装5,
用以支持webpack5:
npm i @storybook/builder-webpack5 -D
npm i @storybook/manager-webpack5 -D
在根目录下创建webpack.config.js
,配置生产模式下的webpack:
// webpack.config.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const glob = require('glob');
const getStyleLoader = (importLoaders, loaderName) => {
return [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'postcss-preset-env'
]
}
}
},
loaderName
].filter(Boolean);
}
const entries = {};
const fileNames = glob.sync('./src/**/*.js?(x)');
// console.log('fileNames: ', fileNames);
fileNames.forEach(file => {
const filePath = file.replace(/^\.\/src\/(.+)\.jsx?$/, '$1');
entries[filePath] = file;
})
module.exports = {
mode: 'production',
entry: entries,
output: {
path: path.resolve(__dirname, 'lib'),
filename: '[name].js',
clean: true,
library: {
name: 'storybook-ui',
type: 'umd'
}
},
module: {
rules: [
{
test: /\.css$/,
use: getStyleLoader(1)
},
{
test: /\.less$/,
use: getStyleLoader(2, 'less-loader')
},
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css'
})
],
optimization: {
splitChunks: {
chunks: 'all',
name: 'chunk'
},
minimizer: [
new CssMinimizerPlugin()
]
},
resolve: {
extensions: ['.js', '.jsx', '.json']
},
externals: {
react: {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react',
},
'react-dom': {
root: 'ReactDOM',
commonjs2: 'react-dom',
commonjs: 'react-dom',
amd: 'react-dom',
}
}
}
配置.storybook/main.js
:
const path = require('path');
module.exports = {
"stories": [
"../stories/**/*.stories.mdx",
"../stories/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
],
"framework": "@storybook/react",
core: {
builder: {
name: 'webpack5',
options: {
lazyCompilation: true,
fsCache: true
}
},
},
webpackFinal: async (config, { configType }) => {
// 配置支持less
// 配置支持postcss兼容
config.module.rules.push({
test: /\.less$/,
include: path.resolve(__dirname, '../src'),
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'postcss-preset-env'
]
}
}
},
'less-loader'
]
});
return config;
}
}
配置package.json
:
{
"name": "storybook-ui-public",
"version": "0.0.2",
"description": "使用storybook发布组件",
"main": "lib/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook",
"build": "webpack --config webpack.config.js",
"pub": "npm run build && npm publish --access=public"
},
"repository": {
"type": "git",
"url": "https://gitee.com/guozia007/storybook-ui.git"
},
"homepage": "https://guozia007.gitee.io/storybook-ui/?path=/docs/mdx-button--default-story",
"keywords": [
"storybook",
"ui",
"framework",
"component",
"react component",
"frontend"
],
"author": "guozi007a",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.21.0",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@storybook/addon-actions": "^6.5.16",
"@storybook/addon-docs": "^6.5.16",
"@storybook/addon-essentials": "^6.5.16",
"@storybook/addon-interactions": "^6.5.16",
"@storybook/addon-links": "^6.5.16",
"@storybook/builder-webpack4": "^6.5.16",
"@storybook/builder-webpack5": "^6.5.16",
"@storybook/manager-webpack4": "^6.5.16",
"@storybook/manager-webpack5": "^6.5.16",
"@storybook/react": "^6.5.16",
"@storybook/testing-library": "0.0.13",
"babel-loader": "^8.3.0",
"css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^4.2.2",
"glob": "^8.1.0",
"less": "^4.1.3",
"less-loader": "^11.1.0",
"mini-css-extract-plugin": "^2.7.2",
"postcss": "^8.4.21",
"postcss-loader": "^7.0.2",
"postcss-preset-env": "^8.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"browserslist": [
">= 0.25%",
"last 1 version",
"not dead"
],
"files": [
"lib"
],
"peerDependencies": {
"react": ">= 16.9.0",
"react-dom": ">= 16.9.0"
},
"dependencies": {}
}
开发组件
// src/index.js
export { default as Button } from './Button';
// src/Button/index.jsx
import React from 'react';
import PropTypes from 'prop-types';
import './index.less';
const Button = ({ loading, primary, backgroundColor, size, label, ...props }) => {
const mode = loading
? 'storybook-button--loading'
: primary
? 'storybook-button--primary'
: 'storybook-button--default';
return (
<button
type="button"
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
style={backgroundColor && { backgroundColor }}
{...props}
>
{
loading ? <span className='storybook-loading-icon'></span> : null
}
{label}
</button>
);
};
export default Button;
Button.propTypes = {
/**
* Is this the principal call to action on the page?
*/
primary: PropTypes.bool,
/**
* Is something in loading?
*/
loading: PropTypes.bool,
/**
* What background color to use
*/
backgroundColor: PropTypes.string,
/**
* How large should the button be?
*/
size: PropTypes.oneOf(['small', 'medium', 'large']),
/**
* Button contents
*/
label: PropTypes.string.isRequired,
/**
* Optional click handler
*/
onClick: PropTypes.func,
};
Button.defaultProps = {
backgroundColor: null,
primary: false,
size: 'medium',
label: 'default button',
onClick: undefined,
loading: false
};
/* src/Button/index.less */
.storybook-button {
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-weight: 700;
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
display: inline-block;
line-height: 1;
user-select: none;
font-size: 14px;
}
.storybook-button--primary {
color: white;
background-color: #1ea7fd;
&:hover {
filter: opacity(.9);
}
}
.storybook-button--default {
color: #333;
background-color: transparent;
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
&:hover {
border: 1px solid #1ea7fd;
color: #1ea7fd;
box-shadow: none;
}
}
.storybook-button--loading {
background-color: #1ea7fd;
cursor: default;
color: #fff;
box-shadow: 0 2px 0 rgb(5 145 255 / 10%);
opacity: 0.65;
color: #fff;
}
@keyframes circle {
0% {}
100% {transform: rotate(360deg);}
}
.storybook-loading-icon {
display: inline-block;
width: 10px;
height: 10px;
border-top: 1px solid #fff;
border-right: 1px solid #fff;
border-radius: 50%;
margin-right: 6px;
animation: circle .7s linear infinite;
vertical-align: middle;
}
.storybook-button--small {
font-size: 12px;
padding: 10px 16px;
}
.storybook-button--medium {
font-size: 14px;
padding: 11px 20px;
}
.storybook-button--large {
font-size: 16px;
padding: 12px 24px;
}
写MDX文档
在stories/Button.stories.mdx
中:
import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs';
import { Button } from '../src';
<Meta title="MDX/Button" component={Button} />
<style>
{`
h2 {
color: rgba(0, 0, 0, 0.88);
font-weight: 500;
font-family: Avenir,-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',sans-serif;
font-size: 24px;
line-height: 32px;
}
ul, ol {
list-style: none;
padding: 0;
margin: 0;
}
ul li {
margin-left: 20px;
padding-left: 4px;
list-style-type: circle;
}
`}
</style>
# Button 按钮
按钮用于开始一个即时操作。
## 何时使用
标记了一个(或封装一组)操作命令,响应用户点击行为,触发相应的业务逻辑。
在 Storybook UI 中我们提供了两种按钮。
- 主按钮:用于主行动点,一个操作区域只能有一个主按钮。
- 默认按钮:用于没有主次之分的一组行动点。
以及两种状态属性与上面配合使用。
- 危险:删除/移动/修改权限等危险操作,一般需要二次确认。
- 加载中:用于异步操作等待反馈的时候,也可以避免多次提交。
## 代码演示
export const Template = (args) => <Button {...args} />
<Canvas>
<Story name="default"
args={{
label: "default button"
}}
>
{Template.bind({})}
</Story>
</Canvas>
<Canvas>
<Story name="primary"
args={{
primary: true,
label: "primary button"
}}
>
{Template.bind({})}
</Story>
</Canvas>
<Canvas>
<Story name="loading"
args={{
loading: true,
label: "Loading"
}}
>
{Template.bind({})}
</Story>
</Canvas>
## API
通过设置 Button 的属性来产生不同的按钮样式,按钮的属性说明如下:
<ArgsTable of={Button} />
发布文档
执行开发环境打包指令:
npm run build-storybook
会生成一个storybook-static
的目录。
代码上传到git,然后发布到githubPages
或者giteePage
。
这里发布后,获取到文档地址,在package.json
中添加:
"homepage": "你的线上文档地址"
发布组件
执行打包并发布的指令:
npm run pub
完成。