前端反卷计划-组件库-04-Button组件开发

简介: 前端反卷计划-组件库-04-Button组件开发

Hi, 大家好!我是程序员库里。

今天开始分享如何从0搭建UI组件库。这也是前端反卷计划中的一项。

在接下来的日子,我会持续分享前端反卷计划中的每个知识点。

以下是前端反卷计划的内容:

image.png

image.png

目前这些内容持续更新到了我的 学习文档 中。感兴趣的欢迎一起学习!

Button

4.1 需求分析

以antd design的Button组件为例

按钮类型

image.png

按钮尺寸

image.png

不可用状态

image.png

4.2 Demo

<Button size='large' type='primary' disabled>
按钮
</Button>

4.3 API

属性 说明 类型 默认值
type 按钮类型 primary default danger link default
size 按钮尺寸 lg sm -
disabled 设置按钮失效状态 boolean FALSE
href 点击跳转的地址,指定此属性 button 的行为和 a 链接一致 string -

4.4 开发

  1. 创建Button组件目录如下:

--components
    --Button
        --_style.scss            // 样式文件
        --button.stories.tsx     // demo
        --button.test.tsx        // 单元测试
        --button.tsx            // 核心代码逻辑
        --index.tsx            //  导出组件
  1. 定义按钮尺寸大小枚举值

export type ButtonSize = 'lg' | 'sm'
  1. 定义按钮类型枚举值

export type ButtonType = 'primary' | 'default' | 'danger' | 'link'
  1. 定义按钮的props

import React, { FC, ButtonHTMLAttributes, AnchorHTMLAttributes } from 'react'

interface BaseButtonProps {
  className?: string;
  /**设置 Button 的禁用 */
  disabled?: boolean;
  /**设置 Button 的尺寸 */
  size?: ButtonSize;
  /**设置 Button 的类型 */
  btnType?: ButtonType;
  children: React.ReactNode;
  href?: string;
}
// ButtonHTMLAttributes 是 React 中的一个内置泛型类型,它用于表示 HTML 按钮元素 (<button>) 上可以接受的属性。这些属性包括按钮的标准 HTML 属性,如 onClick、disabled、type 等
type NativeButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLElement>
// AnchorHTMLAttributes 是 React 中的一个内置泛型类型,它用于表示 HTML 锚点元素 (<a>) 上可以接受的属性。这些属性包括锚点元素的标准 HTML 属性,例如 href、target、onClick 等
type AnchorButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLElement>
// Partial可选
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>
  1. 根据传入的props决定按钮尺寸、按钮类型等逻辑

const {
    btnType,
    className,
    disabled,
    size,
    children,
    href,
    ...restProps
  } = props
  // btn, btn-lg, btn-primary
  const classes = classNames('btn', className, {
    [`btn-${btnType}`]: btnType,
    [`btn-${size}`]: size,
    'disabled': (btnType === 'link') && disabled
  })
  1. 判断是显示a标签还是button按钮

if (btnType === 'link' && href) {
    return (
      <a
        className={classes}
        href={href}
        {...restProps}
      >
        {children}
      </a>
    )
  } else {
    return (
      <button
        className={classes}
        disabled={disabled}
        {...restProps}
      >
        {children}
      </button>
    )
  }
  1. 定义按钮样式变量

src/styles/_variable.scss

// 按钮
// 按钮基本属性
$btn-font-weight:             400;
$btn-padding-y:               .375rem !default;
$btn-padding-x:               .75rem !default;
$btn-font-family:             $font-family-base !default;
$btn-font-size:               $font-size-base !default;
$btn-line-height:             $line-height-base !default;

//不同大小按钮的 padding 和 font size
$btn-padding-y-sm:            .25rem !default;
$btn-padding-x-sm:            .5rem !default;
$btn-font-size-sm:            $font-size-sm !default;

$btn-padding-y-lg:            .5rem !default;
$btn-padding-x-lg:            1rem !default;
$btn-font-size-lg:            $font-size-lg !default;

