从零到一搭建React组件库

SegmentFault

共 10030字,需浏览 21分钟

 · 2021-04-24

作者:ToSmile
来源:Segmentfault 思否


最近一直在捣鼓如何搭建React组件库,至于为什么会产生这个想法,主要是因为组件库对于前端生态来说究极重要,每一个着眼于长远发展、看重开发效率的的互联网公司基本上都会量身定制自己的组件库,它的好处不用多说。对于前端工程师而言,去理解以及掌握它,可以让我们在今后的工作中以及应聘过程中多出一项特殊技能,并且对自身的纵向发展也就是很有利的。下面是我记录我在搭建组件库的过程。

初始化工程


搭建工程不打算采用 create-react-app 脚手架来搭建,因为脚手架封装好了很多东西,而有些东西对于组件库并不适用,用来搭建组件库过于臃肿,因此我不打算借助任何脚手架来搭建工程。

首先,先创建一个工程文件夹 pony-react-ui,在该文件夹下执行如下命令:

npm init // 生成package.json
tsc --init // 生成tsconfig.json

然后,按照如下目录结构初始化工程:

pony-react-ui
├── src
    ├── assets
    ├── components
        ├── Button
            ├── Button.tsx
            └── index.ts
        └── Dialog
            ├── Dialog.tsx
            └── index.ts
    ├── styles
        ├── _button.scss
        ├── _dialog.scss
        ├── _mixins.scss
        ├── _variables.scss
        └── pony.scss
    └── index.ts // 打包的入口文件,引入pony.scss,抛出每一个组件
├── index.js // 入口文件,package.json中main字段指定的文件
├── package.json
├── tsconfig.json
├── webpack.config.js
└── README.md

编写一个Button组件

Button组件应该满足一下需求:

  • 不同尺寸
  • 不同类型
  • 不同颜色
  • 禁用状态
  • 点击事件
Button.tsx
import React from 'react';
import classNames from 'classnames';

export interface IButtonProps {
  onClick?: React.MouseEventHandler;
  // 类型
  primary?: boolean;
  secondary?: boolean;
  outline?: boolean;
  dashed?: boolean;
  link?: boolean;
  text?: boolean;
  // 尺寸
  xLarge?: boolean;
  large?: boolean;
  small?: boolean;
  xSmall?: boolean;
  xxSmall?: boolean;
  // 颜色
  success?: boolean;
  warn?: boolean;
  danger?: boolean;
  // 禁用状态
  disabled?: boolean;
  className?: string;
  style?: React.CSSProperties;
  children?: React.ReactNode;
}

export const Button = (props: IButtonProps) => {
  const {
    className: tempClassName,
    style,
    onClick,
    children,
    primary,
    secondary,
    outline,
    dashed,
    link,
    text,
    xLarge,
    large,
    small,
    xSmall,
    xxSmall,
    success,
    danger,
    warn,
    disabled,
  } = props;
  
  
  const className = classNames(
    {
      'pony-button'true,
      'pony-button-primary': primary,
      'pony-button-secondary': secondary && !text,
      'pony-button-outline': outline,
      'pony-button-dashed': dashed,
      'pony-button-link': link,
      'pony-button-text': text && !secondary,
      'pony-button-text-secondary': secondary && text,
      'pony-button-round': round,
      'pony-button-rectangle': noRadius,
      'pony-button-fat': fat,
      'pony-button-xl': xLarge,
      'pony-button-lg': large,
      'pony-button-sm': small,
      'pony-button-xs': xSmall,
      'pony-button-xxs': xxSmall,
      'pony-button-long': long,
      'pony-button-short': short,
      'pony-button-success': success,
      'pony-button-warn': warn,
      'pony-button-danger': danger,
      'pony-button-disabled': disabled,
    },
    tempClassName
  );
  
  return (
    <button 
      type="button"
      className={className}
      style={style}
      onClick={onClick}
      disabled={disabled}>
      <span className="pony-button__content">{children}</span>
    </button>
  );
}
在Button/index.ts文件中抛出Button组件以及定义的类型
export * from './Button';
这样,一个示例组件就基本完成了,有同学肯定会有这么一个疑问,为什么在Button.tsx中没有引入它的样式文件_button.scss,而是在使用时引入全局样式或者单独引入_button.scss呢?
// 单独引入组件样式
import { Button } from 'pony-react-ui';
import 'pony-react-ui/lib/styles/button.scss';

