当前位置: 首页 > news >正文

React--》规划React组件库编码规范与标准 — Button篇

目前前端组件化已经成为前端开发的核心思想之一,在这篇文章中将深入探讨如何规划一个规范的Button组件,让它不仅能高效支持不同的功能需求还能确保跨项目、跨团队的一致性,抛砖引玉的方式引出后面组件库的其他组件的开发!

目录

Button组件搭建

bem命名规范

组件正式编码

样式解决方案

组件单元测试

组件文档搭建 

GitHub Actions

changeset版本

Button组件搭建

        Button按钮本质:一个button按钮之所以有需要样式的选择,究其原因就是class名称的组合,根据赋值的不同类名来实现不同的样式结果,如下所示:

class="ease-button--primary ease-button--larger is-plain is-disabled"

完成一个组件库需要考虑的问题有以下几个方面,根据任务进行需求分析,然后初始化项目确定项目文件结构,接下来就是规范基础写法、样式解决方案以及色彩系统等基础方面内容了:

代码结构;样式解决方案;组件需求分析和编码;组件测用例和编码;代码打包输出和发布;CI/CD及文档生成等

像Button组件大部分关注的只是样式而没有交互,根据分析我们可以得到如下具体的属性列表:

type:不同的样式(default、primary、danager、info、success、warning)
plain:样式的不同展现模式boolean
round:圆角boolean
circle:圆形按钮,适合图标boolean
disabled:禁用boolean
icon:图标添加
loading:加载样式添加

        组件目录结构:后期所有组件的开发按照如下目录进行规范进行,styles文件存储当前组件所有关联的样式;types文件存储当前组件所有关联的类型;其他tsx文件存储当前组件所有的核心代码;index.tsx文件存储当前组件暴露出去的封装组件及类型,如下所示:

bem命名规范

        bem:(Block, Element, Modifier)是一种CSS命名规范,用于使前端代码更加结构化和可维护,它通过三个主要部分来命名CSS类名,如下所示:

1)Block(块):代表一个独立的功能模块,可以是页面中的一个组件或者布局的一部分。例如header、button、menu

2)Element(元素):表示一个块内的子元素,通常是该块的一部分依赖于块的存在,例如header__title(title是header块的元素),button__icon。

3)Modifier(修饰符):用于描述块或元素的不同状态或外观变化,例如button--primary(代表一种主要按钮样式),header__title--large(代表较大的标题样式)

brm的命名规则让类名具有清晰的层级关系和可读性,有助于避免命名冲突和使代码更容易维护,这里我将其抽离到项目的utils文件夹下用来供全局组件进行调用,代码如下所示:

// block代码块 element元素 modifier装饰 state状态
/*** 示例:* ease-button--primary* is-checked*/// 创建BEM命名规范
const createBEM = (prefixName: string) => {const block = (blockSuffix: string = '') => _bem(prefixName, blockSuffix, '', '')const element = (element: string) => element ? _bem(prefixName, '', element, '') : ''const modifier = (modifier: string | undefined) => modifier ? _bem(prefixName, '', '', modifier) : ''const be = (blockSuffix: string = '', element: string = '') => blockSuffix && element ? _bem(prefixName, blockSuffix, element, '') : ''const bm = (blockSuffix: string = '', modifier: string = '') => blockSuffix && modifier ? _bem(prefixName, blockSuffix, '', modifier) : ''const em = (element: string = '', modifier: string = '') => element && modifier ? _bem(prefixName, '', element, modifier) : ''const bem = (blockSuffix: string = '', element: string = '', modifier: string = '') => blockSuffix && element && modifier ? _bem(prefixName, blockSuffix, element, modifier) : ''const is = (name: string, state: boolean | undefined) => (state ? `is-${name}` : "")return { block, element, modifier, be, bm, em, bem, is }
};// BEM命名规范实现
const _bem = (prefixName: string, blockSuffix: string, element: string, modifier: string) => {if (blockSuffix) prefixName += `-${blockSuffix}`;if (element) prefixName += `__${element}`;if (modifier) prefixName += `--${modifier}`;return prefixName;
}// 创建命名空间
export const createNameSpace = (name: string) => {const prefixName = `ease-${name}`return createBEM(prefixName)
};

组件正式编码

        接下来我们开始对button进行正式代码书写,当我们封装一个对应的原生组件的时候,原生属性必须要进行考量是否添加好,这里我们可以参考 MDN 官网提供的文档详细了解原生组件拥有哪些原生属性以及其具体的作用是什么:

