从零构建自己的React组件库(贰)—— Alert

291 阅读6分钟

前情回顾

在上一篇中我就说过,之前采用的是先进行需求分析,功能使用,代码开发,测试添加的方式完成一个组件。这种方式在编写测试用例时让我感觉到一种不舒服的感觉,因为它是根据已经开发出来的组件进行编写的,所以需要不停的看已经开发出来的组件代码,根据组件代码来决定需要写什么样的测试用例,那么这样编写出的测试用例很有可能就是错误的,导致测试通过。 那么我从这一节开始,将使用TDD的开发模式进行开发。

moon-ui 仅用于学习使用,组件功能参考antd,内部代码尽量由我个人编写实现,部分代码内容可能会参考antd的实现。

一、需求分析

第一步仍然是需求分析,借鉴一下 Antd 的功能来进行分析,一个 Alert 组件具备以下功能:

  1. 首先 Alert 拥有着 message 属性用于展示需要展示内容
  2. Alert组件跟Button组件一样,有不同的 type ,这些 type 会展示不同的背景颜色
  3. 还有左边的 icon 及 控制是否展示的 showIcon 两个属性,展示的图标根据 type 类型决定
  4. 当存在 description 时,message 字体增大,同时 icon 也会变为大号的
  5. 有控制是否现实关闭按钮的 closable 属性,并且支持自定义按钮的 closeIcon 属性,以及点击关闭的回调事件 onClose
  6. banner 用于顶部展示公告,并且默认type = "warning"showIcon = true

二、TDD 第一步

src/components下创建Alert及文件,当前整体项目目录如下

moon-ui
├─ package.json
├─ src
│  ├─ App.css
│  ├─ App.tsx
│  ├─ components
│  │  ├─ Alert
│  │  │  ├─ __test__
│  │  │  │  └─ alert.test.tsx
│  │  │  └─ alert.tsx
│  │  └─ Button
│  │     ├─ __test__
│  │     │  └─ button.test.tsx
│  │     ├─ _style.scss
│  │     ├─ button.tsx
│  ├─ icons
│  │  ├─ _index.scss
│  │  └─ loadingOutlined
│  │     ├─ _style.scss
│  │     └─ index.tsx
│  ├─ index.css
│  ├─ index.tsx
│  ├─ logo.svg
│  ├─ react-app-env.d.ts
│  ├─ reportWebVitals.ts
│  ├─ setupTests.ts
│  └─ styles
│     ├─ _mixin.scss
│     ├─ _reboot.scss
│     ├─ _variables.scss
│     └─ index.scss
├─ tsconfig.json
└─ yarn.lock

alert.tsx文件内容如下

// src/components/Alert/alert.tsx

const Alert = () => {

  return (
    <div>
    </div>
  )
}

export default Alert

TDD 的开发方式就是先写测试用例,再根据测试结果写实际的运行代码,先在alert.test.tsx中写上一个渲染最基本的Alert组件,并给它传入message属性。

// src/components/Alert/__test__/alert.test.tsx
import { render } from '@testing-library/react'
import Alert from '../alert'

describe('test Alert component', () => {
    it('should render the correct default alert', () => {
        const wrapper = render(<Alert />)
        // 一般应用在想要测试的元素上 getByTestId 方法是 jest 提供的
        // 需要在对应元素上有 data-testid
        const element = wrapper.getByTestId('test-alert')
    })
})

在运行yarn test后就会出现 ❌ 错误提示TestingLibraryElementError: Unable to find an element by: [data-testid="test-alert"] 意思就是没有在元素中找到有属性 data-testid且值为test-alert的元素。此时只需要在Alert组件上加上这个即可

// src/components/Alert/alert.tsx

const Alert = () => {

  return (
    <div data-testid="test-alert">
    </div>
  )
}

export default Alert

再看控制台就会发现测试通过 ✅ 。

三、添加 message

其中,在写上message属性时,编辑器就会提醒message不存在Alert组件上,可以此时就在Alert组件上定义props的类型 包含着message的接口,其中messageReactNode类型

// src/components/Alert/alert.tsx

interface AlertProps {
  message: React.ReactNode
}
const Alert = (props: AlertProps) => {

  return (
    <div data-testid="test-alert">
    </div>
  )
}

export default Alert

测试通过 ✅ 接着来看一下 message 的渲染,在alert.test.tsx中添加上message并尝试测试其渲染

// src/components/Alert/__test__/alert.test.tsx
// ......
    it('should render the correct default alert', () => {
        const message = "i am a message"
        // 添加上 message
        const wrapper = render(<Alert message={message}/>)
        const element = wrapper.getByTestId('test-alert')
        // 并尝试通过 message 找到该元素
        const textElment = wrapper.getByText(message)
        expect(element).toBeInTheDocument()
        expect(textElment).toBeInTheDocument()
    })
// ......

测试抛错❌ ,message没有被渲染上,在Alert组件中加上message的渲染即可。

四、类型 type

类型有4种:successinfowarningerror 在测试函数中新写一个测试块,使用上type属性,其值为AlertType.Success,此时组件上没有type并且也没有地方能找到AlertType这个变量。那么就需要在Alert组件上添加type属性,并在其中声明AlertType

// src/components/Alert/__test__/alert.test.tsx
// ......
  it('should render the correct type on different props', () => {
      const wrapper = render(<Alert message="msg" type={AlertType.Success} />)
  })
// ......

Alert 组件:

// src/components/Alert/alert.tsx
export enum AlertType {
    Success = 'success',
    Info = 'info',
    Warning = 'warning',
    Error = 'error'
}

