Bootstrap

【react storybook】从零搭建react脚手架,并使用storybook发布组件库到npm,并生成可视化UI文档


成品展示

可视化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

完成。

在这里插入图片描述


;