在开发过程中,我们可以通过TypeScript来对我们的代码来进行类型校验来防止我们的代码出现类型错误,但是运行时的数据我们没法通过TS类型来处理,当遇到表单等动态数据需要校验时,就需要用到运行时类型校验工具了。
Joi
和Zod
是两款常见的运行时类型校验工具,下面我们来对比一下两款工具的使用差异。
Joi
Joi 是一款强大的数据验证库,最初为 Hapi.js 框架开发,后广泛应用于各类 JavaScript 项目。它以简洁、直观的语法描述数据结构与验证规则,使数据校验轻松实现。
基本使用
- 创建验证模式:
通过组合 Joi 提供的类型和规则构建验证模式。例如,验证用户名和密码:
import Joi from 'joi';
const schema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().pattern(new RegExp('^(a-zA-Z0-9){3,30}$'))
});
上述代码中,username必须为 3 - 30 位的字母数字组合且必填;password需匹配指定正则表达式。
- 验证数据:
使用validate方法验证数据,返回包含错误信息和验证后数据的对象:
const { error, value } = schema.validate({ username: 'abc', password: '123' });
if (error) {
console.error(error.details);
} else {
console.log(value);
}
若验证通过,error为undefined;否则,error包含详细错误信息。
常用API
基础类型校验
Joi.string()
: 字符串类型校验Joi.number()
: 数字类型校验Joi.boolean()
: 布尔类型校验Joi.object()
: 对象类型校验Joi.array()
: 数组类型校验Joi.date()
: 日期类型校验Joi.null()
: null 类型校验Joi.any()
: 任意类型校验
字符串常用约束
.min(length)
: 最小长度限制.max(length)
: 最大长度限制.required()
: 设为必填项.email()
: 校验邮箱格式.pattern(regExp)
: 正则匹配校验.alphanum()
: 仅允许字母和数字.trim()
: 自动去除首尾空格.valid(...values)
: 枚举值校验(必须是指定值之一).invalid(...values)
: 排除指定值
数字常用约束
.min(value)
: 最小值限制.max(value)
: 最大值限制.integer()
: 必须为整数.positive()
: 必须为正数.negative()
: 必须为负数.precision(digits)
: 限制小数位数
对象常用约束
.keys(schema)
: 定义对象字段及校验规则.requiredKeys(...keys)
: 指定必填字段.optionalKeys(...keys)
: 指定可选字段.unknown(false)
: 禁止出现未定义的字段.when(condition, options)
: 条件校验(根据其他字段值动态调整规则)
数组常用约束
.items(schema)
: 定义数组元素的校验规则.min(length)
: 数组最小长度.max(length)
: 数组最大长度.unique()
: 数组元素必须唯一.has(schema)
: 数组必须包含符合指定规则的元素
校验与错误处理
.validate(value)
: 执行校验,返回包含error
和value
的对象.validateAsync(value)
: 异步校验(支持自定义异步规则).messages(customMessages)
: 自定义错误提示信息error.details
: 校验失败时的详细错误信息数组
代码实战
例如在页面中集成 Joi 进行表单验证。假设创建一个用户注册表单:
import React, { useState } from'react';
import Joi from 'joi';
const userSchema = Joi.object({
username: Joi
.string()
.alphanum()
.min(3)
.max(30)
.required()
.messages({
'string.base': '用户名必须是字符串',
'string.empty': '用户名不能为空',
'string.min': '用户名至少需要3个字符',
'string.max': '用户名最多30个字符',
'string.alphanum': '用户名只能包含字母和数字'
}),
email: Joi
.string()
.email()
.required()
.messages({
'string.base': '邮箱必须是字符串',
'string.empty': '邮箱不能为空',
'string.email': '邮箱格式不正确'
}),
password: Joi
.string()
.pattern(new RegExp('^(?=.*[a - z])(?=.*[A - Z])(?=.*[0 - 9]).{8,}$'))
.required()
.messages({
'string.base': '密码必须是字符串',
'string.empty': '密码不能为空',
'string.pattern.base': '密码至少8个字符,且包含大小写字母和数字'
}),
});
const RegisterForm = () => {
const [formData, setFormData] = useState({ username: '', email: '', password: '' });
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({...formData, [name]: value });
};
const handleSubmit = async (e) => {
e.preventDefault();
const result = await userSchema.validate(formData);
if (result.error) {
const errorObj = {};
result.error.details.forEach(detail => {
errorObj[detail.path[0]] = detail.message;
});
setErrors(errorObj);
} else {
console.log('表单提交成功:', result.value);
setErrors({});
}
};
return (
<form onSubmit={handleSubmit}>
<label>用户名:</label>
<input type="text" name="username" onChange={handleChange} />
{errors.username && <span style={{ color:'red' }}>{errors.username}</span>}
<br />
<label>邮箱:</label>
<input type="email" name="email" onChange={handleChange} />
{errors.email && <span style={{ color:'red' }}>{errors.email}</span>}
<br />
<label>密码:</label>
<input type="password" name="password" onChange={handleChange} />
{errors.password && <span style={{ color:'red' }}>{errors.password}</span>}
<br />
<button type="submit">提交</button>
</form>
);
};
export default RegisterForm;
上述代码中,每次输入框内容改变时更新formData。提交表单时,用userSchema验证formData,若有错误,将错误信息存储在errors对象中并展示在对应输入框下方。
Zod
Zod 是 TypeScript-first 的数据验证库,也能在 JavaScript 项目中良好使用。它基于 TypeScript 类型系统构建,提供类型安全且简洁的验证语法。
基本使用
- 创建验证模式:
与 Joi 类似,通过组合 Zod 的类型和方法定义验证模式:
import { z } from 'zod';
const schema = z.object({
username: z.string().min(3).max(30).regex(/^[a-zA-Z0-9]+$/),
password: z.string().min(8).regex(/^(?=.*[a - z])(?=.*[A - Z])(?=.*[0 - 9])/)
});
这里username是 3 - 30 位字母数字组合,password至少 8 位且包含大小写字母和数字。
- 验证数据:
使用parse或safeParse方法验证数据:
const data = { username: 'abc', password: '123' };
try {
const result = schema.parse(data);
console.log(result);
} catch (error) {
console.error(error);
}
parse方法验证失败时抛出错误,safeParse则返回包含是否成功及错误信息的对象,适合需更灵活处理错误的场景。
常用API
基础类型校验
z.string()
: 字符串类型校验z.number()
: 数字类型校验z.boolean()
: 布尔类型校验z.object()
: 对象类型校验z.array()
: 数组类型校验z.date()
: 日期类型校验z.null()
: null 类型校验z.unknown()
: 未知类型校验
字符串常用约束
.min(length, message?)
: 最小长度限制.max(length, message?)
: 最大长度限制.email(message?)
: 校验邮箱格式.regex(regExp, message?)
: 正则匹配校验.trim()
: 自动去除首尾空格.includes(substring, message?)
: 必须包含指定子串.enum(values)
: 枚举值校验(如z.enum(['a', 'b'])
)
数字常用约束
.min(value, message?)
: 最小值限制.max(value, message?)
: 最大值限制.int(message?)
: 必须为整数.positive(message?)
: 必须为正数.negative(message?)
: 必须为负数.multipleOf(value, message?)
: 必须是指定值的倍数
对象常用约束
.shape(fields)
: 定义对象字段及校验规则(如z.object({ name: z.string() })
).required()
: 设为必填项(针对对象字段).optional()
: 设为可选项(针对对象字段).strict()
: 严格模式(禁止类型自动转换).passthrough()
: 允许传递未定义的字段.refine(condition, options)
: 自定义校验逻辑
数组常用约束
.element(schema)
: 定义数组元素的校验规则.min(length, message?)
: 数组最小长度.max(length, message?)
: 数组最大长度.nonempty(message?)
: 数组不能为空.unique(message?)
: 数组元素必须唯一
类型转换与组合
.optional()
: 类型设为可选(可能为undefined
).nullable()
: 类型设为可空(可能为null
).default(value)
: 设置默认值.or(schema)
: 联合类型(如z.string().or(z.number())
).and(schema)
: 交叉类型.transform(fn)
: 数据转换(校验后修改值)
校验与错误处理
.parse(value)
: 执行校验,失败时抛出错误.safeParse(value)
: 安全校验,返回{ success: boolean, data?: T, error?: ZodError }
.parseAsync(value)
: 异步校验(支持异步refine
)z.infer<typeof schema>
: 从 schema 推导 TypeScript 类型
代码实战
在页面上使用 Zod 验证表单。以用户登录表单为例:
import React, { useState } from'react';
import { z } from 'zod';
const loginSchema = z.object({
username: z
.string()
.min(3, { message: '用户名至少需要3个字符' })
.max(12, { message: '用户名最多12个字符' })
// .nonempty({ message: '用户名不能为空' })
.regex(/^[a-zA-Z0-9]+$/, { message: '用户名只能包含字母和数字' }),
password: z
.string()
.min(8, { message: '密码至少8个字符' })
.max(16, { message: '密码不能超过16个字符' })
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/, { message: '密码至少8个字符,且包含大小写字母和数字' }),
});
const LoginForm = () => {
const [formData, setFormData] = useState({ username: '', password: '' });
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({...formData, [name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
const result = loginSchema.safeParse(formData);
if (!result.success) {
const errorObj = {};
result.error.issues.forEach(issue => {
errorObj[issue.path[0]] = issue.message;
});
setErrors(errorObj);
} else {
console.log('表单提交成功:', result.data);
setErrors({});
}
};
return (
<form onSubmit={handleSubmit}>
<label>用户名:</label>
<input type="text" name="username" onChange={handleChange} />
{errors.username && <span style={{ color:'red' }}>{errors.username}</span>}
<br />
<label>密码:</label>
<input type="password" name="password" onChange={handleChange} />
{errors.password && <span style={{ color:'red' }}>{errors.password}</span>}
<br />
<button type="submit">提交</button>
</form>
);
};
export default LoginForm;
该表单中,每次输入改变更新formData。提交时用loginSchema的safeParse方法验证,若失败,将错误信息存入errors并展示在对应输入框旁。
差异对比
语法风格
Joi:语法更接近自然语言,描述规则直观,对习惯 JavaScript 传统语法开发者友好。如Joi.string().alphanum().min(3).max(30).required()定义用户名规则。
Zod:基于 TypeScript 类型系统,语法简洁紧凑,类型标注感强,熟悉 TypeScript 的开发者易上手。如z.string().min(3).max(30).regex(/^[a-zA-Z0-9]+$/)定义用户名规则。
类型支持
Joi:原生 JavaScript 库,虽无内置 TypeScript 支持,但可通过社区类型定义文件(如@types/joi)在 TypeScript 项目中使用,不过类型推断和检查可能不如 Zod 直接和精确。
Zod:专为 TypeScript 设计,紧密结合 TypeScript 类型系统,定义验证模式时自动推导类型,在 TypeScript 项目中提供出色类型安全和智能提示,降低类型相关错误风险。
错误处理
- Joi:validate方法返回包含error和value的对象,error是ValidationError对象,details属性包含详细错误信息数组,需手动遍历处理,如:
const { error, value } = schema.validate({ username: '', password: '123' });
if (error) {
error.details.forEach(detail => {
console.error(detail.message);
});
}
- Zod:parse方法验证失败抛错,safeParse方法返回包含success、data(验证后数据)和error(ZodError对象)的对象,ZodError的issues属性是错误信息数组,处理更灵活直观,如:
const result = schema.safeParse({ username: '', password: '123' });
if (!result.success) {
result.error.issues.forEach(issue => {
console.error(issue.message);
});
}
生态与社区
Joi:历史悠久,社区成熟,广泛应用于 Node.js 后端项目,有丰富插件和资源,在 Express、Hapi 等框架中常作数据验证工具。但在纯前端 React 项目中,可能因后端导向特性,使用便捷性逊于部分前端专属校验库。
Zod:较新,专注前端和 TypeScript 生态,在 React、Vue 等前端框架中受青睐,尤其 TypeScript 项目。其社区活跃,文档完善,不断更新迭代,积极适配前端开发新趋势和需求。