interface AlertProps {
    message: React.ReactNode,
    type?: AlertType
}

function Alert(props: AlertProps) {
    const { message } = props
    return (
        <div data-testid="test-alert">
            <div>{message}</div>
        </div>
    )
}

export default Alert

测试通过 ✅

不同的类型有不同的样式,也就是需要根据类型type去添加对应的className,这与上一章写Button组件一致,但这里的思路是先测试,再根据测试报错来添加业务代码。

// src/components/Alert/__test__/alert.test.tsx
const TEST_ID = 'test-alert'
// ......
    it('should render the correct type on different props', () => {
        const wrapper = render(<Alert message="success" type={AlertType.Success} />)
        // 根据 id 获取元素
        const element = wrapper.getByTestId(TEST_ID)
        // 断言 元素 应该拥有的 class
        expect(element).toHaveClass('alert alert-success-wrapper')
    })

// ......

此时因为组件代码中并不含有alert alert-success-wrapper这两个class 报错 image.pngAlert组件中添加上对应的 class 即可

// src/components/Alert/alert.tsx
export enum AlertType {
    Success = 'success',
    Info = 'info',
    Warning = 'warning',
    Error = 'error'
}

interface AlertProps {
    message: React.ReactNode,
    type?: AlertType
}

function Alert(props: AlertProps) {
    const { message } = props

    const baseClassName = `alert-${type}`
    const classes = classNames('alert', {
        [`${baseClassName}-wrapper`]: type
    })

    return (
        <div className={classes} data-testid="test-alert">
            <div>{message}</div>
        </div>
    )
}

export default Alert

保存以后测试通过 ✅ ,接着只需要在测试文件中<Alert />组件上更换剩下的几种type来测试一下看其他类型是否成功加上对应的class,然后在_style.scss 文件中添加上几种样式(这里省略了封装_mixin.scss中的alert-style函数的描述,其实就是设置backgroundborder-color)。

// src/components/Alert/_style.scss

.alert {
	box-sizing: border-box;
	border: 1px solid;
	position: relative;
	padding: 8px 15px;
	border-radius: 2px;
}

.alert-success-wrapper {
	@include alert-style($success-color-deprecated-bg, $success-color-deprecated-border)
}

.alert-info-wrapper {
	@include alert-style($info-color-deprecated-bg, $info-color-deprecated-border)
}

.alert-warning-wrapper {
	@include alert-style($warning-color-deprecated-bg, $warning-color-deprecated-border)
}

.alert-error-wrapper {
	@include alert-style($error-color-deprecated-bg, $error-color-deprecated-border)
}

五、支持关闭

如下图实现点击按钮关闭Alert的功能 May-17-2022 19-05-45.gif 编写测试用例如下:

//
import userEvent from '@testing-library/user-event'

    it('could be closed', () => {
        const onClose = jest.fn() // 模拟一个事件
        const wrapper = render(
            <Alert
                message="closabel"
                closable // 设置为可关闭
                onClose={onClose} // 关闭回调
            />
        )
        const element = wrapper.getByTestId(TEST_ID)
        const closeElement = wrapper.getByTestId(TEST_CLOSE_ICON_ID)
        userEvent.click(closeElement) // userEvent 与 fireEvent 一样用于事件触发
        expect(onClose).toHaveBeenCalled() // 判断是否调用了事件
        expect(element).not.toBeInTheDocument() // 接着判断元素是否不在页面上
    })

    it('support closeIcon', () => {
        // 测试是否支持自定义关闭元素
        const wrapper = render(<Alert closable closeIcon={<span>close</span>} message="" />)

        expect(wrapper.getByText('close')).toBeInTheDocument()
    })

根据测试用例的失败提示编写代码

// src/components/Alert/alert.tsx
export enum AlertType {
    Success = 'success',
    Info = 'info',
    Warning = 'warning',
    Error = 'error'
}

interface AlertProps {
    message: React.ReactNode,
    type?: AlertType
}

function Alert(props: AlertProps) {
    const { message } = props

    const baseClassName = `alert-${type}`
    const classes = classNames('alert', {
            [`${baseClassName}-wrapper`]: type
    })
  
    {/* 是否关闭 */}
    const [closed, setClosed] = useState(false)

    {/* 关闭事件 */}
    const handleClose = (e: React.MouseEvent<HTMLButtonElement>) => {
        setClosed(true)
        props.onClose?.(e)
    }
  
  {/* 判断为关闭则设置为返回 null */}
  if (closed) return null 

    return (
        <div className={classes} data-testid="test-alert">
            <div>{message}</div>
          {/* 添加关闭元素 */}
          {
            closable && (
                <button
                    type="button"
                    data-testid="test-alert-close-icon"
                    className="alert-close-icon"
                    onClick={handleClose}
                >
                    <span className="alert-close-text">{closeIcon}</span>
                </button>
            )
        }
        </div>
    )
}

export default Alert

接着添加一下按钮的样式就可以了,样式代码就不贴了,github 地址在第一篇。

结尾

尽管我还没有给它加上 icon,但我想写到这里就结尾好了,老实讲,在一边写组件一边写文章的过程真的是比较折磨的一件事,这个系列我会争取写下去,后续在做的过程中会保持以TDD的方式去开发,但是在文章输出时不会涉及到了,主要是写起文章来比较麻烦。

最近会开一个算法系列,把常用的数据结构和算法都写上一遍,算是写这个系列的一点调味品吧(因为真的有点儿枯燥~)。