Situation 背景

  • 多人开发的老项目里面,很多地方都写了验证手机格式的需求,代码各有千秋、百花齐放
  • 实现:有的写在公共组件库里,有的是单独开发局部组件支持,有的直接手写不复用,有的抽离正则到utils再引入
  • 正则:正则校验也各有千秋,比如/^\d{11}/、/1\d10/、/1[2−9]\d9/、/^1\d{10}/、/^1[2-9]\d{9}/、/1\d10/、/1[2−9]\d9/、/^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$/等等。
  • 长度限制:有的地方限制maxLength=11,而有的地方没限制
  • 输入格式:有的地方满足334格式(181 2222 3333)手机号码输入, 而有的地方只允许输入纯数字

Target 目标

  • 实现一个易用的手机号码公共组件
  • 使用较为宽松的正则校验并与后端达成一致
  • 不再限制输入长度但会报错提示
  • 支持334的格式输入,并自动去除所有空格
  • 写好参数说明,方便开发使用
  • 尽可能的保持灵活,适用更多场景

Action 行动

import type {
  FormItemProps,
  InputProps,
  RulesProps,
} from '@arco-design/web-react';
import { Form, Input } from '@arco-design/web-react';
import { usePersistCallback } from '@byted/hooks';
import type { ReactNode } from 'react';
import { useMemo } from 'react';
export type VerifyInputProps = {
  /** 🔥 绑定属性 */
  field: string;
  /** 🔥 显示标签文字 */
  label: string;
  /** 标签前面是否显示必填*号 */
  labelRequired?: boolean;
  /** 🔥 rules是否加入必填校验(为true时无需再设置labelRequired=true) */
  phoneRequired?: boolean;
  /** 除检验手机号格式外其他规则,默认添加正则检验/^1[2-9]\d{9}$/ */
  rules?: RulesProps<any>[];
  /** 显示提示文字 */
  placeholder?: string;
  /** 是否禁用input */
  disabled?: boolean;
  /** phone输入框onChange事件 */
  onChange?: InputProps['onChange'];
  /** FormItem其他属性 */
  formItemProps?: FormItemProps;
  /** Input其他属性 */
  inputProps?: InputProps;
};
/** 手机号表单(带有 Form.Item) */
export const VerifyPhoneInput = (props: VerifyInputProps) => {
  const {
    field,
    label,
    placeholder,
    rules = [],
    labelRequired = false,
    phoneRequired = false,
    disabled,
    onChange,
    formItemProps,
    inputProps,
  } = props;
  const resultRules = useMemo(() => {
    const arr = [
      {
        validator(val: string | undefined, cb: (error?: ReactNode) => void) {
          const realVal = (`${val}` as any).replaceAll(' ', '');
          if (!val || /^1[2-9]\d{9}$/.test(realVal)) {
            cb();
          } else {
            cb(`请输入正确的${label}`);
          }
        },
      },
      ...rules,
    ];
    if (phoneRequired) {
      arr.unshift({
        validator(val: string | undefined, cb: (error?: ReactNode) => void) {
          if (!val) {
            cb(`请输入${label}`);
          } else {
            cb();
          }
        },
      });
    }
    return arr;
  }, [rules, phoneRequired, label]);
  const normalize = usePersistCallback((val: string | undefined) => {
    if (val) {
      const realVal = (`${val}` as any).replaceAll(' ', '');
      if (val !== realVal) {
        return realVal;
      }
    }
    return val;
  });
  const resultInputProps = {
    disabled,
    onChange,
    allowClear: true,
    placeholder: placeholder || `请输入${label}`,
    ...inputProps,
  };
  return (
    <Form.Item
      required={labelRequired || phoneRequired}
      field={field}
      label={label}
      formatter={normalize}
      rules={resultRules}
      {...formItemProps}
    >
      <Input {...resultInputProps} />
    </Form.Item>
  );
};
  • 定义入参类型VerifyInputProps,并按使用频率对属性排序并加以说明
  • 将是否必填从rules中移出变成phoneRequired参数,错误提示也根据label自动拼接请输入正确的${label},封装复杂、暴露简单
  • placeholder默认根据label自动拼接成(请输入${label}),提高易用性
  • 将FormItem其他属性收敛到formItemProps中,方便开发同学扩展,提高通用性
  • 默认allowClear=true,可以通过inputProps进行覆盖
  • 通过formatter自动去掉所有空格,支持334格式
  • 将Input其他属性收敛到inputProps中,方便开发同学扩展,提高通用性
  • 暴露rules,支持自定义远程验证等规则
  • 暴露常用的disabled、onChange在最外层方便使用