// 按钮边框
$btn-border-width:            $border-width !default;

// 按钮其他
$btn-box-shadow:              inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default;
$btn-disabled-opacity:        .65 !default;

// 链接按钮
$btn-link-color:              $link-color !default;
$btn-link-hover-color:        $link-hover-color !default;
$btn-link-disabled-color:     $gray-600 !default;


// 按钮 radius
$btn-border-radius:           $border-radius !default;
$btn-border-radius-lg:        $border-radius-lg !default;
$btn-border-radius-sm:        $border-radius-sm !default;

$btn-transition:              color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default;
  1. 编写按钮基本样式

.btn {
  position: relative;
  display: inline-block;
  font-weight: $btn-font-weight;
  line-height: $btn-line-height;
  color: $body-color;
  white-space: nowrap;
  text-align: center;
  vertical-align: middle;
  background-image: none;
  border: $btn-border-width solid transparent;
  @include button-size( $btn-padding-y,  $btn-padding-x,  $btn-font-size,  $border-radius);
  box-shadow: $btn-box-shadow;
  cursor: pointer;
  transition: $btn-transition;
  &.disabled,
  &[disabled] {
    cursor: not-allowed;
    opacity: $btn-disabled-opacity;
    box-shadow: none;
    > * {
      pointer-events: none;
    }
  }
}

在这里为了不重复复制样式代码,我们采用mixin来处理。

比如上面代码中的@include button-size 函数,这个是scss的一个特性,可以从官网上看下介绍。

@include button-size( $btn-padding-y,  $btn-padding-x,  $btn-font-size,  $border-radius);

要使用上面的方法,需要在mixin编写上面的函数

新建 src/styles/_mixin.scss,编写如下代码:

这里解释一下:相当于在button-size中传了4个参数,使用这4个参数来定义样式属性,使用的时候即可传入对应的样式变量即可。

@mixin button-size($padding-y, $padding-x, $font-size, $border-raduis) {
  padding: $padding-y $padding-x;
  font-size: $font-size;
  border-radius: $border-raduis;
}
  1. 编写按钮尺寸大小的代码,这里同样适用mixin,使用button-size函数进行复用。

.btn-lg {
  @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg);
}
.btn-sm {
  @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm);
}
  1. 编写按钮类型的样式代码,这里同样适用mixin,使用了button-style,这就需要在_mixin.scss中进行定义

.btn-primary {
  @include button-style($primary, $primary, $white)
}
.btn-danger {
  @include button-style($danger, $danger, $white)
}

.btn-default {
  @include button-style($white, $gray-400, $body-color, $white, $primary, $primary)
}
  1. 在_mixin.scss中定义 button-style函数

@mixin button-style(
  $background,
  $border,
  $color,
  $hover-background: lighten($background, 7.5%),
  $hover-border: lighten($border, 10%),
  $hover-color: $color,
) {
  color: $color;
  background: $background;
  border-color: $border;
  &:hover {
    color: $hover-color;
    background: $hover-background;
    border-color: $hover-border;    
  }
  &:focus,
  &.focus {
    color: $hover-color;
    background: $hover-background;
    border-color: $hover-border;    
  }
  &:disabled,
  &.disabled {
    color: $color;
    background: $background;
    border-color: $border;    
  }
}
  1. 编写 按钮是 link 标签时候的样式代码

.btn-link {
  font-weight: $font-weight-normal;
  color: $btn-link-color;
  text-decoration: $link-decoration;
  box-shadow: none;
  &:hover {
    color: $btn-link-hover-color;
    text-decoration: $link-hover-decoration; 
  }
  &:focus,
  &.focus {
    text-decoration: $link-hover-decoration;
    box-shadow: none;
  }
  &:disabled,
  &.disabled {
    color: $btn-link-disabled-color;
    pointer-events: none;
  }
}
  1. 完整代码

