常见问题解答 (FAQ)

基础问题

Q: schema-dsl 适合解决什么问题?

A: schema-dsl 适合把验证规则写成紧凑、可序列化、便于在配置、API、前端表单和后端服务之间共享的形式。

import { s } from 'schema-dsl/pure';

const schema = s({
  username: 'string:3-32!',
  email: 'email!'
});

主要区别

  • 更简洁的 DSL 语法
  • 字段需要元数据或自定义约束时,可继续使用链式 builder
  • 支持数据库 Schema 导出
  • 内置常见验证器(username、password、phone)
  • 基于 JSON Schema 标准

Q: 如何安装 schema-dsl?

npm install schema-dsl

Node.js 版本要求>=18.0.0

当前版本以 Node.js >=18.0.0 为运行时基线,不再承诺旧 Node 版本兼容。


Q: 支持 ES Modules 吗?

A: 支持。

// 公开文档推荐入口
import { s, validate } from 'schema-dsl/pure';

const schema = s({ email: 'email!' });
const result = validate(schema, { email: 'test@example.com' });

Q: i18n 目录加载支持哪些语言包文件格式?

A: 在 Node.js >= 18.0.0 下,s.config({ i18n: '/path/to/locales' }) 默认支持:

  • .js(CommonJS 语言包)
  • .cjs
  • .json
  • .jsonc
  • .json5

推荐:如果你的项目是 type: module / ESM,优先使用 .cjs.json.jsonc.json5,兼容性最稳定。

.js / .cjs 语言包会作为可信 Node 代码执行。如果 locale 目录可能包含不可信文件,请配置 s.config({ i18n: '/path/to/locales', codeLocaleFiles: 'deny' }),仅加载 .json.jsonc.json5


DSL 语法问题

Q: 'string:3-32!' 是什么意思?

A: 这是 DSL 语法:

  • string - 类型
  • 3-32 - 长度范围(最小3,最大32)
  • ! - 必填

更多示例:

'string:10'      // 最大长度10
'string:3-'      // 最小长度3
'number:0-100'   // 数值范围0-100
'email!'         // 必填邮箱
'a|b|c'          // 枚举值

Q: 如何定义数组?

A: 使用 array 类型:

// 简单数组
tags: 'array'

// 带长度约束
tags: 'array:1-10'      // 1-10个元素
tags: 'array!1-10'      // 必填,1-10个元素

// 带元素类型
tags: 'array<string>'   // 字符串数组
tags: 'array<number>'   // 数字数组
tags: 'array<string:1-20>'  // 带约束的字符串数组

Q: 如何定义嵌套对象?

A: 直接嵌套即可:

import { s } from 'schema-dsl/pure';

const schema = s({
  user: {
    name: 'string!',
    address: {
      city: 'string!',
      zip: 'string:5-10!'
    }
  }
});

Q: 如何使用 String 扩展?

A: String 扩展属于显式兼容/易用性路径。新的公开示例默认使用 schema-dsl/pure + s,因为它同时支持纯 DSL 字符串、s('...')s.xxx(),并且不会往 String.prototype 上安装方法。

import { s } from 'schema-dsl/pure';

const schema = s({
  email: s('email!')
    .label('邮箱地址')
    .messages({
      'required': '{{#label}}不能为空',
      'format': '请输入有效的{{#label}}'
    }),

  username: s('string:3-32!')
    .pattern(/^[a-z0-9_]+$/)
    .label('用户名')
    .username('medium')
});

如果确实想使用直接字符串链式,请参考 String 扩展 中的显式 runtime/type 入口。


验证问题

Q: 如何验证数据?

A: 使用 validate() 函数或 Validator 类:

// 方式1:便捷函数
import { s, validate } from 'schema-dsl/pure';

const schema = s({ email: 'email!' });
const result = validate(schema, data);

// 方式2:Validator 实例
import { Validator } from 'schema-dsl/pure';

const validator = new Validator();
const result = validator.validate(schema, data);

Q: 验证结果的格式是什么?

A: 返回对象包含:

{
  valid: true/false,      // 是否通过
  data: {},               // 当前实现会返回本次验证数据,失败时也便于定位输入
  errors: []              // 成功时为空数组,失败时包含详细错误
}

Q: 如何获取所有错误而不是只有第一个?

A: 默认就会返回全部错误。如果你只想保留首条错误,可以显式关闭 allErrors

validate(schema, data, { allErrors: false });

