uniapp 里写表单校验太麻烦?我把这套 useForm 提取成了公共方法

2026-08-10 0 322

你有过这种经历吗?每个表单页面都要写上几十行 if 判断来校验用户输入,还得手动控制提示文字,改一个字段名就要全局搜索替换。上个月我在 uniapp 项目里写了三个差不多的表单页面,每页都有手机号、验证码、同意协议这种组合,代码几乎复制粘贴。一时没忍住,就自己写了一个基于 Vue3 组合式 API 的 `useForm` 校验工具,从此表单验证变得轻松多了。

今天把思路和代码拆出来,虽然不是特别复杂,但能帮你在项目里统一表单校验逻辑,少写几百行重复代码。

首先,先看一段原始的表单校验代码

假设这是一个登录页,需要校验手机号和密码:

<template>
  <view>
    <input v-model="phone" placeholder="手机号" />
    <input v-model="password" type="password" placeholder="密码" />
    <button @tap="submit">登录</button>
  </view>
</template>

<script setup>
import { ref } from 'vue'

const phone = ref('')
const password = ref('')

function submit() {
  if (!phone.value) {
    uni.showToast({ title: '请输入手机号', icon: 'none' })
    return
  }
  if (!/^1[3-9]d{9}$/.test(phone.value)) {
    uni.showToast({ title: '手机号格式不正确', icon: 'none' })
    return
  }
  if (!password.value) {
    uni.showToast({ title: '请输入密码', icon: 'none' })
    return
  }
  if (password.value.length < 6) {
    uni.showToast({ title: '密码至少6位', icon: 'none' })
    return
  }

  // TODO 发送登录请求
}
</script>

以后再加一个“确认密码”和“同意协议”,你就会发现代码里全是重复的 if 和 return。校验规则一旦变动,比如手机号号段增加一个,你得在一个个页面里搜索。这种写法在页面少的时候还能忍,多了真是折磨。

核心思路:把校验规则抽离成数据

我的想法很简单:把每个字段的校验规则写成一个配置对象,然后用一个统一的 `validate` 方法遍历规则,遇到错误就返回第一条错误信息。页面里只需要调用 `validate()` 就能拿到是否通过,如果没通过,直接用 toast 报错。

这其实就是把命令式的 if 判断转换为声明式的规则描述。所以第一步,先定义一个规则对象的结构:

const rules = {
  phone: [
    { required: true, message: '请输入手机号' },
    { pattern: /^1[3-9]d{9}$/, message: '手机号格式不正确' }
  ],
  password: [
    { required: true, message: '请输入密码' },
    { minLength: 6, message: '密码至少6位' }
  ]
}

这个结构非常直观。`phone` 字段对应一个数组,数组里每个对象就是一个校验规则,`message` 是失败时的提示信息。你可以扩展任何规则,比如 `min`、`max`、`validator` 函数等。

然后我写一个通用校验函数,传入字段值以及这个字段的所有规则,返回错误信息或空字符串:

function validateField(value, ruleList) {
  for (const rule of ruleList) {
    if (rule.required && (value === undefined || value === null || String(value).trim() === '')) {
      return rule.message
    }
    if (rule.pattern && !rule.pattern.test(value)) {
      return rule.message
    }
    if (rule.minLength && value.length  rule.maxLength) {
      return rule.message
    }
    if (typeof rule.validator === 'function') {
      const result = rule.validator(value)
      if (result !== true) {
        return result || rule.message
      }
    }
  }
  return ''
}

这里只是演示几种常见规则,你可以按项目需求继续扩展。

把校验方法封装成 useForm

光有校验规则还不够,我需要把它和页面里的表单数据关联起来。Vue3 的组合式函数正好适合做这件事。

我创建一个 `useForm.js` 文件,核心代码如下:

import { reactive, toRefs } from 'vue'

// 模拟 uni.showToast,方便测试
function showToast(message) {
  uni.showToast({
    title: message,
    icon: 'none',
    duration: 2000
  })
}

export function useForm(formData, rules) {
  // 表单数据
  const state = reactive({
    values: formData,
    errors: {}
  })

  // 校验单个字段
  const validateField = (field) => {
    const value = state.values[field]
    const ruleList = rules[field]
    if (!ruleList) return true

    const message = checkValue(value, ruleList)
    state.errors[field] = message
    return !message
  }

  // 校验整个表单
  const validate = () => {
    const fields = Object.keys(rules)
    let firstErrorMessage = ''

    for (const field of fields) {
      const isValid = validateField(field)
      if (!isValid && !firstErrorMessage) {
        firstErrorMessage = state.errors[field]
      }
    }

    if (firstErrorMessage) {
      showToast(firstErrorMessage)
      return false
    }
    return true
  }

  // 校验过程中如果用户修改某个字段,清除对应的错误信息
  const clearError = (field) => {
    if (state.errors[field]) {
      delete state.errors[field]
    }
  }

  // 为了防止多个表单实例混在一起,这里返回一个绑定到当前表单的方法
  return {
    values: toRefs(state.values), // 如果需要在模板中 v-model 使用,也可以用 toRefs
    errors: state.errors,
    validate,
    validateField,
    clearError
  }
}