import React, { FC, ButtonHTMLAttributes, AnchorHTMLAttributes } from 'react'
import classNames from 'classnames'

export type ButtonSize = 'lg' | 'sm'
export type ButtonType = 'primary' | 'default' | 'danger' | 'link'

interface BaseButtonProps {
  className?: string;
  /**设置 Button 的禁用 */
  disabled?: boolean;
  /**设置 Button 的尺寸 */
  size?: ButtonSize;
  /**设置 Button 的类型 */
  btnType?: ButtonType;
  children: React.ReactNode;
  href?: string;
}
type NativeButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLElement>
type AnchorButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLElement>
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>
/**
 * 页面中最常用的的按钮元素,适合于完成特定的交互
 * ### 引用方法
 * 
 * ~~~js
 * import { Button } from 'curry-design'
 * ~~~
 */
export const Button: FC<ButtonProps> = (props) => {
  const {
    btnType,
    className,
    disabled,
    size,
    children,
    href,
    ...restProps
  } = props
  // btn, btn-lg, btn-primary
  const classes = classNames('btn', className, {
    [`btn-${btnType}`]: btnType,
    [`btn-${size}`]: size,
    'disabled': (btnType === 'link') && disabled
  })
  if (btnType === 'link' && href) {
    return (
      <a
        className={classes}
        href={href}
        {...restProps}
      >
        {children}
      </a>
    )
  } else {
    return (
      <button
        className={classes}
        disabled={disabled}
        {...restProps}
      >
        {children}
      </button>
    )
  }
}

Button.defaultProps = {
  disabled: false,
  btnType: 'default'
}

export default Button;

4.1.14 效果展示

http://localhost:6006/?path=/docs/example-button--docs

4.5 单元测试

  1. 测试工具

https://testing-library.com/docs/react-testing-library/intro/

pnpm install --save-dev @testing-library/react
  1. jest-dom

https://testing-library.com/docs/ecosystem-jest-dom/

增加dom操作的类型断言

npm install @testing-library/jest-dom --save-dev

create-react-app已经帮我们导入了src/setupTests.ts

// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

安装jest的ts类型,create-react-app默认自带了,就不用安装了。之前版本没有,就需要安装。

npm install --save-dev @types/jest

4.5.1 测试1:展示正确的默认按钮

import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Button from './button'

const defaultProps = {
    onClick: jest.fn()
}

describe('test Button component', () => {
    // 渲染正确的默认的按钮
    it('should render the correct default button', () => {
      // 通过 render 来渲染一个按钮,赋值给wrapper
      const wrapper = render(<Button {...defaultProps}>按钮</Button>)
      // 使用wrapper的getByText获取指定文字的按钮,赋值给element
      const element = wrapper.getByText('按钮') as HTMLButtonElement
      // 使用expect函数,表示断言。将element传入expect,调用toBeInTheDocument表示按钮插入到了页面中
      expect(element).toBeInTheDocument()
      // 获取按钮的tagName,使用toEqual函数看按钮的tagName是否叫BUTTON
      expect(element.tagName).toEqual('BUTTON')
      // 使用toHaveClass函数来判断按钮是否有btn btn-default这两个class
      expect(element).toHaveClass('btn btn-default')
      // 传入按钮的disabled,使用toBeFalsy来判断按钮是否带有disabled属性,toBeFalsy表示false
      expect(element.disabled).toBeFalsy()
      // 使用fireEvent执行点击事件
      fireEvent.click(element)
      // 执行上述点击事件后,使用toHaveBeenCalled来判断按钮是否被点击了,toHaveBeenCalled表示按钮被点击了。
      expect(defaultProps.onClick).toHaveBeenCalled()
    })
})

在终端输入:npm run test 执行下测试用例,看是否通过。可以看到测试用例通过了。
image.png

4.5.2 测试2:根据传入的props渲染对应的按钮

