PasswordValidation 密码校验组件实现与详解

一、组件介绍

PasswordValidation 是一个基于 React 和 Ant Design 的通用密码强度校验与规则提示组件。它能够实时校验用户输入的密码是否符合企业安全要求,并以弹窗形式直观展示校验规则及当前状态。组件支持与 antd Form 无缝联动,便于在注册、修改密码等场景中复用。

二、使用场景

  • 用户注册、修改密码、重置密码等需要密码输入的表单
  • 需要统一密码强度策略的企业级后台系统
  • 需要实时提示密码合规性的前端页面
  • 需要与 Ant Design Form 组件集成的项目

三、核心思路

  • 规则配置:将密码校验规则(长度、字符类型、多样性)抽象为常量配置,便于维护和扩展。
  • 实时校验:每次输入变更时,自动校验所有规则,并将结果同步到表单校验状态。
  • 规则提示:通过 Popover 弹窗,实时展示每条规则的通过情况,提升用户体验。
  • 表单联动:支持与 antd Form 组件联动,自动设置字段校验状态和错误提示。
  • ref 暴露:通过 ref 暴露校验方法,便于外部自定义校验逻辑。

四、关键代码实现

1. 规则配置与校验
js 复制代码
const PASSWORD_RULES = {
  length: {
    min: 8,
    max: 20,
    test: (password: string) => password.length >= 8 && password.length <= 20
  },

  chars: {
    pattern: /^[a-zA-Z0-9!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~]+$/,
    test: (password: string) => PASSWORD_RULES.chars.pattern.test(password) && !/\\s/.test(password)
  },

  variety: {
    minTypes: 3,
    test: (password: string) => {
      let types = 0;
      if (/[A-Z]/.test(password)) types++;
      if (/[a-z]/.test(password)) types++;
      if (/[0-9]/.test(password)) types++;
      if (/[!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~]/.test(password)) types++;
      return types >= PASSWORD_RULES.variety.minTypes;
    }
  }
};
2. 核心实现
js 复制代码
export const PasswordValidation = forwardRef<WithPasswordRulesRef, WithPasswordRulesProps>(({
  children, 
  form, 
  value = '', 
  name, 
  trigger = 'hover', 
  placement = 'bottomLeft', 
  title, 
  ...restProps
}, ref) => {
  const validationsRef = useRef({ length: false, chars: false, variety: false });
  
  const validatePassword = useCallback((password: string) => ({
    length: PASSWORD_RULES.length.test(password),
    chars: PASSWORD_RULES.chars.test(password),
    variety: PASSWORD_RULES.variety.test(password)
  }), []);

  const updateFieldStatus = useCallback((isValid: boolean) => {
    if (!name || !form) return;
    if (!value) {
      form.setFields([{ name, errors: ['密码不能为空'], validating: false, touched: true }]);
    } else if (!isValid) {
      form.setFields([{ name, errors: ['请按正确格式输入密 码'], validating: false, touched: true }]);
    } else {
      form.setFields([{ name, errors: undefined, validating: false }]);
    }
  }, [name, form, value]);

  const passwordRulesContent = useMemo(() => {
    const validations = validatePassword(value);
    const rules = [
      { valid: validations.length, text: '8-20个字符', key: 'length' },
      { valid: validations.chars, text: '只能包含大小写字母、数字以及标点符号(除空格)', key: 'chars' },
      { valid: validations.variety, text: '大写字母、小写字母、数字和标点符号至少包含3种', key: 'variety' }
    ];

    return (
      <div className={styles.container}>
        {rules.map(rule => (
          <div className={styles.item} key={rule.key}>
            <CheckCircleOutlined className={classnames(styles.icon, { [styles.success]: rule.valid })} />
            <Text className={classnames(styles.text, { [styles.success]: rule.valid })}>{rule.text}</Text>
          </div>
        ))}
      </div>
    );
  }, [value, validatePassword]);

  const handleValidator = useCallback(async () => {
    const isValid = Object.values(validationsRef.current).every(valid => valid);
    if (!isValid) {
      return Promise.reject('请按正确格式输入密码');
    }
    return Promise.resolve();
  }, []);
  
  useEffect(() => {
    validationsRef.current = validatePassword(value);
    const isValid = Object.values(validationsRef.current).every(valid => valid);
    updateFieldStatus(isValid);
  }, [value, validatePassword, updateFieldStatus]);

  // 处理子组件焦点
  const handleChildFocus = useCallback((originalOnFocus?: (e: React.FocusEvent<HTMLInputElement>) => void) => {
    return (e: React.FocusEvent<HTMLInputElement>) => {
      originalOnFocus?.(e);
      if (form) {
        const isValid = Object.values(validationsRef.current).every(valid => valid);
        updateFieldStatus(isValid);
      }
    };
  }, [form, updateFieldStatus]);

  // 克隆子组件
  const childrenWithProps = useMemo(() => {
    return React.cloneElement(children, {
      onFocus: handleChildFocus(children.props.onFocus)
    });
  }, [children, handleChildFocus]);

  useImperativeHandle(ref, () => ({
    getValidations: () => validationsRef.current,
    handleValidator
  }), [handleValidator]);

  return (
    <Popover
      content={passwordRulesContent}
      trigger={trigger}
      placement={placement}
      title={title || '密码需满足以下要求'}
      overlayClassName={styles.password_popover}
      getPopupContainer={triggerNode => triggerNode.parentNode as HTMLElement}
      {...restProps}
    >
      {childrenWithProps}
    </Popover>
  );
});
3. 基础使用
js 复制代码
<Form.Item
  label="设置密码"
  name="password"
  rules={[
    { required: true, message: '请输入密码' },
    {
      validator: async (_, value) => {
        if (passwordRef.current) {
          await passwordRef.current.handleValidator();
        }
      }
    }
  ]}