typescript类型:当我们编写一个原生组件给其他人使用的时候ts类型尤为重要,只有我们写了ts类型之后用户才会知道我们开发的组件到底有哪些核心参数可以使用,这里我们完全可以参考其他热门组件库文件提供的一些范例来给我们开发组件库提供一些思路:

对于一个button按钮来讲,他有许多常量类型供我们选择、如按钮类型、形状、html类型、 尺寸等常量类型,这里我们可以将其抽离出一个constant.ts文件里面用于定义常量类型,如下所示:

友情提示!!!:我们可以定义代码块用于快速折叠相似功能代码:

// #region 规范按钮组件的常量类型
// 定义包含五种按钮类型的元组
const _ButtonTypes = ['default', 'primary', 'dashed', 'link', 'text'] as const;
export type ButtonType = (typeof _ButtonTypes)[number]; // 限制组件属性的取值范围// 定义包含三种按钮形状的元组
const _ButtonShapes = ['default', 'circle', 'round'] as const;
export type ButtonShape = (typeof _ButtonShapes)[number];// 定义包含三种按钮HTML类型的元组
const _ButtonHTMLTypes = ['submit', 'button', 'reset'] as const;
export type ButtonHTMLType = (typeof _ButtonHTMLTypes)[number];// 定义包含三种按钮尺寸的元组
const _ButtonSizes = ['large', 'middle', 'small'] as const;
export type ButtonSize = (typeof _ButtonSizes)[number];
// #endregion

定义完常量类型之后,我们在types类型文件夹下的入口index.ts文件中开始引入这些常量类型,然后设置props类型并给其赋值,常见的props类型我们可以参考热门组件库文档给出的范例,最终代码如下所示:

import React from 'react'
import { ButtonHTMLType, ButtonSize, ButtonShape, ButtonType } from './constant'interface BaseButtonProps {type: ButtonType; // 按钮类型,默认为 'default'shape: ButtonShape; // 按钮形状,默认为 'default'htmlType: ButtonHTMLType; // 按钮的 HTML 类型,默认为 'button'size: ButtonSize; // 按钮尺寸,默认为 'middle'plain: boolean; // 是否为朴素按钮,默认为 falsedisabled: boolean; // 是否禁用状态,默认为 falseloading: boolean; // 是否为加载中状态,默认为 falsecolor: string; // 按钮颜色children: React.ReactNode; // 按钮内容,默认为 null
}// 合并HTMLAttributes和 ButtonHTMLAttributes的属性,但不包括type、color、disabled、children属性
type MergedHTMLAttributes = Omit<React.HTMLAttributes<HTMLElement> &React.ButtonHTMLAttributes<HTMLElement> &React.AnchorHTMLAttributes<HTMLElement>,'type' | 'color' | 'disabled' | 'children'
>;export interface inheritProps extends BaseButtonProps, MergedHTMLAttributes {href: string;autoInsertSpace: boolean;
}// 使所有属性可选(Partial)且只读(Readonly)
export type ButtonProps = Readonly<Partial<inheritProps>>;

核心代码实现:编写完button组件的ts类型之后,接下来我们开始正式对原生button按钮进行组件化开发,这里我们定义一个组件的基本结构,通过props传递数据,并且借助函数将实例暴露出去,通过bem规范给组件动态设置类名:

import React, { forwardRef, useCallback } from 'react';
import clsx from 'clsx';
import { createNameSpace } from "@ztk63lrd/utils/create"
import type { ButtonProps } from './types';const EButton = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {// #region 组件变量声明const bem = createNameSpace('button');const { type, shape, htmlType = 'button',  size, disabled, loading, color, children, ...rest } = props;// #endregion// #region 组件样式构建const classes = clsx( // 使用 clsx 动态构建类名bem.block(), // 基础类名bem.modifier(type), // type 修改器bem.modifier(shape), // shape 修改器bem.modifier(size), // size 修改器bem.is('disabled', disabled), // disabled 状态bem.is('loading', loading) // loading 状态);// #endregion// #region 组件事件处理const handleClick = useCallback((e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => {if (disabled) {e.preventDefault();return;}props.onClick?.('href' in props? (e as React.MouseEvent<HTMLAnchorElement, MouseEvent>): (e as React.MouseEvent<HTMLButtonElement, MouseEvent>),);}, [props.onClick, disabled]);// #endregionreturn (<buttonclassName={classes} // 直接应用动态类名type={htmlType}disabled={disabled || loading}onClick={handleClick}ref={ref}{...rest}>{children}</button>);
});export default EButton;

