You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在客户端实现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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 03:05:31