>
  <PasswordValidation ref={passwordRef} form={form}>
    <Input.Password placeholder="请输入密码" />
  </PasswordValidation>
</Form.Item>

五、注意事项

  • 与 antd Form 联动时,必须传入 form 和 name 属性,否则无法自动设置表单校验状态。
  • 建议外部受控 value,组件内部不维护 value 状态。
  • Popover 默认 hover 触发,可通过 trigger 属性自定义。
  • 如需多语言或自定义文案,可自行扩展。
  • 如需自定义样式,可通过 overlayClassName 传递 className。
  • 如需自定义校验规则,可修改 PASSWORD_RULES 配置。

六、总结

PasswordValidation 组件极大简化了密码强度校验和规则提示的开发工作,支持灵活扩展和与 antd Form 的无缝集成。它不仅提升了用户体验,也保证了密码安全策略的统一落地。推荐在所有涉及密码输入的场景中使用。如需多语言、弱密码黑名单等高级功能,可在此基础上二次开发。

相关推荐
烛阴几秒前
Python模块热重载黑科技:告别重启,代码更新如丝般顺滑!
前端·python
吉吉611 小时前
Xss-labs攻关1-8
前端·xss
拾光拾趣录1 小时前
HTML行内元素与块级元素
前端·css·html
小飞悟1 小时前
JavaScript 数组精讲:创建与遍历全解析
前端·javascript
喝拿铁写前端1 小时前
技术是决策与代价的平衡 —— 超大系统从 Vue 2 向 Vue 3 演进的思考
前端·vue.js·架构
拾光拾趣录1 小时前
虚拟滚动 + 加载:让万级列表丝般顺滑
前端·javascript
然我1 小时前
数组的创建与遍历:从入门到精通,这些坑你踩过吗? 🧐
前端·javascript·面试
豆豆(设计前端)2 小时前
如何成为高级前端开发者:系统化成长路径。
前端·javascript·vue.js·面试·electron
今天你写算法了吗2 小时前
ScratchCard刮刮卡交互元素的实现
前端·javascript
谢尔登2 小时前
【React Native】布局和 Stack 、Slot
javascript·react native·react.js