接下来我们在组件文件夹的入口文件index.tsx处将封装组件及其类型暴露出去,然后在packages文件夹下的入口文件将所有components文件夹下写的所有组件再暴露一遍出去就可以了:

接下来我们开始在根目录终端执行pnpm run build即可,打包后就会在packages文件下生成build文件目录,并且会按照问你设置的组件目录结构生成相应的js文件和.d.ts类型声明文件,如下:

接下来我们终端运行pnpm run dev命令来执行review文件下测试组件项目的代码,在该文件下设置如下代码来查看我们编写的组件情况,如果我们直接导入ease-reactify就是我们打包好的实际组件代码,如果导入@ztk63lrd/components/button就是我们实时编写的组件代码:

import EButton from "@ztk63lrd/components/button"
// import { EButton } from "ease-reactify";const ButtonView = () => {return (<div><EButton size="middle" type="primary" shape="round" disabled>按钮</EButton></div>)
}export default ButtonView

通过上面我们给封装好的button组件添加类型限制之后,我们的bem规范已经被成功执行并且也出现在我们的运行项目上面了:

样式解决方案

        大家都知道作为一个组件库我们要采用什么样的色彩是尤为重要的,每一个热门的组件库都有它一套特定的色谱或者说是色彩系统,所以这时候我们就需要了解一下颜色对一个组件库的构成模式,这里我们可以参考antd的对于色彩的一些讲解:地址 文章还是比较详细的介绍了系统色以及产品色的一些概念内容,对我们设计组件库颜色方案给到了一个很好的启发:

        了解好样式解决方案之后,接下来我们可以访问 地址 来选择我们开发组件的一个默认样式:

        除上面我们可以选择颜色内容之后,我们也可以访问如下网站来设计一下我们的button组件:

确认好最终的一个样式方案之后,接下来我们开始正式对组件进行样式的调整,这里我们采用的css预处理scss进行样式调整,后期有精力的话也按照antd来设计全部用ts来写样式,目前的方案还是采用css处理器解决样式问题,目前的话还是根据不同的作用划分css文件,然后定义一些常用的方法和变量名来实现css样式内容:

然后根据我们要设计的组件,给button设计不同状态下的css样式,其中用到了封装好的公共的变量和css方法,如下所示:

最终我们在预览界面中实现的效果如下所示:

组件单元测试

        当我们编写完组件之后,为了确保后期组件的稳定性,即使后面项目的迭代和变更也不会影响原组件的功能,这里我们需要借助组件的单元测试来确保组件的稳定性,我们组件开发使用单元测试的重要性主要有以下几点原因:

1)高质量代码

2)更早的发现bug,减少成本

3)让重构和升级变得更加容易和可靠

4)让开发流程更加敏捷

        单元测试的方法有很多,因为我们采用的是vite作为打包器进行构建项目,所以这里我们也采用vite一派的单元测试工具 vitest 进行操作,详情可以参考我之前的文章:地址 ,vitest是兼容Jest的,所以说Vitest的api与Jest非常相似,用户从Jest迁移到Vitest基本上没啥学习成本:

接下来我们终端执行如下命令安装测试插件:

pnpm install -D vitest

安装完成之后,我们在vite的配置文件当中配置一下单元测试的一些关键配置:

当我们在相关组件下新建tests文件夹,就可以实现对组件的一些单元测试了:

浏览器模式与框架无关因此不提供任何渲染组件的方法。不过可以使用框架的测试工具包,这里我们采用如下的包进行组件测试,终端执行如下命令安装:

pnpm install @testing-library/react @testing-library/jest-dom @testing-library/user-event -D

然后这里我们使用该三方库中的render函数来测试渲染的组件库内容,如下可以看到我们给按钮设置的名称通过函数拿到按钮的文本,测试案例也正常通过了:

当然testing library旗下还有许多其他的工具,通过地址我们可以了解更多的测试工具的使用:

了解了一些工具的使用之后,接下来我们开始正式的对EButton组件进行单元测试,如下:

