React+Ts中如何优雅的二次封装组件

lxf2023-12-21 03:20:02

React+Ts中如何优雅的二次封装组件

前言

在react中相信大家用的最多的组件库就是Antd了,可是往往在公司的实际开发过程中,我们会发现ui给的设计图和组件有着不小的差别,不管是在样式上还是功能上。并且可能存在根据项目主题定制的风格,那我们最好是不要一个个的去使用antd里的组件,然后再去修改其内部样式和功能扩展或修改,这样不管是从效率还是维护性都不是理想的。那如何优雅的对现有的组件库进行二次封装就是我们今天探讨的主要内容了,以下做法有不足之处望各位大佬不吝赐教。
ps:项目演示采用 react18+umi4+antd5+ts

样式

若是确定了组件在项目中的整体样式,可以写在全局样式中,做一个覆盖效果。
以下的overrides.less 是Umi中专门为修改组件样式定制的,我们通过增加id选择器的方式加强权重来进行覆盖默认样式的效果。

React+Ts中如何优雅的二次封装组件

若是有某些样式不一致的地方,我们可以直接到组件里再进行一次覆盖,如下所示:
同理只是又借用了组件的id进行加强权重 React+Ts中如何优雅的二次封装组件 这样就避免了 !import的普遍存在了

类型扩展

我们可能会遇到组件依赖外部类型来决定内部类型的情况,就比如表格组件中列表的数据类型肯定是不一样的,那我们就需要通过泛型让外面传递进来。从而达到规范的效果。

import type { PaginationProps } from 'antd'
import { Table } from 'antd'
import type { TableProps } from 'antd/es/table'
import {
  FilterValue,
  RowSelectMethod,
  SorterResult,
  TableCurrentDataSource,
  TablePaginationConfig,
  TableRowSelection,
} from 'antd/es/table/interface'
import { ForwardedRef, forwardRef, Key, useCallback, useImperativeHandle, useMemo, useState } from 'react'
import styles from './index.less'

// 处理forwardRef使其可以接受泛型
declare module 'react' {
  // eslint-disable-next-line @typescript-eslint/ban-types
  function forwardRef<T, P = {}>(
    render: (props: P, ref: React.Ref<T>) => React.ReactElement | null
  ): (props: P & React.RefAttributes<T>) => React.ReactElement | null
}

export type onChangeType<RecordType> = (
  pagination: TablePaginationConfig,
  filters: Record<string, FilterValue | null>,
  sorter: SorterResult<RecordType> | SorterResult<RecordType>[],
  extra: TableCurrentDataSource<RecordType>
) => void

export type onSelectChangeType<T> = (
  selectedRowKeys: Key[],
  selectedRows: T[],
  info: {
    type: RowSelectMethod
  }
) => void

interface TProps<T> extends React.PropsWithChildren<TableProps<T>> {
  tableData?: pagingResProps<T> // 总数据
  tableOnChange?: onChangeType<T> // 变化回调
  tableRowSelection?: TableRowSelection<T> // 自定义行数据设置
  tablePagination?: TablePaginationConfig //自定义分页配置
  bottomTitleFlag?: boolean // 左下页角信息是否显示
}

// table ref的参数
export interface TableDefaultRefProps<T> {
  selectedRowKeys: Key[]
  selectedRowRows: T[]
}

const _TableDefault = <T extends object>(props: TProps<T>, ref: ForwardedRef<TableDefaultRefProps<T>>) => {
  return <div>table</div>
}

const TableDefault = forwardRef(_TableDefault)
export default TableDefault

我们使用时就可以直接传入类型

React+Ts中如何优雅的二次封装组件

功能扩展 继承 修改 拦截

我们想要扩展组件内的结构,可以自定义字段来控制显示,如下的bottomTitleFlag,就是控制标题的展示。
我们可以通过...otherProps的方式接收剩余参数,从而实现了可以在我们的组件上传递antd规定的组件属性,若是相同则会进行覆盖采用新传入的。若是我们不传递则采用内置的,若是内置的还不满足需求,我们可以在内置里再加上剩余参数的写法进行补充如 ...tablePagination。
事件需要拦截可以内置一个事件,然后通过调用内置事件时进行数据相关处理后再去调传入的事件,这样就实现拦截的效果了。
另外对于外界可能会用的的一些参数我们可以通过 useImperativeHandle 进行Ref抛出,使得更好去获取内部的属性。