// 全局引入组件样式,打包时抽离出来的样式
import 'pony-react-ui/lib/styles/index.scss';
因为这跟样式的权重有关,通过import引入的样式权重会低于JSX中className定义的样式,因此才可以在组件外部修改内部的样式。
举个实例:
import { Button } from 'pony-react-ui';

import 'pony-react-ui/lib/styles/button.scss';
import styles from './index.module.scss';

const Demo = () => (
  <div className={styles.btnBox}>
    <Button onClick={submit}>submit</Button>
  </div>
)
引入组件库中的Button.scss和本地的index.module.scss在打包后会以<style></style>标签注入到页面中,而且顺序是:
<style type="text/css">
  // Button.scss的样式
</style>

<style type="text/css">
  // index.module.scss的样式
</style>
因此,index.module.scss中的样式权重是高于Button.scss中的样式,可以在index.module.scss中修改Button.scss的样式

编写样式

├── styles
    ├── _button.scss
    ├── _dialog.scss
    ├── _mixins.scss
    ├── _variables.scss
    └── pony.scss
我在style文件下存放所有的样式文件,与_button.scss、_dialog.scss类型的样式文件属于组件的样式文件,_mixins.scss用于存放mixin指令,提高样式逻辑复用
// _mixins.scss

@mixin colors($text$border$background) {
  color: $text;
  background-color: $background;
  border-color: $border;
}

// 设置按钮大小
@mixin button-size($padding-x, $height$font-size) {
  height: $height;
  padding: 0 $padding-x;
  font-size: $font-size;
  line-height: ($height - 2);
}
比如,在_button.scss中使用
$values#ff0000, #00ff00, #0000ff;
.primary {
  @include colors($values...);
}
node-sass会将其编译成
.primary {
  color: #ff0000;
  background-color: #00ff00;
  border-color: #0000ff;
}
_variables.scss用于存放一些样式常量,比如定义不同尺寸按钮的字体大小:
$button-font-size: 14px !default;
$button-xl-font-size: 16px !default;
$button-lg-font-size: 16px !default;
$button-sm-font-size: 12px !default;
pony.scss会引入所有的样式文件,_mixins.scss、_variables.scss这类工具类样式文件需要置前引入,因为后面的组件样式文件可能依赖它们
@import 'variables';
@import 'mixins';
@import 'button';
@import 'dialog';
...
在对样式文件构建处理时,我没有使用css modules去避免样式重名,而是使用BEM规范书写样式规避这一问题。为什么我要这么做呢?
rules: [
  {
    test: /\.(sa|sc|c)ss$/,
    use: [
      loader: 'css-loader',
      options: {
        modules: false // 禁止css modules
      }
    ]
  }
]
因为使用css modules导致无法从组件外部修改组件内部样式了,从外部修改组件样式一般会这样写:
<Button className="btn">按钮</Button>

// 修改Button内部样式,假如组件内部样式有个样式类名为pony-button-promary
.btn {
    :global {
        .pony-button-promary {
            color: #da2227;
        }
    }
}
但是,采用了css modules后,pony-button-promary类型后面会多出一串hash值,而且在每次修改Button组件后,生成的hash都会不同,这将导致在深度遍历查找过程中找不到类名
.btn {
    :global {
        // 下次修改Button组件构建后,生成的hash不一定为sadf6756 
        .pony-button-promary-sadf6756 {
            color: #da2227;
        }
    }
}

打包输出UMD规范

打包输出es module规范

docz生成组件使用文档

发布到npm仓库



点击左下角阅读原文,到 SegmentFault 思否社区 和文章作者展开更多互动和交流,扫描下方”二维码“或在“公众号后台回复“ 入群 ”即可加入我们的技术交流群,收获更多的技术文章~

- END -


浏览 4
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报