如果你需要一个提前停止的 Validator,也可以使用 new Validator({ allErrors: false })。注意:构造期已经关闭 allErrors 的 Validator 无法在单次调用时恢复 AJV 没有收集的错误;默认 Validator 和 root helpers 可以通过 { allErrors: false } 在单次调用中只保留首条格式化错误。


Q: 如何使用默认值?

A: 使用 .default() 方法:

import { s, validate } from 'schema-dsl/pure';

const schema = s({
  status: s('string').default('active'),
  count: s('integer').default(0)
});

const result = validate(schema, {});
console.log(result.data);
// { status: 'active', count: 0 }

性能问题

Q: schema-dsl 的性能怎么样?

A: 当前 benchmark 应作为项目本地吞吐证据,而不是永久营销结论。具体吞吐、环境和场景胜负只在 性能优化指南 的 tracked snapshot 表中维护,FAQ 不复制易过期数据。

结论:

  • ✅ 在这台本地机器上,热路径验证已处于百万 ops/sec 级别。
  • ✅ 内置缓存可避免复用 schema 时重复解析。
  • ✅ 性能指南区分 19 个可比场景和不计入胜负的 AV2_THROW 异步抛错诊断场景。
  • ✅ 这些数字适合作为回归基线;运行时、依赖或 schema 复杂度变化后应重新跑 benchmark。

Q: 有效/无效数据场景性能差异为什么大?

A: 无效数据吞吐高度依赖错误收集和格式化方式。schema-dsl 会把热验证路径和本地化消息渲染分开,因此原始无效数据 benchmark 可以接近有效数据 benchmark。启用自定义格式化、i18n 或大型嵌套错误输出后,应使用自己的真实 schema 与错误输出重新测量。


Q: 什么时候性能会成为瓶颈?

A: 以下场景才可能成为瓶颈:

  1. API 网关(每秒 >50万次验证)
  2. 高并发服务(每秒 >50万次请求)
  3. 实时数据处理(毫秒级延迟要求)

大多数应用(每秒 <10万次验证)不会遇到性能瓶颈。


Q: 验证速度慢怎么办?

A: 使用预编译和缓存:

// 1. 使用预编译
const validator = new Validator();
const validateUser = validator.compile(userSchema);

// 2. 启用缓存

const validator = new Validator({
  cache: {
    maxSize: 5000,   // 缓存5000个Schema
    ttl: 0           // 不按时间过期,由 LRU 管理生命周期
  }
});

// 3. 复用 Validator 实例
// ❌ 错误:每次都创建新实例
app.post('/api/users', (req, res) => {
  const validator = new Validator();  // 慢
  // ...
});

// ✅ 正确:复用实例
const validator = new Validator();
app.post('/api/users', (req, res) => {
  const result = validator.validate(schema, req.body);  // 快
  // ...
});

Q: 缓存如何工作?

A: schema-dsl 当前通过 CacheManager 委托 cache-hubMemoryCache 实现编译缓存:

const validator = new Validator({
  cache: {
    maxSize: 5000,   // 最大缓存5000条
    ttl: 0           // 不按时间过期,由 LRU 管理生命周期
  }
});

// 缓存统计
const stats = validator.getCacheStats();
console.log(stats);
// {
//   hits: 8500,
//   misses: 150,
//   hitRate: '98.27',
//   size: 150,
//   maxSize: 5000,
//   enabled: true
// }

Q: 如何批量验证?

A: 使用 SchemaUtils.validateBatch()

import { SchemaUtils, Validator } from 'schema-dsl/pure';

const validator = new Validator();
const batch = SchemaUtils.validateBatch(schema, [data1, data2, data3], validator.getAjv());

console.log(batch.summary.valid);
console.log(batch.results[0].valid);

设计理念

Q: 为什么选择运行时解析而不是编译时构建?

A: 这是有意的设计选择,优先考虑灵活性而非极致性能

运行时解析的优势:

  1. 完全动态 - 可从配置/数据库动态生成规则
  2. 多租户支持 - 每个租户不同规则,零代码修改
  3. 可序列化 - 可存储、传输、共享
  4. 前后端共享 - 一套规则,两端使用
  5. 低代码基础 - 可视化配置表单验证

编译时构建的限制:

  • ❌ Schema 固定,无法动态调整
  • ❌ 无法序列化和传输
  • ❌ 多租户困难
  • ❌ 无法从数据库读取规则

详细说明: 设计理念文档


Q: Schema-DSL 适合什么场景?

