为什么改了一个 max,另一个表单也受影响?ArkTS 校验规则的共享实例问题
注册页要求用户名最多 8 个字符,资料编辑页允许 32 个字符。两个页面的规则很接近,把公共部分提取出来,看起来只需要几行代码。
但"共用规则定义"和"共用同一个规则实例"之间,有一个容易忽略的区别。
这篇用我维护的鸿蒙 ArkTS 校验库 @hmkit/validator,说明共享引用会怎样影响规则,以及 1.4.0 的 SchemaFactory 如何组织独立构造与派生。
示例使用 1.4.0:
bash
ohpm install @hmkit/validator@1.4.0
1. 两个变量,可能仍是同一个 Schema
先看这个例子:
typescript
import { liteV } from '@hmkit/validator/lite';
const base = liteV.string().required();
const shortName = base.max(8);
const longName = base.max(32);
console.info(`${shortName === longName}`); // true
console.info(`${shortName.validate('123456789').valid}`); // false
console.info(`${longName.validate('123456789').valid}`); // false
9 个字符本来符合"最多 32 个字符"的要求,为什么 longName 也没有通过?
原因有两层:
- 当前基础链式方法会修改自身,并返回当前实例。这里的三个变量引用同一个对象。
.max()会追加一条规则。继续调用.max(32),不会自动删除前面的.max(8)。
因此,这个实例同时包含两个最大长度约束,输入需要同时通过它们。最多 8 个字符的限制仍然生效。
这是当前可变 API 的行为约定。需要分别维护两种业务规则时,应让它们拥有独立实例。
2. 先用构造函数建立独立实例
最直接的解决方式,是复用创建规则的函数:
typescript
import { liteV, StringSchema } from '@hmkit/validator/lite';
function createNameRule(maxLength: number): StringSchema {
return liteV.string().required().max(maxLength);
}
const registerName = createNameRule(8);
const profileName = createNameRule(32);
console.info(`${registerName === profileName}`); // false
console.info(`${registerName.validate('123456789').valid}`); // false
console.info(`${profileName.validate('123456789').valid}`); // true
这里复用的是"如何创建规则",每次调用都创建新的 StringSchema。
如果业务只有两三个简单变体,普通函数已经足够清楚。随着字段组合、派生配置增加,也可以把创建与派生组织成统一接口。
3. 新建外层对象,也要看内部引用
对象派生还有一处需要分清。
partial() 返回新的对象 Schema,并独立维护哪些字段允许缺失;内部字段仍可能引用相同的子 Schema:
typescript
import { liteV } from '@hmkit/validator/lite';
const sharedName = liteV.string().required();
const original = liteV.object({ 'name': sharedName });
const patch = original.partial();
sharedName.max(8);
const input: Record<string, Object> = {
'name': '123456789'
};
console.info(`${original === patch}`); // false
console.info(`${patch.validate({}).valid}`); // true
console.info(`${original.validate({}).valid}`); // false
console.info(`${original.validate(input).valid}`); // false
console.info(`${patch.validate(input).valid}`); // false
patch 允许 name 缺失,并没有把 original 的字段也变成可选。但是,name 出现时,两者仍使用 sharedName 进行校验。
因此,"外层 Schema 不同"还不足以证明整个可变规则图已经隔离。对象字段、数组元素、联合分支和递归结构,都需要考虑子规则的创建位置。
4. 用 SchemaFactory 组织构造和派生
1.4.0 新增了可选入口 @hmkit/validator/reuse:
typescript
import { liteV, StringSchema } from '@hmkit/validator/lite';
import { SchemaFactory } from '@hmkit/validator/reuse';
const names = new SchemaFactory<StringSchema>(
(): StringSchema => liteV.string().required()
);
const shortNames = names.derive(
(schema: StringSchema): StringSchema => schema.max(8)
);
const longNames = names.derive(
(schema: StringSchema): StringSchema => schema.max(32)
);
const shortRule = shortNames.create();
const longRule = longNames.create();
console.info(`${shortRule.validate('123456789').valid}`); // false
console.info(`${longRule.validate('123456789').valid}`); // true
它的执行过程可以分成三步:
- 保存基础构造函数。
derive()保存派生配置,此时不执行构造。- 每次
create()都重新执行基础构造,再应用相应配置。
这样,各个变体可以共用创建逻辑,实际执行校验时使用各自的规则实例。派生配置也可以返回不同输出类型的 Schema,具体类型由返回的规则决定。
构造与派生回调可能被多次执行,因此适合放规则创建和配置逻辑。请求接口、写入外部状态等副作用应放在相应业务流程中处理。
5. 子规则也要在构造函数里创建
例如,一个资料对象包含姓名和标签数组,可以这样组织:
typescript
import { liteV, ObjectSchema } from '@hmkit/validator/lite';
import { SchemaFactory } from '@hmkit/validator/reuse';
function createProfileSchema(): ObjectSchema<Record<string, Object>> {
return liteV.object({
'name': liteV.string().required(),
'tags': liteV.array(liteV.string().required())
});
}
const profiles = new SchemaFactory<ObjectSchema<Record<string, Object>>>(
createProfileSchema
);
const firstProfile = profiles.create();
const secondProfile = profiles.create();
这里的对象、姓名规则、数组和数组元素规则,都在每次构造时重新创建。
相反,如果回调只是返回外部已经存在的实例:
typescript
const shared = liteV.string().required();
const reused = new SchemaFactory<StringSchema>(
(): StringSchema => shared
);
多次 create 仍然会得到 shared。SchemaFactory 执行你提供的构造函数,不会自动复制它返回的对象。
同样,可变默认对象、递归图以及自定义回调捕获的外部状态,也需要按照实际使用方式管理。
6. 为什么采用重新构造的方式
Schema 可能包含函数、插件、元数据和递归引用,简单的 JSON 序列化无法保留这些执行语义。
另一方面,原有链式方法采用可变约定,直接把所有方法改成返回新实例,会影响已有调用方式。
1.4.0 采用显式工厂,让需要隔离的业务变体从新的规则图开始,同时保留旧 API 的调用习惯。它提供的是按构造约定实现的隔离方案,任意已有 Schema 的通用深拷贝仍需要单独设计。
在项目中,我建议把规则构造函数放到独立模块,按场景创建实例;需要统一管理变体时,再使用 SchemaFactory。检查复用问题时,除了比较外层对象,也要核对内部可变子规则的来源。
开源地址与说明
欢迎带着可复现的规则组合和输入数据反馈问题,尤其是嵌套规则、递归结构与自定义插件的复用场景。