目录

StoryBook + Lerna 开发自己的UI组件库(一)

1.前言

我们在日常工作中会接触到公共组件的开发,那公司内部肯定会创建自己的私有组件库,有的完全自研,从项目的脚手架搭建到最后的成品由专业团队去做,如果为了快捷或者方便,也可以选择现有的方案去实现,比如dumi:基于 Umi 造、为组件开发场景而生的文档工具,用大实话讲,dumi 就是可以用来写文档、官网和组件库 Demo 的 Umi;又或者StoryBook :一个快捷构建 UI 组件或者页面的开源工具。本文以 StoryBook 为例,从 0 开始搭建一个 UI 组件私有库

2.项目的初始化

2.1.基本初始化

可以按照storybook 官网推荐的方式

1
npx sb init

它会让你选择项目类型,我们这里选择 react 就行

注意
初始化项目后,npm install 依赖之后,然后 run start-storybook -p 6006,这个时候可能会报 Cannot find module 'react/package.json'或者 Cannot find module 'react-dom/package.json'的错,导致项目运行不起来,解决的办法也就是手动安装npm install react react-dom --save -dev依赖。这个问题似乎普遍存在 storybook 6.x 的版本中,也可以看下Issues

安装好依赖,npm run storybook,项目就能运行起来,默认会初始化一些组件

https://i.loli.net/2021/08/28/HbDFx7fCgVKsYXo.png

2.2.目录介绍

这样初始化下来的项目目录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
├─package.json
├─yarn.lock
├─stories
|    ├─Button.jsx
|    ├─Button.stories.jsx
|    ├─Header.jsx
|    ├─Header.stories.jsx
|    ├─Introduction.stories.mdx
|    ├─Page.jsx
|    ├─Page.stories.jsx
|    ├─button.css
|    ├─header.css
|    ├─page.css
|    ├─assets
|    |   ├─code-brackets.svg
|    |   ├─colors.svg
|    |   ├─comments.svg
|    |   ├─direction.svg
|    |   ├─flow.svg
|    |   ├─plugin.svg
|    |   ├─repo.svg
|    |   └stackalt.svg
├─.storybook
|     ├─main.js
|     └preview.js

首先是.storybook,这是包含的是storybook的配置文件,可以指定整个项目的入口,引入的样式,引入的插件,webpack的配置等等;然后是stories,这里面就是UI组件具体的开发地方,可以看到有.stories.jsx、.stories.mdx、.jsx三种类型的文件。

可以这么理解:.jsx即是你的组件源码,.stories.jsx是每个组件的使用示例,可以展示在页面上,而.stories.mdx则是类似于文档的东西。

3.storybook的配置

3.1.支持typescript

打开.storybook/main.js,在这里添加webpack的配置,我们添加如下代码,代表添加ts文件的解析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
webpackFinal: async (config) => {
  config.module.rules.push({
    test: /\.(ts|tsx)$/,
    use: [
      {
        loader: require.resolve('babel-loader'),
        options: {
          presets: [require.resolve('babel-preset-react-app')],
        },
      },
    ],
  });
  return config;
},

3.2.支持less文件

同样的,在.storybook/main.js中添加对less的解析,最后该文件的代码为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
module.exports = {
  "stories": [
    "../stories/**/*.stories.mdx",
    "../stories/**/*.stories.@(js|jsx|ts|tsx)"
  ],
  webpackFinal: async (config) => {
    config.module.rules.push({
      test: /\.(ts|tsx)$/,
      use: [
        {
          loader: require.resolve('babel-loader'),
          options: {
            presets: [require.resolve('babel-preset-react-app')],
          },
        },
      ],
    });
    config.module.rules.push({
      test: /\.less$/,
      loaders: [
        'style-loader',
        'css-loader',
        {
          loader: 'less-loader',
          options: {
            lessOptions: {
              javascriptEnabled: true,
            },
          },
        },
      ],
    });
    return config;
  },
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-essentials"
  ]
}

3.3.安装基础的UI库

因为本次是基于antd、antd-mobile开发,所以在依赖中记得添加antd以及antd-mobile

4.组件开发

现在把stories目录下自动创建的组件删除,自己新建一个index.tsx、index.less、index.stories.mdx文件,其中index.stories.mdx则为展示的内容,

4.1.组件的开发

打开index.tsx,我们简单定义一个Button组件,依赖于antd

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react'
import { Button } from 'antd'
import PropTypes from 'prop-types'

import './index.less'

export interface PrimaryButtonProps {
  text: string
}

export const PrimaryButton: React.FC<PrimaryButtonProps> = (props) => {
  const { text } = props

  const handleClick = () => {
  }

  return <Button type="primary" onClick={handleClick} > {text} </ Button>
}

