react二次封装antd Button,实现鉴权按钮功能。

1,413 阅读5分钟

这篇文章写的比较复杂,路走歪了。感兴趣的可以继续阅读。 不感兴趣,只想实现功能的可以看这篇(超简单实现): # reactHoots+ts。鉴权组件(超简单,有手就会)

现在需要对antd的 Button组件进行二次封装。来实现鉴权功能(即传入权限信息id,权限信息为true的才会渲染到页面)

本来是很简单的事情,但是我发现antd的ts类型声明有够复杂,而且并不能直接拿过来用,你敢用就敢报错。

然后基于antd组件库5.1.4的Button按钮的API,傻瓜式的一个一个加ts的类型声明。

最后,实在受不了这样的傻瓜式代码,最终进行的优化。

1.jpg

详情:antd组件库Button

一:一开始图省事,直接简单封装:

import React, { ReactNode } from "react";
import { Button } from "antd";
import { useSelector } from "react-redux";
import { RootState } from "@/store";

type Props = {
  // 拿到所有传入进来的所有标签属性
  [x: string]: any;
  children?: ReactNode;
  id: number;
};


// ...rest表示结构除了 children 和 id 之外的所有传入的标签属性
function AuthButton({ children, id, ...rest }: Props) {

  // 从仓库中获取按钮权限信息,这里我的权限信息经过整理,以及过滤了 权限不位true的(即:在这个数组里面存在的信息,就是有权限的)
  const buttonArr = useSelector(
    (state: RootState) => state.loginStore.authButtonArr
  );

  // 当你使用按钮的时候传递过来的id,能在权限信息里面找到的时候(或者你传-1的时候,这个-1的逻辑不需要可以删掉)才渲染,否则渲染null(即:不渲染任何元素)
  return buttonArr.some((v: any) => v.id === id) || id === -1 ? (
    // 把传入的标签属性给 antd 的Button组件
    <Button {...rest}>{children}</Button>
  ) : null;
}

// 使用 React.memo 来优化组件,避免组件的无效更新,类似 类组件里面的PureComponent
const MemoAuthButton = React.memo(AuthButton);

export default MemoAuthButton;

二:使用

              <AuthButton
                id={102}
                type="text"
                danger
                onClick={() => addObject(item.id)}
              >
                编辑
              </AuthButton>

确实可以使用,也不会报错。但是在使用的时候没有字段提示。

输入ty,不会自动提示type,后面的text也不会自己补全

(使用ts我不能允许这样的事情发生,不然使用ts干嘛!)

三:使用 ts 的 interface 接口 继承 antd 的 Button 按钮 类型

鼠标移入 antd 的 Button 组件,显示类型为 CompoundedComponent

1.jpg

于是:导入 CompoundedComponent,并且继承

import { CompoundedComponent } from "antd/es/float-button/interface";

interface Props extends CompoundedComponent {
  // 拿到所有传入进来的所有标签属性
  [x: string]: any;
  children?: ReactNode;
  id: number;
};

然后:坑爹的地方来了,antd的封装,并不是我们简单理解的类似原生元素的封装,继承之后直接使用,你敢用就敢报错

1.jpg

于是我又开始研究 antd 封装的关于 Button 组件的TS类型源码

以下是源码内容

import * as React from 'react';
import type { SizeType } from '../config-provider/SizeContext';
import Group from './button-group';
declare const ButtonTypes: ["default", "primary", "ghost", "dashed", "link", "text"];
export type ButtonType = typeof ButtonTypes[number];
declare const ButtonShapes: ["default", "circle", "round"];
export type ButtonShape = typeof ButtonShapes[number];
declare const ButtonHTMLTypes: ["submit", "button", "reset"];
export type ButtonHTMLType = typeof ButtonHTMLTypes[number];
export type LegacyButtonType = ButtonType | 'danger';
export declare function convertLegacyProps(type?: LegacyButtonType): ButtonProps;
export interface BaseButtonProps {
    type?: ButtonType;
    icon?: React.ReactNode;
    /**
     * Shape of Button
     *
     * @default default
     */
    shape?: ButtonShape;
    size?: SizeType;
    disabled?: boolean;
    loading?: boolean | {
        delay?: number;
    };
    prefixCls?: string;
    className?: string;
    ghost?: boolean;
    danger?: boolean;
    block?: boolean;
    children?: React.ReactNode;
}
export type AnchorButtonProps = {
    href: string;
    target?: string;
    onClick?: React.MouseEventHandler<HTMLElement>;
} & BaseButtonProps & Omit<React.AnchorHTMLAttributes<any>, 'type' | 'onClick'>;
export type NativeButtonProps = {
    htmlType?: ButtonHTMLType;
    onClick?: React.MouseEventHandler<HTMLElement>;
} & BaseButtonProps & Omit<React.ButtonHTMLAttributes<any>, 'type' | 'onClick'>;
export type ButtonProps = Partial<AnchorButtonProps & NativeButtonProps>;
type CompoundedComponent = React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLElement>> & {
    Group: typeof Group;
};
declare const Button: CompoundedComponent;
export default Button;