import { expect, test, describe, it, vi } from "vitest";
import { render, fireEvent } from "@testing-library/react"
import EButton, { ButtonProps } from "../index";
import '@testing-library/jest-dom'const defaultProps = {onClick: vi.fn()
}
const testProps: ButtonProps = {type: "primary",size: 'large',className: 'ease-test',
}
const disabledProps: ButtonProps = {disabled: true,onClick: vi.fn()
}describe("test EButton components", () => {it("应呈现正确的默认按钮", () => {const wrapper = render(<EButton {...defaultProps}>测试按钮</EButton>);const element = wrapper.getByText("测试按钮") as HTMLButtonElement; // 获取文本为"测试按钮"的元素expect(element).toBeInTheDocument(); // 断言该元素存在于文档中expect(element.tagName).toBe("BUTTON"); // 断言该元素的标签名为"BUTTON"expect(element).toHaveClass("ease-button"); // 断言该元素具有类名"ease-button"expect(element.disabled).toBeFalsy(); // 断言该元素未被禁用fireEvent.click(element);expect(defaultProps.onClick).toHaveBeenCalled(); // 断言onClick函数被调用});it("应根据不同的props渲染正确的组件", () => {const wrapper = render(<EButton {...testProps}>测试按钮</EButton>);const element = wrapper.getByText("测试按钮");expect(element).toBeInTheDocument();expect(element).toHaveClass("ease-button ease-button--primary ease-button--large ease-test");});it("禁用时应呈现禁用按钮设置为true", () => {const wrapper = render(<EButton {...disabledProps}>测试按钮</EButton>);const element = wrapper.getByText("测试按钮") as HTMLButtonElement;expect(element).toBeInTheDocument(); // 断言该元素存在于文档中expect(element.disabled).toBeTruthy(); // 断言该元素已被禁用expect(disabledProps.onClick).not.toHaveBeenCalled(); // 断言onClick函数未被调用})
});

最终可以看到我们的测试用例代码都通过了:

组件文档搭建 

        项目采用组件文档进行代码和具体相关展示,组件库文档有很多选择,可以参考文章了解一下其他组件库插件的实现,这里我们还是以vite衍生的组件库文档vitePress来实现,VitePress可以单独使用也可以安装到现有项目中,在这两种情况下都可以使用以下方式安装它:

npm add -D vitepress
npx vitepress init

安装完插件之后根据自身情况进行选择即可,如果正在构建一个独立的VitePress站点可以在当前目录 (./) 中搭建站点,但是如果在现有项目中与其他源代码一起安装VitePress,建议将站点搭建在嵌套目录(例如 ./docs)中以便它与项目的其余部分分开:

安装好插件之后,vitepress会自动帮助我们配置好环境命令,我们直接运行即可:

pnpm install markdown-it markdown-it-container markdown-it-mathjax3 -D

然后根据具体的一些官网配置进行调配即可:

GitHub Actions

        GitHub Actions:是GitHub提供的一个自动化工具,它可以帮助开发者自动执行构建、测试、部署等任务,通过GitHub Actions开发者可以定义一系列的工作流(workflow),并且这些工作流可以在GitHub上的代码库发生特定事件时触发,比如推送代码、发起 pull 请求、发布版本等。

一个工作流通常由多个步骤(steps)组成,每个步骤可以执行不同的命令或者运行一个自定义的脚本,工作流是由yaml文件描述的,存放在项目的.github/workflows 目录中,其特点主要包括:

1)自动化流程:自动执行重复的任务减少手动操作

2)GitHub紧密集成:可以直接在GitHub上定义和管理工作流

3)多平台支持:支持Linux、macOS 和 Windows环境

4)自定义工作流:可以根据自己的需求创建自定义的工作流和步骤

5)与其他工具服务集成:可以与其他工具API或者云服务集成,扩展功能

此次我们组件库的工作流,目前主要有两个,如下所示:

部署组件库文档:该工作流设置了名称,通过on来构建触发工作流的时机是提交代码

name: 'Deploy Docs'  # 工作流名称on:push:branches:- masterjobs:build-and-deploy:runs-on: ubuntu-latestpermissions:contents: write  # 添加写入权限steps:- name: 读取仓库内容uses: actions/checkout@v4  # 必须有uses来获取代码- name: 安装pnpmuses: pnpm/action-setup@v2  # 添加这一步安装pnpm- name: 安装依赖run: pnpm install --no-frozen-lockfile   # 运行pnpm install来安装依赖- name: 项目测试run: pnpm run test  # 运行pnpm测试项目- name: 构建项目run: pnpm run docs:build  # 运行构建文档的命令- name: 部署GitHub Pagesuses: JamesIves/github-pages-deploy-action@v4  # 使用GitHub Actions来部署到gh-pageswith:branch: gh-pages  # 指定部署的分支folder: 'docs/dist'  # 指定构建后的文件夹路径clean: true  # 清理旧的部署文件