Result 结果

  • 已经投入使用,测试符合预期,提供给其他同事使用,后续继续完善

Review 复盘

  • 问题1:使用normalize去空格后,input下面的错误验证并没有重新触发导致输入框数据已经是对的,而错误提示还存在的现象。
    • 解决:第一种方案将form传入,在formatter之后通过form?.validate(field)来重新触发验证;第二种方案将rules改为validator的实现方式手动去空格。不希望引入更多参数,最终采用第二种。
  • 问题2:正则格式到底怎么定义才能平衡正确性和普适性
    • 解决:和后端协商使用适用目前所有可见场景下的最严格的验证格式/^1[2-9]\d{9}$/
  • 问题3:老代码治理
    • 解决:一次性将所有手机号码相关的代码全部改掉,需要测试介入带来大量的回归测试时间,而收益很小。所以新需求使用此组件,老需求改动的时候再改成此组件的方案代价更小。
  • 问题4:还能再丰富一哈吗?
    • 支持151****2222的显示与编辑时未修改不校验,保证手机号私密性
  • 问题5:VerifyPhoneInput实际是个FormItem组件,且api与FormItem原本的有很大区别,会不会给使用者带来心智负担?
    • 解决:
import type { FormItemProps, InputProps } from '@arco-design/web-react';
import { Form, Input } from '@arco-design/web-react';
import { usePersistCallback } from '@byted/hooks';
import type { ReactNode } from 'react';
import { useMemo } from 'react';
const defaultLabel = '手机号码';
export type PhoneFormItemProps = {
  /** 🔥 rules是否加入必填校验(为true时无需再设置labelRequired=true) */
  phoneRequired?: boolean;
  /** 🔥 rules是否加入正则校验(/^1[2-9]\d{9}$/),默认true */
  phoneFormatCheck?: boolean;
  /** Input其他属性 */
  inputProps?: InputProps;
} & FormItemProps;
/** 手机号表单(带有 Form.Item) */
export const PhoneFormItem = (props: PhoneFormItemProps) => {
  const {
    phoneRequired = false,
    phoneFormatCheck = true,
    inputProps,
    ...formItemProps
  } = props;
  const resultRules = useMemo(() => {
    const arr = [...(formItemProps.rules || [])];
    if (phoneFormatCheck) {
      arr.unshift({
        validator(val: string | undefined, cb: (error?: ReactNode) => void) {
          const realVal = (`${val}` as any).replaceAll(' ', '');
          if (!val || /^1[2-9]\d{9}$/.test(realVal)) {
            cb();
          } else {
            cb(`请输入正确的${formItemProps?.label || defaultLabel}`);
          }
        },
      });
    }
    if (phoneRequired) {
      arr.unshift({
        validator(val: string | undefined, cb: (error?: ReactNode) => void) {
          if (!val) {
            cb(`请输入${formItemProps.label || defaultLabel}`);
          } else {
            cb();
          }
        },
      });
    }
    return arr;
  }, [
    formItemProps.rules,
    formItemProps.label,
    phoneFormatCheck,
    phoneRequired,
  ]);
  const normalize = usePersistCallback((val: string | undefined) => {
    if (val) {
      const realVal = (`${val}` as any).replaceAll(' ', '');
      if (val !== realVal) {
        return realVal;
      }
    }
    return val;
  });
  const resultInputProps = {
    allowClear: true,
    ...inputProps,
    placeholder:
      inputProps?.placeholder || `请输入${formItemProps.label || defaultLabel}`,
  };
  return (
    <Form.Item
      normalize={normalize}
      rules={resultRules}
      {...formItemProps}
      required={formItemProps.required || phoneRequired}
    >
      <Input {...resultInputProps} />
    </Form.Item>
  );
};

以前都是用vue,来字节之后天天react,感觉自己变菜了好多,又或许我原本就菜的离谱。

以上就是字节封装React组件手机号自动校验格式FormItem的详细内容,更多关于React封装手机号校验格式的资料请关注阿兔在线工具其它相关文章!

点赞(0)

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部