发现,里里外外封装了好多层,可能是考虑到其他组件的复用。

当时脑子短路了,然后因为项目比较急,用了傻瓜式的办法:

四:直接把 antd 关于 Button 的所有属性,自己来定义。

import React, { ReactNode } from "react";
import { Button } from "antd";
import { useSelector } from "react-redux";
import { RootState } from "@/store";

type Props = {
  children?: ReactNode;
  id: number;
  danger?: boolean; //设置危险按钮
  block?: boolean; //将按钮宽度调整为其父宽度的选项
  disabled?: boolean; //设置按钮失效状态
  ghost?: boolean; //幽灵属性,使按钮背景透明
  href?: string; //点击跳转的地址,指定此属性 button 的行为和 a 链接一致
  htmlType?: "button" | "submit" | "reset" | undefined; //设置 button 原生的 type 值,可选值请参考 HTML 标准
  icon?: ReactNode; //设置按钮的图标组件
  loading?: boolean | { delay: number }; //设置按钮载入状态
  shape?: "default" | "circle" | "round"; //设置按钮形状
  size?: "large" | "middle" | "small"; //设置按钮大小
  target?: string; //相当于 a 链接的 target 属性,href 存在时生效
  type?: "primary" | "ghost" | "dashed" | "link" | "text" | "default"; //设置按钮类型
  onClick?: () => void; //点击按钮时的回调
};

// console.log('有权限的按钮集合',buttonArr);

function AuthButton({
  children,
  id,
  danger = false,
  block = false,
  disabled = false,
  ghost = false,
  href,
  htmlType = "button",
  icon,
  loading = false,
  shape = "default",
  size = "middle",
  target,
  type = "default",
  onClick,
}: Props) {
  const buttonArr = useSelector(
    (state: RootState) => state.loginStore.authButtonArr
  );

  return buttonArr.some((v: any) => v.id === id) || id === -1 ? (
    <Button
      danger={danger}
      block={block}
      disabled={disabled}
      ghost={ghost}
      href={href}
      htmlType={htmlType}
      icon={icon}
      loading={loading}
      shape={shape}
      size={size}
      target={target}
      type={type}
      onClick={onClick}
    >
      {children}
    </Button>
  ) : null;
}

const MemoAuthButton = React.memo(AuthButton);

export default MemoAuthButton;

确实完美解决。但是写这种代码我不能忍啊!真的不能忍!

在项目交付初版之后,闲下来又研究了一下 antd ButtonProps 的源码,然后发现我们只需要拿 ButtonProps 这个接口来继承就可以了(当时怎么没注意…………)

于是:

五:接口继承于 ButtonProps

import React, { ReactNode } from "react";
import { Button, ButtonProps } from "antd";
import { useSelector } from "react-redux";
import { RootState } from "@/store";


interface Props extends ButtonProps {
  children?: ReactNode;
  id: number;
  [x: string]: any;
}


function AuthButton({ children, id, ...rest }: Props) {
  const buttonArr = useSelector(
    (state: RootState) => state.loginStore.authButtonArr
  );

  return buttonArr.some((v: any) => v.id === id) || id === -1 ? (
    <Button {...rest}>{children}</Button>
  ) : null;
}

const MemoAuthButton = React.memo(AuthButton);

export default MemoAuthButton;

然后,悲剧的又报错了,原因是 antd 里面的 ButtonProps 接口 已经定义过 id 这个属性。我这里又重复定义,所以会报错。

1.jpg

这里最简单的解决办法就是 把自己定义的参数 id 修改为其他,比如:Aid,就能解决

1.jpg

但是!!问题又来了,这样修改之后,我在页面所有用到的地方全部都要修改之前传递的参数 id 改成 Aid。我不能接受!(如果是在项目初期,还没有使用这个组件传递 id 参数的话就没关系,可以不用往下看了。)

最后:最终优化版来了。

六:保留自定义 id 参数的接口继承

使用泛型来覆盖 ButtonProps 里定义的 id 属性

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

interface Props extends Omit<ButtonProps, "id"> {
  children?: ReactNode;
  id: number;
  [x: string]: any;
}

完整代码:

import React, { ReactNode } from "react";
import { Button, ButtonProps } from "antd";
import { useSelector } from "react-redux";
import { RootState } from "@/store";

// 定义一个接口继承,过滤重复的字段id
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

// 过滤接口 ButtonProps 里面的字段id,使用自己定义的 id
interface Props extends Omit<ButtonProps, "id"> {
  children?: ReactNode;
  id: number;
  [x: string]: any;
}


function AuthButton({ children, id, ...rest }: Props) {
  const buttonArr = useSelector(
    (state: RootState) => state.loginStore.authButtonArr
  );

  return buttonArr.some((v: any) => v.id === id) || id === -1 ? (
    <Button {...rest}>{children}</Button>
  ) : null;
}

const MemoAuthButton = React.memo(AuthButton);

export default MemoAuthButton;

终于舒服了~,启动项目,完美,没有报错,鉴权功能也完美。