这里有个需要注意的点,`formData` 我直接用 `reactive` 包起来了,所以初始化必须是对象。然后返回的值中 `values` 用 `toRefs` 解构,让你在模板里能直接使用 `phone` 这样的响应式属性。

不过上面我写的 `checkValue` 函数还没实现,别急,我把它一起补上:

function checkValue(value, ruleList) {
  for (const rule of ruleList) {
    if (rule.required && (value === undefined || value === null || String(value).trim() === '')) {
      return rule.message
    }
    if (rule.pattern && !rule.pattern.test(String(value))) {
      return rule.message
    }
    if (rule.minLength !== undefined && String(value).length  rule.maxLength) {
      return rule.message
    }
    if (typeof rule.validator === 'function') {
      const res = rule.validator(value)
      if (res !== true) {
        return res || rule.message
      }
    }
  }
  return ''
}

至此,`useForm` 的核心功能已经完成。接下来把它用在页面里。

在登录页里使用 useForm

改造后的登录页面如下:

<template>
  <view>
    <input v-model="phone" placeholder="手机号" type="text" />
    <input v-model="password" type="password" placeholder="密码" />
    <button @tap="submit">登录</button>
  </view>
</template>

<script setup>
import { useForm } from '@/utils/useForm'

// 初始化要校验的字段
const rules = {
  phone: [
    { required: true, message: '请输入手机号' },
    { pattern: /^1[3-9]d{9}$/, message: '手机号格式不正确' }
  ],
  password: [
    { required: true, message: '请输入密码' },
    { minLength: 6, message: '密码至少6位' }
  ]
}

const { values, errors, validate } = useForm({
  phone: '',
  password: ''
}, rules)

const { phone, password } = values

function submit() {
  if (validate()) {
    // 校验通过,执行登录逻辑
    uni.showLoading({ title: '登录中' })
    // 模拟请求
    setTimeout(() => {
      uni.hideLoading()
      uni.showToast({ title: '登录成功', icon: 'success' })
    }, 500)
  }
}
</script>

这里我把 `phone` 和 `password` 从 `values` 里解构出来,直接用 `v-model=”phone”` 就能双向绑定,因为 `values` 是响应式对象,解构出来的属性依然是响应式的引用。注意不能这样写:

const { phone } = values

这样 `phone` 是一个固定值,不会自动更新。所以必须用 `toRefs` 或者直接使用 `values.phone`。上面我使用 `const { phone, password } = values` 实际上解构的是 refs。在 `useForm` 中返回的 `values` 是通过 `toRefs` 转换过的,所以 `phone` 是 ref 对象,在模板中自动解包,在 script 中要 `phone.value`。同样,方法内可以正常使用。

由于我在模板里写了 `v-model=”phone”`,这事就自动完成了。现在当你点击登录时,`validate()` 会依次检查所有字段,如果有错,它会在内部调用 `uni.showToast`,返回 `false`;只有全部通过时才返回 `true`,继续执行登录。

这样,表单校验逻辑完全从业务代码中剥离,以后想要修改提示语,只需要在 rules 里改,不用再动 `submit` 函数。

提高扩展性:加入自定义校验函数

有些字段的校验光靠 patterns 和 minLength 不够,比如需要验证两次密码是否一致。这可以用 `validator` 函数解决。看一个注册页面的例子:

const form = useForm({
  username: '',
  password: '',
  confirmPassword: ''
}, {
  username: [
    { required: true, message: '请填写用户名' },
    { minLength: 3, message: '用户名至少3个字符' }
  ],
  password: [
    { required: true, message: '请填写密码' },
    { minLength: 6, message: '密码至少6位' }
  ],
  confirmPassword: [
    { required: true, message: '请再次输入密码' },
    {
      validator: (value) => {
        if (value !== form.values.password.value) {
          return '两次密码输入不一致'
        }
        return true
      }
    }
  ]
})

由于 `useForm` 内部提供了一个 `form.values`,你可以在校验函数里访问其他字段的值。注意上面 `form.values.password` 是 ref 对象,所以要加 `.value`。如果不喜欢 `.value`,也可以在规则定义前先提取 `const passwordRef = ref(”)`,然后直接使用 `passwordRef.value`。

这种写法非常灵活,几乎能覆盖所有场景。而且它只返回一个字符串,如果返回的是 `true` 则通过,其他任意字符串都会作为错误提示显示。

进阶:在输入框交互时清除错误提示

一个好的表单体验是:当用户开始修改某个字段,该字段的错误提示应该立即消失。我可以在 `useForm` 里暴露 `clearError` 方法,然后在页面里监听 input 事件。但为了更简单,我直接给输入框绑定一个 `@input` 事件:

<input v-model="phone" @input="clearError('phone')" placeholder="手机号" />

这样当用户输入第一个字时,错误信息就会被移除。你可以配合一个用于显示错误文字的小标签:

<view v-if="errors.phone" class="error">{{ errors.phone }}</view>