PrimaryButton.propTypes = {
  text: PropTypes.string
}
注意
这样写出来的组件是不包含antd的样式的,所以到时候展示的页面会是元素的默认样式,所以我们要将antd的样式文件引入项目中

将找到的样式文件放入stories目录下,然后打开.storybook/preview.js文件,讲刚才的样式文件引入到全局

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// .storybook/preview.js

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
}


import '../stories/global-style/antd/index.less'
import '../stories/global-style/antd-mobile/index.less'

4.2.组件的文档

打开index.stories.mdx,描述该组件的基本使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { Meta, Story, Preview, Props } from "@storybook/addon-docs/blocks"
import { PrimaryButton } from './index.tsx';

<Meta title="Components/PrimaryButton" component={PrimaryButton} />

# PrimaryButton

## 基本使用

<Preview>
  <Story name="基本使用">
    <PrimaryButton text="我的第一个按钮组2"/>
  </Story>
</Preview>

注意,这里使用的是storybook/addon-docs提供的组件,可以在官网找到描述

4.3.组件的展示

这样编辑好了以后,storybook首先会从.storybook/main.js中找到入口文件:

1
2
3
4
"stories": [
  "../stories/**/*.stories.mdx",
  "../stories/**/*.stories.@(js|jsx|ts|tsx)"
],

然后加载index.stories.mdx展示内容,包含其中的组件代码

5.eslint的引入

作为公共的组件库的项目,还是有必要引入eslint去做代码规范的

5.1.需要安装的依赖

首先需要添加以下依赖,这是使用typescript环境下,使用eslint必须的,其中还用了各种plugin是为了兼容各种语法,当然此项目比较简单,可以删除这些plugin

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
"@typescript-eslint/eslint-plugin": "^4.22.1",
"@typescript-eslint/parser": "^4.22.1",
"eslint": "^7.32.0",
"eslint-config-standard": "^16.0.2",
"eslint-config-standard-jsx": "^10.0.0",
"eslint-import-resolver-webpack": "^0.13.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-json": "^3.0.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.2.0",
"typescript": "^4.3.5"

5.2.eslintrc.js的配置

这里我只是简单对引号、console.log语法、分号以及max-len做了一些限制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
module.exports = {
  'env': {
    'node': true,
    'browser': true,
    'commonjs': true,
    'amd': true,
  },
  'extends': [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
  ],
  'parser': '@typescript-eslint/parser',
  'parserOptions': {
    'ecmaFeatures': {
      'jsx': true,
    },
    'ecmaVersion': 12,
    'sourceType': 'module',
  },
  'plugins': [
    'react',
    '@typescript-eslint',
  ],
  'rules': {
    'no-var': 'error',
    'no-console': 'error',
    'quotes': [
      'error',
      'single',
    ],
    'semi': [
      'error',
      'never',
    ],
    'comma-dangle': [
      'error', 
      'always-multiline',
    ],
    'max-len': ['error', 120],
  },
}

5.3.vscode环境下的开发配置

仅仅像上面这样配置,eslint只会在run lint命令的时候才会去校验,为了在开发阶段就去校验eslint并且让编辑器去自动的修复不规范的语法,在项目根目录创建一个.vscode目录,并在下面创建一个setting.json去覆盖全局设置,其内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// .vscode/settings.json
{  
  // 对那些文件进行自动校验
  "eslint.validate": [
    "javascript",
    // jsx
    "javascriptreact",
    // ts 
    "typescript",
    // tsx
   "typescriptreact",
  ],
  "editor.formatOnPaste": false,
  "editor.formatOnSave": false,
  // 保存时修复不合理的语法
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
}

这样eslint的配置基本完成

6.小结

6.1.当前的项目目录结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
├─.editorconfig
├─.eslintignore
├─.eslintrc.js
├─package-lock.json
├─package.json
├─yarn.lock
├─stories
|    ├─index.less
|    ├─index.stories.mdx
|    ├─index.tsx
|    ├─global-style
|    |      ├─index-mobile.less
|    |      ├─index.less
|    |      ├─normalize-mobile.less
|    |      ├─normalize.less
|    |      ├─antd-mobile
|    |      |      ├─anim.less
|    |      |      ├─component.less
|    |      |      ├─index.less
|    |      |      ├─mixins.less
|    |      |      └theme.less
|    |      ├─antd
|    |      |  ├─component.less
|    |      |  ├─dark.less
|    |      |  ├─index.less
|    |      |  ├─mixins.less
|    |      |  └theme.less
├─.vscode
|    └settings.json
├─.storybook
|     ├─main.js
|     └preview.js

6.2页面运行的效果

这里引入的antd样式是一个绿色的主色

https://i.loli.net/2021/08/28/tSi8RD3qEFlwcsn.png