造了个简易版的表单校验器轮子

137 阅读2分钟

在工作中往往都是使用第三方UI库来做表单功能,这次在使用 vue3+tsx 重写记账项目的过程中打算通过原生form标签来实现表单校验功能。

前期准备

  1. 一个🌰:校验 name 和 remark 这两个字段,失败显示对应的校验失败原因
        <form>
          <div>
            <label>
              <span>name</span>
              <input/>
            </label>
          </div>
          <div>
            <label>
              <span>remark</span>
              <input/>
            </label>
          </div>
          <div>
            <button>确定</button>
          </div>
        </form>
  1. 确定校验规则
  • name 为必填
  • remark 为必填并且最多只能输入7个字符
  1. 确定触发表单提交校验的方式 通过监听表单的onSubmit事件

为什么不监听按钮的点击事件???

不一定通过点击按钮提交 ,提交也可通过回车、键盘事件触发 所以不要只监听鼠标的点击事件来提交。

  1. 校验和form组件分离 有些UI库是将校验写在了form组件中,不打算使用这种方案,希望各自做各自的活。

具体实现

  1. 定义关系
const errors= validate(formData, rules)

向 validate 函数传入表单数据以及校验规则,返回错误信息。

  1. 定义结构
  • rules是数组而不是对象,因为需要具有先后校验的顺序,数组里面具体描述校验的每一项规则并且定义错误提示。
  • errors是对象结构,key为校验的字段,展示错误信息
const formData = reactive({
      name: "",
      sign: "",
    });
const rules= [
        { key: "name",
          type: "required", 
          message: "必填" 
        },
        {
          key: "remark",
          type: "pattern",
          regex: /^.{1,7}$/,
          message: "只能填 1 到 7 个字符",
        },
      ];
const errors = reactive({
      name: ['错误1','错误2'],
      mark: ['错误1']
    });
  1. validate 逻辑

将传入的rules遍历通过校验类型来做逻辑判断校验是否失败,失败则将message以对象形式放入errors中,将errors暴露给外部使用。

   const errors = {}
   rules.map(rule => {
      const { key, type, message } = rule
      const value = formData[key]
      switch (type) {
        case 'required':
          if (value === null || value === undefined || value === '') {
            errors[key] = errors[key] ?? []
            errors[key]?.push(message)
          }
          break;
        case 'pattern':
          if (value && !rule.regex.test(value.toString())) {
            errors[key] = errors[key] ?? []
            errors[key]?.push(message)
          }
          break;
        default:
          return
      }
    })
    return errors
  1. 在提交方法中使用它
const errors = {}
// 清空上一次的错误记录
Object.assign(errors, {
  name: undefined,
  remark: undefined
})
// 获取到新的校验失败内容
Object.assign(errors, validate(formData, rules))
// 表单事件onsubmit默认会刷新页面,阻止页面刷新
e.preventDefault()

typescript 小课堂

  1. type 不可直接循环引用自身 🥲, interface 可以做到!

定义 formData 类型

// error
type XData = Record<string, string | number | null | undefined | XData>;
// success
interface FData {
    [k: string]: string | number | null | undefined | FData
  }
  1. rules 只有 required 以及 pattern 两种规则,ts使用|实现2选1,测试居然两种规则可并存???
type XRule = {
  key: string;
  message: string;
} & ({ required: boolean } | { RegExp: RegExp });
// r 竟然可以同时拥有required和RegExp???
const r: XRule = {
  key: "name",
  message: "必填",
  required: true,
  RegExp: /^.{1,7}$/,
};

A | B 要实现2选1功能,A与B就必须有**互斥属性 **

type FRule = {
  key: string;
  message: string;
} & ({ type: "required" } | { type: "pattern"; regex: RegExp });

// 将报错
const r2: FRule = {
  key: "name",
  message: "必填",
  required: true, 
  RegExp: /^.{1,7}$/,
};

image.png

  1. 应用范型 Rules的key和errors的key都只能是FData中的key,如何将他们关联? 使用范型

让T属于FData

interface FData {
    [k: string]: string | number | null | undefined | FData
  }
  type Rule<T> = {
    key: keyof T
    message: string
  } & (
      { type: 'required' } |
      { type: 'pattern', regex: RegExp }
    )
  type Rules<T> = Rule<T>[]
  export type { Rules, Rule, FData }
  export const validate = <T extends FData>(formData: T, rules: Rules<T>) => {
    type Errors = {
      [k in keyof T]?: string[]
    }
    const errors: Errors = {}
  }

最终代码

validate.tsx

interface FData {
    [k: string]: string | number | null | undefined | FData
  }
  type Rule<T> = {
    key: keyof T
    message: string
  } & (
      { type: 'required' } |
      { type: 'pattern', regex: RegExp }
    )
  type Rules<T> = Rule<T>[]
  export type { Rules, Rule, FData }
  export const validate = <T extends FData>(formData: T, rules: Rules<T>) => {
    type Errors = {
      [k in keyof T]?: string[]
    }
    const errors: Errors = {}
    rules.map(rule => {
      const { key, type, message } = rule
      const value = formData[key]
      switch (type) {
        case 'required':
          if (value === null || value === undefined || value === '') {
            errors[key] = errors[key] ?? []
            errors[key]?.push(message)
          }
          break;
        case 'pattern':
          if (value && !rule.regex.test(value.toString())) {
            errors[key] = errors[key] ?? []
            errors[key]?.push(message)
          }
          break;
        default:
          return
      }
    })
    return errors
  }

Form.tsx

import { defineComponent, PropType, reactive } from 'vue';
import { Rules, validate } from '../../shared/validate';
export const Form = defineComponent({
  setup: (props, context) => {
    const formData = reactive({
      name: '',
      remark: '',
    })
    const errors = reactive<{ [k in keyof typeof formData]?: string[] }>({})
    const onSubmit = (e: Event) => {
      const rules: Rules<typeof formData> = [
        { key: 'name', type: 'required', message: '必填' },
        { key: 'name', type: 'pattern', regex: /^.{1,7}$/, message: '只能填 1 到 7个字符' },
        { key: 'remark', type: 'required', message: '必填' },
      ]
      // 清空上一次的错误记录
      Object.assign(errors, {
        name: undefined,
        remark: undefined
      })
      Object.assign(errors, validate(formData, rules))
      // 表单事件onsubmit默认会刷新页面,阻止页面刷新
      e.preventDefault()
    }
    return () => (
          <form onSubmit={onSubmit}>
            <div>
              <label>
                <span>name</span>
                  <input v-model={formData.name} />
                  <span>{errors['name'] ? errors['name'][0] : ' '}</span>
              </label>
            </div>
            <div>
              <label>
                <span>renark</span>
                  <input v-model={formData.remark} />
                  <span>{errors['remark'] ? errors['remark'][0] : ' '}</span>
              </label>
            </div>
            <button>确定</button>
          </form>
    )
  }
})