const _TableDefault = <T extends object>(props: TProps<T>, ref: ForwardedRef<TableDefaultRefProps<T>>) => {
  const { tableData, tableOnChange, tableRowSelection, tablePagination, bottomTitleFlag = false, ...otherProps } = props

  // 当前选择的key和行
  const [selectedRowKeys, setSelectedRowKeys] = useState<Key[]>([])
  const [selectedRowRows, setSelectedRowRows] = useState<T[]>([])

  // 左右分页样式处理
  const itemRender: PaginationProps['itemRender'] = (_, type, originalElement) => {
    if (type === 'prev') {
      return <a style={{ color: '#CCF2FF', fontSize: '14px' }}>上一页</a>
    }
    if (type === 'next') {
      return <a style={{ color: '#CCF2FF', fontSize: '14px' }}>下一页</a>
    }
    return originalElement
  }

  const handelChange: onChangeType<T> = useCallback((pagination, filters, sorter, extra) => {
    // 想要做的拦截操作
    props.tableOnChange?.(pagination, filters, sorter, extra)
  }, [])

  // 选择多选框回调
  const onSelectChange: onSelectChangeType<T> = (selectedRowKeys, selectedRows, info) => {
    setSelectedRowKeys(selectedRowKeys)
    setSelectedRowRows(selectedRows)
  }

  // ref抛出变量
  useImperativeHandle(ref, () => ({
    selectedRowKeys,
    selectedRowRows,
  }))
  // 开始页码
  const startCode = useMemo(
    () => () => {
      if (!tableData || !tableData?.current) return 1
      return (tableData?.current - 1) * tableData?.size + 1
    },
    [tableData]
  )
  // 结束页码
  const endCode = useMemo(
    () => () => {
      if (!tableData) return 99
      return Math.min(tableData?.total, tableData?.current * tableData?.size)
    },
    [tableData]
  )
  return (
    <div
      className={styles.tableDefault}
      id="tableDefault"
    >
      <Table
        rowKey="id"
        dataSource={tableData?.records}
        pagination={{
          itemRender,
          total: tableData?.total,
          showSizeChanger: false,
          pageSize: tableData?.size,
          current: tableData?.current,
          ...tablePagination,
        }}
        onChange={handelChange}
        rowSelection={{
          type: 'checkbox',
          fixed: false,
          columnWidth: '120px',
          selectedRowKeys,
          onChange: onSelectChange,
          ...tableRowSelection,
        }}
        {...otherProps}
      />
      <div className={styles.leftIcon}></div>
      {bottomTitleFlag && (
        <div className={styles.bottomTitle}>{`显示第${startCode()}到第${endCode()}条记录,总共${
          tableData?.total
        }条记录`}</div>
      )}
    </div>
  )
}

const TableDefault = forwardRef(_TableDefault)
export default TableDefault

我们想要扩展组件内的功能,增加内置功能可以直接在组件内部增加,使用内部数据来完成,最后进行一个抛出。
这里使用 node[fieldNames.key] 计算属性名的原因是树组件的字段可能会发生变化,所以我们需要根据传入的fieldNames来进行字段更新。

树组件

// 当前节点展开
  function nowNodeExpand(node: DataNode) {
    const newExpandedKeys: any[] = []
    const fn = (node: any) => {
      node[fieldNames.key] && newExpandedKeys.push(node[fieldNames.key])
      node[fieldNames.children] && node[fieldNames.children].forEach((item: DataNode) => fn(item))
    }
    fn(node)
    setExpandedKeys(uniq([...expandedKeys, ...newExpandedKeys]))
  }
  
  // ref抛出变量
  useImperativeHandle(ref, () => ({
    nowNodeExpand,
  }))

End

有不对的地方或者有更好的做法大家可以畅所欲言!希望本文能给你带来帮助。
一键三连,关注不迷路!

React+Ts中如何优雅的二次封装组件

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!