从0到1搭建react组件库-样式方案(二)- 以Button为例

357 阅读2分钟

前言

书接上回,上次定义了组件库整体的色值和less变量, 本次聚焦于实现组件的样式与变体,首先就需要根据组件的类型、状态等属性,去计算得出组件的类名,然后在less中通过这些变体,去计算出对应类名下组件的样式。两者相结合最终形成组件的样式。

Utils(工具函数)

在组件库的设计中,引入工具模块,为全局的组件提供通用的方法。在这里以独立的文件夹的形式存在。

packages/ui/src目录下新建一个utils文件夹。

在utils下创建一个统一的出口文件index.ts,通过这种方式为工具函数的引用提供一个统一的出口,也减少在其它组件位置多行的文件导入代码。

warning

utils下新建一个warging.ts,warning主要是实现对报错的封装,统一声明报错的信息, 例如前缀为 mini-ui/ui , 标识错误发生库。

export function warning(
    condition: boolean,
    message: string,
    ...options: unknown[]
) {
  if(!console || !condition)
    return

  console.error(`
    [@mini-ui/ui]: ${message}
  `, ...options)
}

classNames

实现classNames组件之前,先观察classNames原函数。

classNames函数,接收不固定数量的参数, 并且对 string number array object null undefined的类型,进行针对性的处理。下面是函数完成的主要内容。

  • 不固定数量的参数可以通过es6中的rest参数,用来获取函数的多余参数。
  • 参数类型为string或者number, 作为class的一部分进行拼接。
  • 参数类型为array, 就需要递归处理数组中的每一项。
  • 参数类型为对象时,首先需要获取到对象的key值,如果key值映射的value值为正,则将key值作为class的一部分进行拼接。如果key映射的value值为负,则不加入class。
  • 如果参数非真值, 直接跳过。
import {isArray, isNumber, isObject, isString} from "./is";
import {warning} from "./warning";

type ClassNamesParams = string | number | Record<string, unknown> | Array<ClassNamesParams> | null | undefined

export function classNames (...params: Array<ClassNamesParams>) {
  const result: Array<string | number> = []
  for (const cls of params) {
    if(!cls) {
      // 非真值,直接跳过处理
      continue
    }
    if(isString(cls) || isNumber(cls)) {
      // string 或者number直接使用
      result.push(cls)
    }else if(isArray(cls)) {
      // 递归处理数组中的每一项
      result.push(classNames(...cls))
    } else if (isObject(cls)) {
      // 遍历object中的每一项,判断value的值,来决定是否跳过key值
      for (const key in cls) {
        if(cls[key]){
          result.push(key)
        }
      }
    } else {
      warning(true, 'classname must be string | array | object')
    }
  }

  // 利用Set去重
  return [...new Set(result)].join(' ')
}

getPrefix

传入组件的名称,通过getPrefix函数,生成统一的前缀+组件名的内容。

export function getPrefix(componentName: string, prefix?: string){
  prefix = prefix || "mini"

  return  `${prefix}-${componentName}`
}

packages/ui/src/utils/index中增加导出语句。

export * from "./warning"
export * from "./getPrefix"
export * from "./classNames"
export * from "./is"

utils工具集结构

image.png

Button组件

完成了准备工作之后,来逐步的实现Button组件的核心功能以及主要特性, 以下代码为示例代码,大部分的重复代码就省去,主要是跑通整体的流程。

类型与状态

  • 状态决定了按钮的主色调,例如:默认状态、成功状态、警告状态、危险状态。
  • 类型则进一步的细化按钮的视觉呈现效果,主要分为三种视觉效果:
    1. 实心按钮
    2. 描边按钮
    3. 文本按钮
  • 通过这种方式,在不同的状态下,为各种类型的按钮呈现出主色的不同色阶,从而实现丰富多样且一致性强的按钮样式。

实现步骤

  1. 定义状态和类型的类型枚举,作为组件的props。
    • Button文件夹下的types.ts, 声明Button按钮的props类型。
    import {CSSProperties, ReactNode} from "react";
    export interface ButtonProps {
      /**
       * @desc 组件的几种变体形式
       * */  
      type?: "primary" | "default" | "secondary" | "dashed" | "outline" | "text";
      /**
       * @zh
       * 按钮的状态
       * */
      status?: "success" | "warning" | "danger" | "default";
      className?: string;
      style?: CSSProperties;
      children?: ReactNode;
    }
    
  2. 根据状态和类型计算样式的逻辑.
    • 获取到button组件的前缀名称,保证统一命名规范,避免样式冲突。
    • 将状态、类型相关枚举通过模版字符串的方式拼接放入classNames函数中。
    import {ButtonProps} from "./interface";
    import {
      getPrefix,
      classNames as cls
    } from "../../utils/";
    
    export const Button = ({
        status = 'default',
        type = 'default',
        className,
        children,
        ...rest
    }: ButtonProps) => {
      const prefix = getPrefix("btn")
      const classNames = cls(
          prefix,
          `${prefix}-${type}`,
          `${prefix}-status-${status}`,
          `${prefix}-size-${size}`,
          className
      )
    
      return <button
          className={classNames}
          {...rest}
      >
        {children}
      </button>
    }
    
  3. 将样式应用到按钮组件,明确按钮组件样式的覆盖关系
    • 设计less变量,在变量中包含状态、类型的枚举值,方便下面通过混入的方式快速实现。
    • 通过less中的mixins混入的方式,将按钮组件的状态和类型作为变量使用,提高样式的复用性。
    // 示例代码
    @import url("./token.less");
    
    @btn-prefix: ~'@{prefix}-btn';
    
    .btn-type(@type) {
      .@{btn-prefix}-@{type}:not(.@{btn-prefix}-disabled) {
        background-color: ~'@{btn-@{type}-color-bg}';
        color: ~'@{btn-@{type}-color-text}';
        border: @border-1 @border-solid ~'@{btn-@{type}-color-border}';
        &:hover {
          color: ~'@{btn-@{type}-color-text}';
          background-color: ~'@{btn-@{type}-color-bg_hover}';
        }
        &:active {
          color: ~'@{btn-@{type}-color-text_active}';
          background-color: ~'@{btn-@{type}-color-bg_active}';
        }
      }
    }
    
    .btn-size(@size) {
      .@{btn-prefix}-size-@{size} {
        height: ~'@{btn-size-@{size}-height}';
      }
    }
    
    .btn-type(primary);
    .btn-type(secondary);
    .btn-type(outline);
    .btn-type(dashed);
    .btn-type(text);
    
    .btn-size(mini);
    .btn-size(small);
    .btn-size(default);
    .btn-size(large);
    
    
    .@{btn-prefix} {
      display: flex;
      justify-content: center;
      align-items: center;
      border-radius: @btn-border-radius;
    
      box-sizing: border-box;
      padding: 0 @btn-padding ;
    }
    
  4. 在DEMO中增加展示的不同按钮的类型和状态的tsx文件。
  5. 在website中的button文档中,引入这个文件,最终展示具体的效果。
# 按钮组件

## 组件的主要变体
<code src="../../../../demo/button/basic-button.tsx" />

## 组件的不同状态
<code src="../../../../demo/button/status-button.tsx" />
  1. 最终效果

image.png

仓库地址

未完待续

下面的一章中,会补全组件的主要功能点, 例如: 指定Button的元素类型, loading状态等

往期文章

从0到1搭建react组件库-项目规范篇

从0到1搭建react组件库-开发方案篇

从0到1搭建react组件库-文档篇

从0到1搭建react组件库-样式方案(一)