如何在 Vue 3 + TypeScript 项目中构建一套可维护、可扩展、支持国际化的表单校验体系?本文分享一套经过实战检验的架构方案。
📖 前言
表单校验是前端开发中最常见却也最容易"写乱"的模块。你是否遇到过这些问题?
- 📌 校验规则散落在各个组件中,重复代码随处可见
- 📌 需求变更时要全局搜索,逐个修改正则或提示文案
- 📌 业务规则和基础规则混在一起,难以复用
- 📌 国际化支持困难,硬编码的中文难以维护
- 📌 新成员接手项目时,不知道该用哪个校验函数
本文将分享一套基于 配置驱动 + Vue 3 Composable 的表单校验架构,帮助你解决以上痛点。
🎯 设计目标
在动手之前,我们先明确架构设计的目标:
| 目标 | 描述 |
|---|---|
| 单一数据源 | 所有校验配置集中管理,避免分散 |
| 关注点分离 | 配置、逻辑、国际化各司其职 |
| 易于扩展 | 添加新规则只需修改配置 + 实现方法 |
| 类型安全 | 充分利用 TypeScript 提供类型提示 |
| 国际化友好 | 支持多语言,文案集中管理 |
| 开发体验好 | API 简洁直观,上手成本低 |
🏗️ 架构设计
整体架构图
┌─────────────────────────────────────────────────────────────┐
│ 业务组件 │
│ const { base, business, combined, preset } = useSimpleRules() │
└────────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ useSimpleRules (Composable) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ base │ │ business │ │ combined │ │ preset │ │
│ │ 基础规则 │ │ 业务规则 │ │ 组合规则 │ │ 预设规则 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼────────────┼────────────┼────────────┼──────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ validation.config.ts │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ BaseValidationConfig│ │BusinessValidationConfig│ │
│ │ patterns/ranges │ │ patterns/ranges │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ ┌─────────────────────────────────────────────┐ │
│ │ ValidationI18nKeys │ │
│ │ 国际化 key 映射(统一管理) │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ i18n/validation.ts │
│ 国际化文案(中/英/...) │
└─────────────────────────────────────────────────────────────┘
文件结构
src/
├── config/
│ └── validation.config.ts # 校验规则配置(正则、范围、i18n key)
├── composables/
│ └── validation/
│ └── useSimpleRules.ts # 校验规则 Composable
└── i18n/
└── zh/
└── validation.ts # 国际化文案
💡 核心实现
1. 配置层:validation.config.ts
配置层负责集中管理所有校验相关的"原材料":正则表达式、数值范围、长度限制、国际化 key。
/**
* 基础校验规则配置
*/
export const BaseValidationConfig = {
// 正则表达式
patterns: {
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
phone: /^1[3-9]\d{9}$/,
telephone: /^[0-9()()\-]+$/,
integer: /^-?\d+$/,
positiveInteger: /^\d+$/,
url: /^https?:\/\/.+/,
noChinese: /^[^\u4E00-\u9FA5]+$/,
username: /^\w+$/,
idCard: /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/,
},
// 数值范围
ranges: {
age: { min: 1, max: 150 },
port: { min: 1, max: 65535 },
percentage: { min: 0, max: 100 },
},
// 长度限制
lengths: {
username: { min: 3, max: 20 },
password: { min: 6, max: 20 },
name: { min: 2, max: 50 },
},
} as const
/**
* 业务校验规则配置(物流/航运领域)
*/
export const BusinessValidationConfig = {
patterns: {
bookingNo: /^[A-Z]{4}\d{6,10}$/, // 订舱号:4位字母 + 6-10位数字
containerNo: /^[A-Z]{4}\d{7}$/, // 集装箱号:4位字母 + 7位数字
hsCode: /^\d{8,10}$/, // HS编码:8-10位数字
blNo: /^[A-Z0-9]{10,20}$/, // 提单号
vesselVoyage: /^[A-Z0-9\s/-]+$/, // 船名航次
fax: /^[\d\-()]+$/, // 传真号
zipCode: /^\d{6}$/, // 邮编
noSpace: /^\S+$/, // 无空格
},
ranges: {
weight: { min: 0.01, max: 50000 }, // 重量(KG)
volume: { min: 0.01, max: 10000 }, // 体积(立方米)
quantity: { min: 1, max: 999999 }, // 数量
containerCount: { min: 1, max: 9999 }, // 集装箱数量
},
lengths: {
blNo: { min: 1, max: 10 },
companyName: { min: 1, max: 500 },
address: { min: 1, max: 1000 },
},
} as const
/**
* 验证提示信息配置(i18n key)
*/
export const ValidationI18nKeys = {
// 基础提示
required: 'validation.required',
requiredSelect: 'validation.requiredSelect',
// 格式提示
email: 'validation.email',
phone: 'validation.phone',
telephone: 'validation.telephone',
// 业务提示
bookingNo: 'validation.bookingNo',
containerNo: 'validation.containerNo',
weight: 'validation.weight',
// ... 更多 key
} as const
设计思考:
- 使用
as const断言,让 TypeScript 推导出字面量类型,获得更精确的类型提示 - 将基础配置和业务配置分开,便于不同项目复用基础部分
- 国际化 key 统一管理,避免硬编码字符串散落各处
2. 国际化层:validation.ts
/**
* 表单验证国际化配置 - 中文
* 使用扁平化 key 结构,与其他 i18n 文件保持一致
*/
export default {
// 基础提示
required: '此字段为必填项',
requiredSelect: '请选择',
requiredInput: '请输入',
// 格式提示
email: '请输入有效的邮箱地址',
phone: '请输入有效的手机号码',
telephone: '请输入有效的电话号码(支持数字、短横线、括号)',
url: '请输入有效的网址',
username: '用户名只能包含英文字母、数字和下划线',
// 类型提示
number: '请输入有效的数字',
integer: '请输入整数',
positiveNumber: '请输入正数',
noChinese: '不能输入中文',
// 范围提示(支持插值)
numberRange: '数值必须在 {min} - {max} 之间',
lengthRange: '请输入 {min} - {max} 个字符',
minLength: '最少输入 {min} 个字符',
maxLength: '最多输入 {max} 个字符',
// 业务提示
bookingNo: '订舱号格式不正确(4位字母 + 6-10位数字)',
containerNo: '集装箱号格式不正确(4位字母 + 7位数字)',
hsCode: 'HS编码格式不正确(8-10位数字)',
weight: '重量必须为正数且不超过 50000 KG',
volume: '体积必须为正数且不超过 10000 立方米',
}
3. Composable 层:useSimpleRules.ts
这是架构的核心,负责将配置转化为可直接使用的校验规则。
import type { FormItemRule } from 'element-plus'
import { useI18n } from 'vue-i18n'
import {
BaseValidationConfig,
BusinessValidationConfig,
ValidationI18nKeys,
} from '~/config/validation.config'
export function useSimpleRules() {
const { t } = useI18n()
// ==================== 基础校验规则 ====================
const base = {
/** 必填项 */
required: (message?: string): FormItemRule => ({
required: true,
message: message || t(ValidationI18nKeys.required),
trigger: 'blur',
}),
/** 下拉选择必选 */
requiredSelect: (message?: string): FormItemRule => ({
required: true,
message: message || t(ValidationI18nKeys.requiredSelect),
trigger: 'change',
}),
/** 邮箱格式 */
email: (message?: string): FormItemRule => ({
pattern: BaseValidationConfig.patterns.email,
message: message || t(ValidationI18nKeys.email),
trigger: 'blur',
}),
/** 手机号格式 */
phone: (message?: string): FormItemRule => ({
pattern: BaseValidationConfig.patterns.phone,
message: message || t(ValidationI18nKeys.phone),
trigger: 'blur',
}),
/** 数字范围(支持对象结构 { value: number }) */
numberRange: (min: number, max: number, message?: string): FormItemRule => ({
validator: (rule, value, callback) => {
// 兼容 ElInputNumber 的对象结构
let numValue: number
if (value && typeof value === 'object' && typeof value.value === 'number') {
numValue = value.value
} else if (typeof value === 'number') {
numValue = value
} else if (typeof value === 'string') {
numValue = Number(value)
} else {
callback(new Error(message || t(ValidationI18nKeys.numberRange, { min, max })))
return
}
if (Number.isNaN(numValue) || numValue < min || numValue > max) {
callback(new Error(message || t(ValidationI18nKeys.numberRange, { min, max })))
return
}
callback()
},
trigger: 'blur',
}),
/** 自定义校验函数 */
custom: (
validator: (value: any) => boolean | Promise<boolean>,
message: string
): FormItemRule => ({
validator: (rule, value, callback) => {
Promise.resolve(validator(value))
.then((result) => {
result ? callback() : callback(new Error(message))
})
.catch(() => callback(new Error(message)))
},
trigger: 'blur',
}),
// ... 更多基础规则
}
// ==================== 业务校验规则 ====================
const business = {
/** 订舱号格式 */
bookingNo: (message?: string): FormItemRule => ({
pattern: BusinessValidationConfig.patterns.bookingNo,
message: message || t(ValidationI18nKeys.bookingNo),
trigger: 'blur',
}),
/** 重量校验(0.01 - 50000) */
weight: (message?: string): FormItemRule => ({
validator: (rule, value, callback) => {
if (!value) {
callback()
return
}
const { min, max } = BusinessValidationConfig.ranges.weight
const weight = typeof value === 'object' ? value.value : Number(value)
if (Number.isNaN(weight) || weight < min || weight > max) {
callback(new Error(message || t(ValidationI18nKeys.weight)))
return
}
callback()
},
trigger: 'blur',
}),
// ... 更多业务规则
}
// ==================== 组合规则 ====================
const combined = {
/** 必填 + 邮箱 */
requiredEmail: (message?: string): FormItemRule[] =>
[base.required(), base.email(message)],
/** 必填 + 订舱号 */
requiredBookingNo: (message?: string): FormItemRule[] =>
[base.required(), business.bookingNo(message)],
// ... 更多组合规则
}
// ==================== 预设规则 ====================
const preset = {
/** 用户名(必填 + 格式 + 长度 3-20) */
username: (message?: string): FormItemRule[] => {
const { min, max } = BaseValidationConfig.lengths.username
return [
base.required(),
base.username(message),
base.length(min, max),
]
},
/** 密码(必填 + 长度 6-20) */
password: (message?: string): FormItemRule[] => {
const { min, max } = BaseValidationConfig.lengths.password
return [base.required(), base.length(min, max, message)]
},
// ... 更多预设规则
}
return {
base,
business,
combined,
preset,
config: {
base: BaseValidationConfig,
business: BusinessValidationConfig,
i18nKeys: ValidationI18nKeys,
},
}
}
🎨 使用示例
示例 1:用户注册表单
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import { reactive, ref } from 'vue'
import { useSimpleRules } from '@/composables/validation/useSimpleRules'
const formRef = ref<FormInstance>()
const { base, combined, preset } = useSimpleRules()
const formData = reactive({
username: '',
password: '',
confirmPassword: '',
email: '',
phone: '',
})
// 声明式定义规则,简洁清晰
const rules: FormRules = {
username: preset.username(),
password: preset.password(),
confirmPassword: [
base.required('请确认密码'),
base.custom(
value => value === formData.password,
'两次输入的密码不一致'
),
],
email: combined.requiredEmail(),
phone: combined.requiredPhone(),
}
</script>
<template>
<el-form ref="formRef" :model="formData" :rules="rules">
<el-form-item label="用户名" prop="username">
<el-input v-model="formData.username" />
</el-form-item>
<!-- ... 其他表单项 -->
</el-form>
</template>
示例 2:订舱业务表单
const { base, business, combined } = useSimpleRules()
const rules: FormRules = {
bookingNo: combined.requiredBookingNo(),
containerNo: combined.requiredContainerNo(),
hsCode: combined.requiredHsCode(),
weight: [
base.required('请输入重量'),
business.weight(),
],
volume: [
base.required('请输入体积'),
business.volume(),
],
}
示例 3:自定义校验消息
// 所有规则方法都支持自定义消息参数
const rules: FormRules = {
email: combined.requiredEmail('请输入企业邮箱'),
bookingNo: business.bookingNo('订舱号格式错误,请参考示例:ABCD123456'),
weight: business.weight('货物重量超出限制'),
}
🔧 扩展新规则
当业务需要新增校验规则时,只需 3 步:
Step 1: 添加配置
// validation.config.ts
export const BusinessValidationConfig = {
patterns: {
// 新增:发票号正则
invoiceNo: /^INV\d{10}$/,
},
}
export const ValidationI18nKeys = {
// 新增:发票号 i18n key
invoiceNo: 'validation.invoiceNo',
}
Step 2: 添加翻译
// i18n/zh/validation.ts
export default {
invoiceNo: '发票号格式不正确(INV + 10位数字)',
}
Step 3: 实现规则
// useSimpleRules.ts
const business = {
invoiceNo: (message?: string): FormItemRule => ({
pattern: BusinessValidationConfig.patterns.invoiceNo,
message: message || t(ValidationI18nKeys.invoiceNo),
trigger: 'blur',
}),
}
📊 架构对比
| 维度 | 传统方式 | 本架构方案 |
|---|---|---|
| 代码位置 | 分散在各组件 | 集中在配置 + Composable |
| 修改成本 | 需全局搜索修改 | 只改一处,全局生效 |
| 复用性 | 复制粘贴 | 引用调用 |
| 国际化 | 硬编码或分散 | 统一 i18n key 管理 |
| 类型提示 | 无或弱 | 完善的 TypeScript 支持 |
| 扩展方式 | 改动多处代码 | 配置 + 实现,职责分离 |
| 测试难度 | 难以单元测试 | 纯函数,易于测试 |
🎓 最佳实践
1. 优先使用高层 API
// ✅ 推荐:使用预设规则
username: preset.username()
// ⚠️ 不推荐:手动组合基础规则
username: [
base.required(),
base.username(),
base.length(3, 20),
]
2. 业务规则与基础规则分离
// ✅ 业务特有规则放 business
business.bookingNo()
business.containerNo()
// ✅ 通用规则放 base
base.email()
base.phone()
3. 复杂校验使用 custom
// ✅ 跨字段校验
base.custom(
value => value === formData.password,
'两次输入的密码不一致'
)
// ✅ 异步校验(如检查用户名是否已存在)
base.custom(
async (value) => {
const exists = await checkUsernameExists(value)
return !exists
},
'用户名已被注册'
)
4. 保持消息一致性
// ✅ 使用 i18n,支持多语言
message: t(ValidationI18nKeys.email)
// ❌ 避免硬编码
message: '请输入有效的邮箱'
🤔 总结与思考
本文介绍的架构方案核心思想是 配置驱动 + 关注点分离:
- 配置层:集中管理正则、范围、i18n key,便于维护
- 国际化层:文案统一管理,支持多语言
- Composable 层:将配置转化为可用的校验规则,提供清晰的 API
- 业务组件:声明式使用,专注业务逻辑
这套方案在我们的物流 SaaS 项目中经过实战检验,显著提升了:
- 📈 开发效率:新增表单校验从"复制粘贴"变成"一行代码"
- 📈 代码质量:消除重复代码,统一校验行为
- 📈 可维护性:需求变更时修改一处即可
- 📈 团队协作:新成员快速上手,API 清晰直观
当然,这套方案也有其适用边界。对于简单项目,直接使用 Element Plus 的内置校验可能更轻量。但当你的项目有以下特征时,不妨考虑引入类似架构:
- 🏢 中大型项目,表单众多
- 🌍 需要国际化支持
- 🔄 业务规则可能频繁变更
- 👥 多人协作,需要统一规范
希望本文对你有所启发!如果你有更好的实践方案,欢迎交流讨论。
📚 参考资源
- Element Plus Form 表单校验
- async-validator - Element Plus 底层使用的校验库
- Vue 3 Composition API
- vue-i18n - Vue 国际化方案
本文基于 Vue 3.5 + TypeScript 5 + Element Plus 2.9 编写,完整代码已在生产环境验证。