schema校验工具 valibot
Valibot 是一个 TypeScrip、极轻量的 schema 校验库
- 可用于表单数据的schema校验工具
valibot常用api
- v.pipe
- 可以链式调用验证
- v.object
- 定义对象 schema
- v.check
- 自定义校验
- v.forward
- 转发错误到指定的字段
- v.nullish(v.string())
- 支持空字符、undefined、null
- v.optional(v.string())
- 支持空字符、undefined
- v.nullable(v.string())
- 只支持 null、string
- v.array 定义数组类型
| 方法名称 | 类别 | 用途描述 | 参数说明 | 示例代码 | 备注 |
|---|---|---|---|---|---|
| string() | 基本类型 | 定义字符串类型 schema,可结合 pipe 添加规则(如长度、email、正则)。 | optional message: 自定义错误消息 | v.string('必须是字符串') | 常用于 pipe 中:v.pipe(v.string(), v.minLength(3)) |
| number() | 基本类型 | 定义数字类型 schema,支持整数/浮点检查。 | optional message | v.pipe(v.number(), v.minValue(18)) | 可结合 integer() 检查整数 |
| object() | 结构类型 | 定义对象 schema,指定键值对的子 schema。 | entries: { [key: string]: Schema } optional rest: Schema | v.object({ name: v.string(), age: v.number() }) | 支持嵌套;用 strictObject() 严格模式(不允许未知键) |
| array() | 结构类型 | 定义数组 schema,指定元素类型。 | item: Schema optional message | v.array(v.string()) 或 v.pipe(v.array(v.number()), v.minLength(1)) | 可结合 pipe 检查数组长度 |
| pipe() | 管道机制 | 链式应用多个验证/转换动作到 schema 上(核心功能)。 | ...actions: [Schema, Action1, Action2, ...] | v.pipe(v.string(), v.trim(), v.email(), v.endsWith('@example.com')) | 支持验证(如 minLength、email)、转换(如 trim、transform) |
| parse() | 解析方法 | 同步验证并解析输入,成功返回输出数据,失败抛出 ValiError。 | schema, input, optional config ({ abortEarly: true 等 }) | const data = v.parse(schema, input); | 推荐用 try-catch 处理错误 |
| safeParse() | 解析方法 | 安全解析,不抛错误,返回 { success, output } 或 { success, issues }。 | schema, input, optional config | const result = v.safeParse(schema, input); if (result.success) { ... } | 最常用,避免异常;issues 是错误详情数组 |
| is() | 类型守卫 | 类型谓词检查输入是否符合 schema(不解析转换)。 | schema, input | if (v.is(schema, data)) { / data 类型已窄化 / } | 仅同步 schema;不返回 issues,不应用转换 |
| partial() | 对象修改 | 将 object schema 的所有键变为可选(类似 TypeScript Partial)。 | objectSchema | v.partial(v.object({ a: v.string(), b: v.number() })) | 常用于可选表单字段 |
| pick() / omit() | 对象修改 | 从现有 object schema 中挑选/排除某些键。 | objectSchema, keys: string[] | v.pick(schema, ['name']) | 类似 TypeScript Pick/Omit |
| optional() | 可选修饰 | 使 schema 可选(值可为 undefined)。 | schema, optional defaultValue | v.optional(v.string(), 'default') | 支持默认值填充 |
| check() | 管道动作 | 在 pipe 中添加自定义同步验证。如果检查失败,返回 issue。 | requirement: (input) => boolean, optional message | v.pipe(v.object({ list: v.array(v.string()), length: v.number() }), v.check(input => input.list.length === input.length, '长度不匹配')) | 与 partialCheck 区别:check 需整个输入完整才执行 |
| checkAsync() | 管道动作 | 异步版自定义验证(支持 Promise,如查数据库)。 | requirement: async (input) => boolean, optional message | v.pipeAsync(v.string(), v.checkAsync(async (val) => await dbCheck(val))) | 用于异步场景 |
| partialCheck() | 管道动作 | 部分字段自定义验证(即使其他字段无效也能执行,常用于表单跨字段检查)。 | paths: string[][], requirement: (selection) => boolean, optional message | v.partialCheck([['password1'], ['password2']], input => input.password1 === input.password2, '密码不匹配') | 常与 forward 搭配使用 |
| partialCheckAsync() | 管道动作 | 异步版部分字段自定义验证。 | paths: string[][], requirement: async (selection) => boolean, message | v.partialCheckAsync([['email']], async ({email}) => await isUnique(email)) | 适合服务器端唯一性检查 |
| forward() | 管道动作 | 将验证动作的错误转发到指定嵌套字段(改善 UI 错误显示位置)。 | action: ValidationAction, path: string[] | v.forward(v.partialCheck(...), ['password2']) | 常用于密码确认错误显示在第二个字段 |
| forwardAsync() | 管道动作 | 异步版错误转发。 | action: AsyncValidationAction, path: string[] | v.forwardAsync(v.partialCheckAsync(...), ['email']) | 与 partialCheckAsync 搭配 |
| transform() | 管道动作 | 在 pipe 中转换输入值(可改变类型或格式)。 | transformer: (input) => output | v.pipe(v.string(), v.transform(Number), v.number()) | 输出类型自动推断 |
| transformAsync() | 管道动作 | 异步版转换(支持 Promise)。 | transformer: async (input) => output | v.pipeAsync(v.string(), v.transformAsync(async (val) => await process(val))) | 用于复杂异步处理 |
| fallback() | 默认值 | 验证失败时返回预设默认值(不抛错,默认值不进一步验证)。 | schema, defaultValue 或 getter: () => value | v.fallback(v.string(), 'hello') | 常用于 optional 或不确定输入 |
| fallbackAsync() | 默认值 | 异步版默认值(支持 Promise)。 | schema, asyncGetter | v.fallbackAsync(v.string(), async () => await getDefault()) | 适合动态默认值 |
配置数组
- 数字数组和字符串数组
items: v.array(v.union([v.string(), v.number()]))
- 普通数组(必须是数组)
items: v.array(v.string())
- 只支持 null(string[] 或 null)
items: v.nullable(v.array(v.string()))
- 不允许空数组(至少 1 项)
items: v.pipe(
v.array(v.string()),
v.minLength(1, '至少需要 1 项')
)
- 数组对象示例
items: v.array(
v.object({
id: v.number(),
name: v.pipe(v.string(), v.trim(), v.nonEmpty('名称必填')),
})
)
组合多类型
- 希望支持数字、字符串、undefined
items: v.optional(v.union([v.string(), v.number()])),
valibot常用方法
- pipe 组合校验
- 默认值
- 安全校验(不 throw)
pipe 组合校验
import { string, minLength, maxLength, email, pipe } from 'valibot'
const EmailSchema = pipe(
string(),
email(),
maxLength(50)
)
可选 & 默认值
import { optional, string, defaultValue } from 'valibot'
const Schema = object({
keyword: optional(string()),
page: defaultValue(number(), 1),
})
安全校验(不 throw)
import { safeParse } from 'valibot'
const result = safeParse(UserSchema, data)
if (!result.success) {
console.log(result.issues)
} else {
console.log(result.output)
}
自定义正则处理
- Valibot 内置了
regex校验器:
import { string, regex, pipe } from 'valibot'
const UsernameSchema = pipe(
string(),
regex(/^[a-zA-Z0-9_]+$/, '只能包含字母、数字、下划线')
)
- 多个正则叠加
const PasswordSchema = pipe(
string(),
regex(/[A-Z]/, '至少包含一个大写字母'),
regex(/[a-z]/, '至少包含一个小写字母'),
regex(/\d/, '至少包含一个数字'),
regex(/[^A-Za-z0-9]/, '至少包含一个特殊字符'),
)
复杂自定义验证(正则 + 业务逻辑)
- regex 正则 + check 自定义函数
const UsernameSchema = v.pipe(
v.string(),
v.regex(/^[a-zA-Z][a-zA-Z0-9_-]*$/, '用户名必须以字母开头'), // 正则部分
v.check((value) => value.length >= 4, '用户名至少 4 位') // 自定义长度(可替换为 minLength)
);
转发到指定字段检验
const RegisterSchema = v.pipe(
v.object({
password1: v.pipe(v.string(), v.minLength(8)),
password2: v.string(),
}),
v.forward(
v.partialCheck(
[['password1'], ['password2']],
(input) => input.password1 === input.password2,
'两次密码不匹配'
),
['password2'] // 把错误转发到 password2 字段
)
);
// 验证失败时,issue 会关联到 password2,便于 UI 显示错误
前端表单校验
简单检验
import {
object,
string,
pipe,
minLength,
email,
safeParse,
} from 'valibot'
export const LoginSchema = object({
email: pipe(string(), email()),
password: pipe(string(), minLength(6)),
})
const result = safeParse(LoginSchema, formData)
if (!result.success) {
const errors = result.issues.map(i => ({
field: i.path?.[0].key,
message: i.message,
}))
}
Issue 对象
issues是一个数组,数组中每一项都是一次校验失败的信息。
{
kind: 'validation',
type: 'regex',
input: 'abc',
expected: '/^[A-Z]/',
received: 'abc',
message: '至少包含一个大写字母',
path: [
{
type: 'object',
key: 'password',
value: 'abc'
}
]
}
最强实战
- 用 safeParse 处理用户输入,如表单相关
- parse 只用于你“确信是正确”的数据
- 因为 parse当解析错误时,会直接throw报错
import * as v from 'valibot'
// 表单校验
export function useFormValid(formInfo: ObjectType, scheme: any) {
// 表单校验状态
const formValidState = reactive<Record<string, string[]>>({})
const getIssuesByKey = (issues: any[] | undefined, key: string) => {
if (!issues?.length)
return []
return issues
.filter(issue => Array.isArray(issue?.path) && issue.path.some((p: any) => p?.key === key))
.map(issue => issue?.message)
.filter(Boolean)
}
// 检验整个表单是否通过
const isValidPass = () => {
const result = v.safeParse(scheme, formInfo)
return result.success
}
// 检验整个表单,并同步 formValidState
const handleValidForm = () => {
const result = v.safeParse(scheme, formInfo)
if (result.success) {
for (const key of Object.keys(formInfo))
formValidState[key] = []
return result
}
const issues = result.issues ?? []
for (const key of Object.keys(formInfo))
formValidState[key] = getIssuesByKey(issues, key)
return result
}
// 检验某个字段
const handleValidField = (key: string) => {
const result = v.safeParse(scheme, formInfo)
if (result.success) {
formValidState[key] = []
}
else {
const issues = result.issues ?? []
formValidState[key] = getIssuesByKey(issues, key)
}
return formValidState[key]
}
// 获取某个字段是否有错误
const hasFieldsError = (key: string) => {
return Array.isArray(formValidState[key]) && formValidState[key].length > 0
}
return {
formValidState,
isValidPass,
handleValidForm,
handleValidField,
hasFieldsError,
}
}
- schema
const schema = v.pipe(
v.object({
name: v.pipe(
v.string(),
v.nonEmpty('名称必填'),
v.maxLength(20, '名称最大长度是20'),
),
password: v.pipe(
v.string(),
v.nonEmpty('密码必填'),
v.maxLength(20, '密码最大长度是20'),
),
type: v.string(),
remark: v.string(),
}),
v.forward(
v.check(
input => input.type.trim() !== '' || input.remark.trim() !== '',
'当未填写类型时,说明为必填',
),
['remark'],
),
)