发布npm平台:该工作流设置了名称,通过on来构建触发工作流的时机是package发生变化

name: Release Packageon:push:branches:- masterpaths:- 'packages/package.json'  # 如果是 monorepo,监听所有子包的 package.jsonjobs:release:runs-on: ubuntu-lateststeps:- name: 读取仓库内容uses: actions/checkout@v4with:fetch-depth: 0  # 获取完整历史,用于版本计算- name: Setup Node.jsuses: actions/setup-node@v3with:node-version: 21registry-url: https://registry.npmjs.org/- name: 安装pnpmuses: pnpm/action-setup@v2- name: 安装依赖(跳过锁文件检查)run: pnpm install --no-frozen-lockfile- name: 项目测试run: pnpm run test- name: 构建项目run: pnpm run build- name: 检查待发布的包run: npx changeset status- name: 创建版本号run: npx changeset version --snapshot ${{ github.event.inputs.release-type }}- name: 登录到npmrun: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc- name: 发布到npmrun: npx changeset publish --only ease-reactifyenv:NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

当我们提交代码的时候就会触发自动化流程,GitHub Actions就会自动帮我们执行对应的部署:

changeset版本

与git结合使用的一个特定文件或目录结构,用于跟踪和管理代码的变更集,这通常出现在需要对代码进行分组提交的场景或者在使用某些自动化工具、工作流、或 CI/CD过程中,终端执行如下命令进行安装:

pnpm add @changesets/cli -D

其常用命令如下所示:

# 初始化
npx changeset init# 添加 changeset
npx changeset add
npx changeset# 版本
npx changeset version# 发布
npx changeset publish

这里我将其命令放在package中的配置脚本中,想要发布的时候执行如下命令即可:

目前部署的组件库文档:地址 ,后期内容会不断完善!

http://www.lryc.cn/news/604014.html

相关文章:

  • 解决Spring MVC中@PathVariable参数为null导致的404问题:全面解析与最佳实践
  • 树形结构递归查询与嵌套结构转换:Flask + PostgreSQL 完整实现
  • EnergyMath芯祥代理 EMS4100可替代 ASW3410
  • 【牛客网C语言刷题合集】(五)——主要二进制、操作符部分
  • 深入解析mediasoup:构建实时音视频通信的高性能SFU解决方案
  • 用LangGraph实现聊天机器人记忆功能的深度解析
  • 深度学习篇---PaddleDetection模型选择
  • 循环神经网络——动手学深度学习7
  • electron-vite 动态加载脚本 实现动态插件
  • 使用jQuery时的注意事项
  • 爬虫逆向之瑞数五案例:某某医学院(补环境,联调)
  • 直播间里的酒旅新故事:内容正在重构消费链路
  • logtrick 按位或最大的最小子数组长度
  • 计算器4.0:新增页签功能梳理页面,通过IO流实现在用户本地存储数据
  • Java注解全面解析与应用实战
  • 三维扫描相机:工业自动化的智慧之眼——迁移科技赋能智能制造新纪元
  • 前端优化之虚拟列表实现指南:从库集成到手动开发
  • MongoDB系列教程-第一章:MongoDB简介、安装 、概念解析、用户管理、连接、实际应用示例
  • Java抽Oracle数据时编码问题
  • Spring Boot with RabbitMQ:四大核心模式指南
  • TDengine 中 TDgpt 异常检测的数据密度算法
  • TDengine 中 TDgpt 异常检测的机器学习算法
  • 中科米堆CASAIM金属件自动3d测量外观尺寸三维检测解决方案
  • 【数据结构初阶】--二叉树(四)
  • C# _列表(List<T>)_ 字典(Dictionary<TKey, TValue>)
  • uniapp 实现全局变量
  • C++与C#实战:FFmpeg屏幕录制开发指南
  • 高级机器学习
  • RTSP协议详解与C++实现实例
  • Witsbb健敏思携手奥运冠军吴敏霞 共启科学分龄育儿新时代