const testProps: ButtonProps = {
    btnType: ButtonType.Primary,
    size: ButtonSize.Large,
    className: 'klass'
}

it('should render the correct component based on different props', () => {
        const wrapper = render(<Button {...testProps}>按钮</Button>)
        const element = wrapper.getByText('按钮')
        expect(element).toBeInTheDocument()
        expect(element).toHaveClass('btn-primary btn-lg klass')
    })

测试结果:

image.png

4.5.3 测试3:测试按钮类型是a标签

 it('should render a link when btnType equals link and href is provided', () => {
        const wrapper = render(<Button btnType='link' href="http://www.baidu.com">Link</Button>)
        const element = wrapper.getByText('Link')
        expect(element).toBeInTheDocument()
        expect(element.tagName).toEqual('A')
        expect(element).toHaveClass('btn btn-link')
    })

测试结果:
image.png

4.5.4 测试4:测试按钮的disabled属性

const disabledProps: ButtonProps = {
    disabled: true,
    onClick: jest.fn(),
}
it('should render disabled button when disabled set to true', () => {
        const wrapper = render(<Button {...disabledProps}>按钮</Button>)
        const element = wrapper.getByText('按钮') as HTMLButtonElement
        expect(element).toBeInTheDocument()
        expect(element.disabled).toBeTruthy()
        fireEvent.click(element)
        expect(disabledProps.onClick).not.toHaveBeenCalled()
    })

测试结果:

image.png

以上4个测试用例全部通过。

4.5.6 完整测试用例代码

import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Button, { ButtonProps, ButtonSize, ButtonType } from './button'

const defaultProps = {
    onClick: jest.fn()
}

const testProps: ButtonProps = {
    btnType: ButtonType.Primary,
    size: ButtonSize.Large,
    className: 'klass'
}
const disabledProps: ButtonProps = {
    disabled: true,
    onClick: jest.fn(),
}

describe('test Button component', () => {
    it('should render the correct default button', () => {
        const wrapper = render(<Button {...defaultProps}>按钮</Button>)
        const element = wrapper.getByText('按钮') as HTMLButtonElement
        expect(element).toBeInTheDocument()
        expect(element.tagName).toEqual('BUTTON')
        expect(element).toHaveClass('btn btn-default')
        expect(element.disabled).toBeFalsy()
        fireEvent.click(element)
        expect(defaultProps.onClick).toHaveBeenCalled()
    })

    it('should render the correct component based on different props', () => {
        const wrapper = render(<Button {...testProps}>按钮</Button>)
        const element = wrapper.getByText('按钮')
        expect(element).toBeInTheDocument()
        expect(element).toHaveClass('btn-primary btn-lg klass')
    })
    it('should render a link when btnType equals link and href is provided', () => {
        const wrapper = render(<Button btnType='link' href="http://www.baidu.com">Link</Button>)
        const element = wrapper.getByText('Link')
        expect(element).toBeInTheDocument()
        expect(element.tagName).toEqual('A')
        expect(element).toHaveClass('btn btn-link')
    })
    it('should render disabled button when disabled set to true', () => {
        const wrapper = render(<Button {...disabledProps}>按钮</Button>)
        const element = wrapper.getByText('按钮') as HTMLButtonElement
        expect(element).toBeInTheDocument()
        expect(element.disabled).toBeTruthy()
        fireEvent.click(element)
        expect(disabledProps.onClick).not.toHaveBeenCalled()
    })
})

系列篇

前端反卷计划-组件库-01-环境搭建

前端反卷计划-组件库-02-storybook

前端反卷计划-组件库-03-组件样式

持续更新

目前这些内容持续更新到了我的 学习文档 中。感兴趣的欢迎一起学习!