A: ✅ 最适合的场景:

  1. 多租户 SaaS 系统 - 每个租户不同验证规则
  2. 后台管理系统 - 管理员配置表单验证
  3. 配置驱动开发 - 验证规则存储在配置/数据库
  4. 低代码/无代码平台 - 可视化表单构建器
  5. 快速原型开发 - 5分钟上手,代码量最少
  6. 前后端共享验证 - 一套规则,两端使用

⚠️ 不适合的场景:

  1. 只追求绝对吞吐量,且不需要 DSL 动态能力
  2. 需要把每个值约束都建模成 TypeScript 静态类型
  3. 验证规则完全静态,永远不需要序列化、存储或配置化编辑

Q: 为什么不做成纯编译时库?

A: 因为会失去核心价值:

失去的能力:

// ❌ 无法从数据库读取规则
const rules = await db.findOne({ entity: 'user' });
const schema = s(rules);

// ❌ 无法多租户动态规则
function getTenantSchema(tenantId) {
  return s(tenantConfig[tenantId]);
}

// ❌ 无法通过 API 传输
res.json({ validationRules: rules });

// ❌ 无法后台配置表单验证

保留的能力:

// ✅ 完全动态
const schema = s({
  username: `string:${config.min}-${config.max}!`
});

// ✅ 可序列化
JSON.stringify({ username: 'string:3-32!' });

// ✅ 前后端共享
// 后端定义 → API传输 → 前端使用

Q: 性能和灵活性如何平衡?

A: Schema-DSL 的设计优先级:

灵活性 > 易用性 > 性能

权衡结果:

  • 增益:规则紧凑、可序列化,可跨运行时边界存储、传输、编辑和共享
  • 成本:TypeScript 无法把每个 DSL 约束都细化成精确静态值域类型

错误处理

Q: 如何自定义错误消息?

A: 使用 .messages() 方法:

username: s('string:3-32!').label('用户名')
  .messages({
    'min': '{{#label}}太短了',
    'max': '{{#label}}太长了',
    'required': '请输入{{#label}}'
  })

Q: 如何支持多语言?

A: 使用 Locale 类:

import { Locale } from 'schema-dsl/pure';

// 添加语言包
Locale.addLocale('zh-CN', {
  'required': '{{#label}}不能为空',
  'min': '{{#label}}长度不能少于{{#limit}}'
});

// 验证时指定语言
validator.validate(schema, data, { locale: 'zh-CN' });

Q: 错误路径格式是什么?

A: 当前返回的是 slash path:

'username'           // 顶层字段
'user/name'          // 嵌套字段
'items/0/name'       // 数组元素

数据库导出

Q: 如何导出为 MongoDB Schema?

import { exporters } from 'schema-dsl/pure';

const exporter = new exporters.MongoDBExporter();
const mongoSchema = exporter.export(schema);

Q: 如何导出为 MySQL DDL?

const exporter = new exporters.MySQLExporter();
const ddl = exporter.export('table_name', schema);

Q: 如何导出为 PostgreSQL DDL?

const exporter = new exporters.PostgreSQLExporter({ schema: 'public' });
const ddl = exporter.export('table_name', schema);

Q: 导出时如何添加注释?

A: 使用 .description()

username: s('string:3-32!').description('用户登录名,只能包含字母数字')

MySQL 会生成 COMMENT,PostgreSQL 会生成 COMMENT ON COLUMN


TypeScript 支持

Q: schema-dsl 支持 TypeScript 吗?

A: 支持。公开 TypeScript 示例推荐 schema-dsl/pure + s:简单字段用纯 DSL 字符串,需要 builder 提示时用 s('...'),需要最完整方法发现时用 s.xxx() factory。

import { s, validate, Validator } from 'schema-dsl/pure';

const schema = s({
  username: 'string:3-32!',
  email: s('email!').label('邮箱地址').error({
    required: '请输入邮箱地址'
  })
});

const validator = new Validator({ allErrors: true });
const result = validate(schema, data);
if (result.valid) {
  console.log(result.data);
}

Q: TypeScript 下如何写出更稳妥的链式提示?

A: 想保留 DSL 语法并获得 builder 方法提示时,建议从 s('...') 开始链式调用:

const schema = s({
  email: s('email!')
    .label('邮箱')
    .error({ format: '请输入有效邮箱地址' })
});

更多问题

如果您有其他问题:

  1. 查看 完整文档
  2. 查看 DSL 语法指南
  3. 查看 API 参考
  4. 提交 GitHub Issue

相关文档


对应示例文件

示例入口: faq.ts
说明: 把 FAQ 里最常被复制的 4 类场景放在一个可运行示例中: 单次验证、多语言错误、批量验证、缓存统计。