但为了不破坏模板,我给 `useForm` 增加一个 `setErrorsField` 方法,或者更直接,在 `validateField` 里自动清除旧的错误。因为我们在 `validateField` 里已经在每次校验时为新错误赋值,如果校验通过,我们直接把该字段的 `error` 删除即可。修改 `validateField` 实现:

const validateField = (field) => {
  const value = state.values[field]
  const ruleList = rules[field]
  if (!ruleList) {
    // 没有规则,默认清除错误
    delete state.errors[field]
    return true
  }

  const message = checkValue(value, ruleList)
  if (message) {
    state.errors[field] = message
  } else {
    delete state.errors[field]
  }
  return !message
}

这样当你调用 `validateField` 时,它会自动更新错误状态,不需要手动清除。你只需要在输入框上绑定 `@blur=”validateField(‘phone’)”`,失焦时校验该字段,效果立刻就有了。

解决多实例之间共享状态的问题

一开始我写了一个全局单例,结果发现两个页面同时使用时会相互污染。所以我在 `useForm` 内部使用了 `reactive` 创建独立的状态,每个表单实例都有自己的 `state`,完全隔离。这是组合式函数的一大优势,你只管调用,它会自己管理作用域。

还有一点,如果你在多个页面同时引入 `useForm`,它不会出现 Vuex 那种全局状态互相干扰的问题。因为内部数据是函数作用域内的局部变量,每次调用 `useForm` 都会生成新的 `state`。

额外功能:返回错误字段并高亮聚焦

校验失败时,如果能把滚动条滚动到第一个出错的输入框并聚焦,用户体验会更好。在 uni-app 里这可以通过 `uni.pageScrollTo` 和 selector 实现。我们可以让 `validate` 返回第一个出错的字段名,然后在页面中处理:

const validate = () => {
  const fields = Object.keys(rules)
  let firstErrorField = ''
  let firstMessage = ''

  for (const field of fields) {
    const message = checkValue(state.values[field], rules[field])
    state.errors[field] = message
    if (message && !firstErrorField) {
      firstErrorField = field
      firstMessage = message
    }
  }

  if (firstErrorField) {
    showToast(firstMessage)
    // 可以通过 id 或 ref 聚焦
    // #ifdef H5
    // document.getElementById(`field-${firstErrorField}`)?.focus()
    // #endif
    return { valid: false, field: firstErrorField }
  }
  return { valid: true }
}

然后在页面里给需要聚焦的输入框加上 `id`,比如 `:id=”‘field-phone'”`。在 `validate` 返回有错误后,调用 `uni.pageScrollTo` 滚动到对应位置,再手动调 `uni.createSelectorQuery()` 拿到节点上下文并调用 `.focus()`。不过这个操作有些繁琐,而且小程序里需要 `focus` 属性,不是所有平台都支持。为了保持代码统一,我在项目里只做了 toast 提示,没有做自动聚焦,目前也够用。

如果你需要这个功能,可以单独去实现,思路就是通过 `uni.createSelectorQuery().select(‘#field-‘ + field).boundingClientRect()` 获取位置,然后 `uni.pageScrollTo({scrollTop})`。

这个 useForm 还能用在那些场景?

不光是登录和注册,任何需要收集表单数据的页面都可以用。比如填写收货地址、发布文章、实名认证等等。我最爽的是在几个需要多类型校验的页面里,直接定义 rules,然后调用 `validate()`,再也不用一个字段一个字段地写了。

如果你有特殊校验逻辑,比如检查用户名是否已存在,可以在 validator 里直接发起异步请求,但要注意返回值是个 Promise。这里为了简单,我们没有做异步校验。不过可以在页面里先校验当前字段,通过后再单独发送异步请求。

比如这样改:

validator: async (value) => {
  const res = await api.checkUsername(value)
  return res.exists ? '用户名已被占用' : true
}

然后在 `validateField` 里对 validator 返回的 Promise 做处理。但为了不把 `useForm` 的同步逻辑复杂化,我一般会把异步校验单独拿出去,在页面里用额外的方法调用。毕竟同步校验已经覆盖了 80% 的场景。

总结:亲手造轮子其实也挺好

这个 `useForm` 不同于第三方插件,它就是一套非常契合 uniapp + Vue3 的轻量方案。虽然它没有像 VeeValidate 那样强大,但它让我理解了表单校验的本质:规则驱动、统一报错、自动清除。

从最开始的复制粘贴,到后来只需传 rules 就能完成校验,维护起来轻松多了。如果你也在用 uniapp 和 Vue3,不妨把这段逻辑抽出来,根据自己的业务场景扩展一下,也许就是你想要的表单解决方案。

如果之后有空,我还会给它加上异步校验、字段联动、自动聚焦等功能。今天这个版本已经让我的项目里少了几百行 if,也让我更加喜欢组合式 API 了。

uniapp 里写表单校验太麻烦?我把这套 useForm 提取成了公共方法
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp uniapp 里写表单校验太麻烦?我把这套 useForm 提取成了公共方法 https://www.taomawang.com/web/uniapp/2519.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务