相关文章
|
15天前
|
数据采集 前端开发 JavaScript
《花100块做个摸鱼小网站! 》第四篇—前端应用搭建和完成第一个热搜组件
本文档详细介绍了从零开始搭建一个包含前后端交互的热搜展示项目的全过程。通过本教程,读者不仅能学习到完整的项目开发流程,还能掌握爬虫技术和前后端交互的具体实践。适合有一定编程基础并对项目实战感兴趣的开发者参考。
58 1
|
12天前
|
JavaScript 前端开发 开发者
哇塞!Vue.js 与 Web Components 携手,掀起前端组件复用风暴,震撼你的开发世界!
【8月更文挑战第30天】这段内容介绍了Vue.js和Web Components在前端开发中的优势及二者结合的可能性。Vue.js提供高效简洁的组件化开发,单个组件包含模板、脚本和样式,方便构建复杂用户界面。Web Components作为新兴技术标准,利用自定义元素、Shadow DOM等技术创建封装性强的自定义HTML元素,实现跨框架复用。结合二者,不仅增强了Web Components的逻辑和交互功能,还实现了Vue.js组件在不同框架中的复用,提高了开发效率和可维护性。未来前端开发中,这种结合将大有可为。
50 0
|
11天前
|
Android开发 iOS开发 C#
Xamarin:用C#打造跨平台移动应用的终极利器——从零开始构建你的第一个iOS与Android通用App,体验前所未有的高效与便捷开发之旅
【8月更文挑战第31天】Xamarin 是一个强大的框架,允许开发者使用单一的 C# 代码库构建高性能的原生移动应用,支持 iOS、Android 和 Windows 平台。作为微软的一部分,Xamarin 充分利用了 .NET 框架的强大功能,提供了丰富的 API 和工具集,简化了跨平台移动应用开发。本文通过一个简单的示例应用介绍了如何使用 Xamarin.Forms 快速创建跨平台应用,包括设置开发环境、定义用户界面和实现按钮点击事件处理逻辑。这个示例展示了 Xamarin.Forms 的基本功能,帮助开发者提高开发效率并实现一致的用户体验。
23 0
|
11天前
|
前端开发 UED 开发者
React组件优化全攻略:深度解析让你的前端应用飞速运行的秘诀——从PureComponent到React.memo的彻底性能比较
【8月更文挑战第31天】在构建现代Web应用时,性能是提升用户体验的关键因素。React作为主流前端库,其组件优化尤为重要。本文深入探讨了React组件优化策略,包括使用`PureComponent`、`React.memo`及避免不必要的渲染等方法,帮助开发者显著提升应用性能。通过实践案例对比优化前后效果,不仅提高了页面渲染速度,还增强了用户体验。优化React组件是每个开发者必须关注的重点。
22 0
|
12天前
|
JavaScript 前端开发
揭秘Vue.js组件魔法:如何轻松驾驭前端代码,让维护变得轻而易举?
【8月更文挑战第30天】本文探讨了如何利用Vue.js的组件化开发提升前端代码的可维护性。组件化开发将复杂页面拆分为独立、可复用的组件,提高开发效率和代码可维护性。Vue.js支持全局及局部组件注册,并提供了多种组件间通信方式如props、事件等。通过示例展示了组件定义、数据传递及复用组合的方法,强调了组件化开发在实际项目中的重要性。
12 0
|
30天前
|
存储 前端开发 JavaScript
前端语言串讲 | 青训营笔记
前端语言串讲 | 青训营笔记
20 0
|
3月前
|
JSON 前端开发 JavaScript
前端Ajax、Axios和Fetch的用法和区别笔记
前端Ajax、Axios和Fetch的用法和区别笔记
62 2
|
3月前
|
前端开发 JavaScript 数据库
如何实现前后端分离-----前端笔记
如何实现前后端分离-----前端笔记
|
3月前
|
前端开发 安全 NoSQL
技术笔记:Security前端页面配置
技术笔记:Security前端页面配置
|
4月前
|
存储 前端开发 JavaScript
前端笔记_OAuth规则机制下实现个人站点接入qq三方登录
前端笔记_OAuth规则机制下实现个人站点接入qq三方登录
74 1