如何在客户端实现Joi验证?复用Express Schema及最佳方案示例
Joi Schema跨端复用与客户端表单验证方案
一、Express的Joi Schema能直接复用在客户端吗?
可以,但要注意Joi版本兼容性——新版joi@17+原生支持浏览器环境,无需额外适配,只要把Schema抽离成独立文件,就能同时给服务端和客户端调用。
实现示例
1. 抽离共享Schema文件
创建不依赖Node专属API的纯Schema定义文件:
// shared/validationSchemas.js const Joi = require('joi'); // 用户注册表单验证规则 exports.userRegisterSchema = Joi.object({ username: Joi.string().alphanum().min(3).max(30).required().messages({ 'string.empty': '用户名不能为空', 'string.alphanum': '用户名只能包含字母和数字', 'string.min': '用户名至少3个字符', 'string.max': '用户名最多30个字符', 'any.required': '用户名是必填项' }), email: Joi.string().email({ minDomainSegments: 2 }).required().messages({ 'string.empty': '邮箱不能为空', 'string.email': '请输入有效的邮箱地址', 'any.required': '邮箱是必填项' }), password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{6,30}$')).required().messages({ 'string.empty': '密码不能为空', 'string.pattern.base': '密码必须是6-30位的字母或数字', 'any.required': '密码是必填项' }) });
2. 服务端Express中使用
// server/routes/auth.js const express = require('express'); const { userRegisterSchema } = require('../../shared/validationSchemas'); const router = express.Router(); router.post('/register', (req, res) => { const { error } = userRegisterSchema.validate(req.body, { abortEarly: false }); if (error) { return res.status(400).json({ errors: error.details.map(detail => detail.message) }); } // 执行注册逻辑 res.status(200).json({ message: '注册成功' }); }); module.exports = router;
3. 客户端(React)中使用
直接引入共享Schema,和服务端用相同规则验证:
// client/components/RegisterForm.jsx import { useState } from 'react'; import { userRegisterSchema } from '../shared/validationSchemas'; export default function RegisterForm() { const [formData, setFormData] = useState({ username: '', email: '', password: '' }); const [errors, setErrors] = useState([]); const handleSubmit = (e) => { e.preventDefault(); const { error } = userRegisterSchema.validate(formData, { abortEarly: false }); if (error) { setErrors(error.details.map(detail => detail.message)); return; } // 提交到后端 fetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }) .then(res => res.json()) .then(data => data.message && alert(data.message)); }; const handleChange = (e) => { setFormData(prev => ({ ...prev, [e.target.name]: e.target.value })); // 输入时清除对应字段的错误提示 setErrors(prev => prev.filter(err => !err.includes(e.target.name))); }; return ( <form onSubmit={handleSubmit}> <div> <label>用户名:</label> <input type="text" name="username" value={formData.username} onChange={handleChange} /> </div> <div> <label>邮箱:</label> <input type="email" name="email" value={formData.email} onChange={handleChange} /> </div> <div> <label>密码:</label> <input type="password" name="password" value={formData.password} onChange={handleChange} /> </div> {errors.length > 0 && ( <div style={{ color: 'red' }}> {errors.map((err, idx) => <p key={idx}>{err}</p>)} </div> )} <button type="submit">注册</button> </form> ); }
二、无法复用的情况?用Joi客户端替代方案
如果遇到旧版Joi不支持浏览器的情况,可以用joi-browser(虽已停止维护,但仍可正常使用),示例如下:
1. 安装依赖
npm install joi-browser
2. 客户端使用示例
// client/components/RegisterForm.jsx import { useState } from 'react'; import Joi from 'joi-browser'; // 客户端定义和服务端一致的Schema const userRegisterSchema = Joi.object({ username: Joi.string().alphanum().min(3).max(30).required().messages({ 'string.empty': '用户名不能为空', 'string.alphanum': '用户名只能包含字母和数字', 'string.min': '用户名至少3个字符', 'string.max': '用户名最多30个字符', 'any.required': '用户名是必填项' }), // 其余字段规则和服务端保持一致 }); // 后续表单逻辑和之前的客户端示例完全相同
三、客户端表单验证最佳方案
目前更推荐轻量跨端验证库+高效表单状态管理的组合,比如Zod(TypeScript友好、体积小)+ React Hook Form(性能优异),既能实现前后端规则复用,又能提升表单体验。
实现示例
1. 安装依赖
npm install zod react-hook-form @hookform/resolvers
2. 定义共享Schema(支持TypeScript)
// shared/validationSchemas.ts import { z } from 'zod'; export const userRegisterSchema = z.object({ username: z.string() .min(3, '用户名至少3个字符') .max(30, '用户名最多30个字符') .regex(/^[a-zA-Z0-9]+$/, '用户名只能包含字母和数字') .nonempty('用户名不能为空'), email: z.string() .email('请输入有效的邮箱地址') .nonempty('邮箱不能为空'), password: z.string() .min(6, '密码至少6个字符') .max(30, '密码最多30个字符') .regex(/^[a-zA-Z0-9]+$/, '密码必须是字母或数字') .nonempty('密码不能为空') }); // 导出TypeScript类型,用于类型提示 export type UserRegisterInput = z.infer<typeof userRegisterSchema>;
3. 服务端Express中使用
// server/routes/auth.js const express = require('express'); const { userRegisterSchema } = require('../../shared/validationSchemas'); const router = express.Router(); router.post('/register', (req, res) => { const result = userRegisterSchema.safeParse(req.body); if (!result.success) { return res.status(400).json({ errors: result.error.issues.map(issue => issue.message) }); } // 执行注册逻辑 res.status(200).json({ message: '注册成功' }); }); module.exports = router;
4. 客户端React中使用
// client/components/RegisterForm.tsx import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { userRegisterSchema, UserRegisterInput } from '../shared/validationSchemas'; export default function RegisterForm() { const { register, handleSubmit, formState: { errors } } = useForm<UserRegisterInput>({ resolver: zodResolver(userRegisterSchema), mode: 'onBlur' // 失去焦点时触发验证,可按需改为onChange/onSubmit }); const onSubmit = (data: UserRegisterInput) => { fetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(res => res.json()) .then(data => data.message && alert(data.message)); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <div> <label>用户名:</label> <input type="text" {...register('username')} /> {errors.username && <p style={{ color: 'red' }}>{errors.username.message}</p>} </div> <div> <label>邮箱:</label> <input type="email" {...register('email')} /> {errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>} </div> <div> <label>密码:</label> <input type="password" {...register('password')} /> {errors.password && <p style={{ color: 'red' }}>{errors.password.message}</p>} </div> <button type="submit">注册</button> </form> ); }
内容的提问来源于stack exchange,提问作者user19359397
相关产品推荐
相关产品推荐

