- 教育
- 后端
- 前端
【免费下载链接】oppia
A free, online learning platform to make quality education accessible for all.
本篇技术指南以 Oppia 仓库中 core/templates/components/forms/validators/README.md 为骨架,深入解析 Oppia 表单 schema 校验体系的核心约定:前端过滤器名称必须与后端验证器名称一一对应(下划线命名转为 camelCase),并完整讲解 9 个内置 SchemaValidators 的行为、参数、边界条件与测试验证,同时给出前端消费链路(applyValidation 指令、schema-based 编辑器)与后端实现(core/schema_utils.py)的源码级对照。读完本文,你将掌握 Oppia 中"写一个验证器、挂到表单、在前后端同时生效"的完整方法与排查思路。
一、核心契约:过滤器名 ↔ 后端验证器名的对应规则
原 README 全文仅两条约束,却是整个校验体系的地基:
- 命名对应:这些过滤器(filters)的名称必须与后端验证器(backend validators)的名称对应,转换规则为"下划线转 camelCase"。例如后端验证器
has_length_at_least对应前端过滤器hasLengthAtLeast,is_url_fragment对应isUrlFragment。 - 参数信任:这些过滤器不会校验 schema 中验证器定义附带传入的参数,即参数(如
minValue、maxValue、charLimit)被假定为正确配置,前端只负责执行校验逻辑本身。
这条约定直接决定了前端如何把 schema 里写死的字符串 id(下划线风格)动态映射到 TypeScript 类方法。实现依赖 underscores-to-camel-case.pipe.ts:
transform(input: string): string { return input.replace(/_+(.)/g, function (match, group1) { return group1.toUpperCase(); }); }_+(.)正则会把每个"下划线 + 一个字符"替换为该字符的大写形式,从而完成has_length_at_least → hasLengthAtLeast的转换。
二、前端执行入口:validate() 函数
前端所有校验最终汇聚到 schema-validators.ts 中导出的validate()函数。它的执行流程是:
- 若
validators为空或长度为 0,直接返回null(通过校验); - 遍历每个 validatorSpec,用
underscoresToCamelCasePipe.transform(validatorSpec.id)得到方法名; - 将 spec 中除
id外的其余字段通过同样的管道转换为 camelCase 参数,并用cloneDeep深拷贝,避免外部对象被意外修改; - 调用
SchemaValidatorsvalidatorName(control),得到ValidationErrors | null; - 只要存在任一错误,就合并
allValidationErrors并返回;全部通过则返回null; - 若方法名在
SchemaValidators上不存在,当前实现静默忽略(源码留有TODO(#15190): Throw an error if validator not found,即"未找到验证器时报错"尚未落地)。
validate()的第二个参数numberConversionService是 Oppia 校验体系的特色:所有数值类校验都先经过它做本地化数字转换(详见第四节)。
三、内置 SchemaValidators 逐一详解
SchemaValidators类(schema-validators.ts)共定义 9 个静态方法,每个都返回一个高阶函数(control) => ValidationErrors | null,完全兼容 AngularAbstractControl校验签名。
3.1 hasLengthAtLeast / hasLengthAtMost:长度上下界
| 方法 | 参数 | 说明 |
|---|---|---|
hasLengthAtLeast | minValue: number | 值长度 ≥minValue才通过 |
hasLengthAtMost | maxValue: number | 值长度 ≤maxValue才通过 |
两者共同点:
- 支持字符串与数组两种值(校验
typeof value === 'string' || Array.isArray(value)),数组场景用于校验多项选择/集合类输入的元素个数; - 值为
null/undefined时不通过,错误对象形如{ hasLengthAtLeast: { minValue, actual } }; - 传入非字符串、非数组的其它类型会抛出异常
'Invalid value passed in control. Expecting a string or Array'(测试用例中显式断言了该行为)。
3.2 isAtLeast / isAtMost:数值上下界(含本地化处理)
| 方法 | 参数 | 说明 |
|---|---|---|
isAtLeast | minValue: number | 值 ≥minValue通过 |
isAtMost | maxValue: number | 值 ≤maxValue通过 |
与长度类校验的关键差异:比较前先调用numberConversionService.convertToEnglishDecimal(String(control.value)),把当前语言环境下的十进制输入(如法语环境1,5)统一转成英文小数点形式再比较;转换失败返回null时按"未通过"处理,错误对象形如{ isAtLeast: { minValue, actual } }。
3.3 isFloat:浮点数校验(含百分比与多语言分隔符)
isFloat(null, numberConversionService)无参数,但内部逻辑最复杂,依赖正则:
var FLOAT_REGEXP = /(?=.*\d)^\-?\d*(\.|\,)?\d*\%?$/;按源码注释,该正则接受以下形式(.换成,同样合法):
0.、0.55、-0.55、.555、-.555;- 带百分号的形式
5.1%、1%等(%只能出现在末尾,3%%不合法); - 对百分号输入,会去掉末尾
%后交给convertToEnglishDecimal转换(百分比按÷100语义处理); - 空字符串、
null直接通过(control.value.toString().trim()为''时返回null)。
值得注意的边界(来自 schema-validators.spec.ts 的注释):'1,5'在英文环境下会被当作合法输入,因为convertToEnglishDecimal只转换"当前激活语言环境"的分隔符,非本地的,原样保留,随后parseFloat截断为1——测试注释明确标注了TODO(#26405): Handle non-locale separators in NumberConversionService,即"非本地方言分隔符"的完整处理尚未实现。法语环境下则相反:1,5、,3、2,5%均合法。
3.4 isInteger:整数校验
isInteger()无参数,实现极简:Number.isInteger(Number(control.value))为假则返回{ isInteger: { actual } }。测试用例表明'3'、'-3'、'3.0'通过,'3.5'失败。
3.5 isNonempty:非空校验
isNonempty()无参数:值为null或value?.length === 0(空字符串/空数组)时返回{ isNonempty: true }。
3.6 isRegexMatched:正则匹配校验
isRegexMatched({ regexPattern })用new RegExp(regexPattern)构造正则后对control.value执行re.test()。测试覆盖了典型场景:
a.$匹配'a '、'a$'、'a2',拒绝'a'、'a$a'、'bb';g(oog)+le匹配'google '、'googoogle';- URL 白名单式正则
(^https:\/\/.*)|(^(?!.*:\/\/)(.*))放行https://...与无协议串www.oppia.com,拒绝http://与abc://...; - 值为
null时直接通过。
3.7 isUrlFragment:URL 片段校验
isUrlFragment({ charLimit })是唯一一个组合了常量正则 + 参数长度的验证器:
const VALID_URL_FRAGMENT_REGEX = new RegExp( AppConstants.VALID_URL_FRAGMENT_REGEX );该常量定义于 assets/constants.ts:"^[a-z]+(-[a-z]+)*$",即只允许小写字母和连字符(连字符只能出现在单词之间,不能位于首尾或连续)。通过条件为:值非空、正则匹配、且value.length <= args.charLimit。测试用例系统性验证了 30 余种非法字符(~、`、!、@、#等)、空格、大写字母、数字及超长输入全部失败;'math'、'computer-sciencet'通过。
四、后端对应实现:core/schema_utils.py
README 所说的"后端验证器"位于 core/schema_utils.py。其中_Validators类(get_validator()通过 schema_utils.py 的get_validator(validator_id)按 id 取方法)提供了同名验证器,例如:
has_length_at_least(obj: List[str], min_value):len(obj) >= min_value(schema_utils.py);has_length_at_most(obj: List[str], max_value):len(obj) <= max_value(schema_utils.py);is_nonempty(obj: str):bool(obj)(schema_utils.py);is_at_least/is_at_most:obj >= min_value/obj <= max_value(schema_utils.py);is_url_fragment(obj: str):基于constants.VALID_URL_FRAGMENT_REGEX的re.match(schema_utils.py);is_regex_matched(obj, regex_pattern):re.match(regex_pattern, obj)(schema_utils.py)。
后端在 schema 归一化流程(normalize())中消费这些验证器:对 schema 声明的validators与global_validators,逐条执行"取 id →get_validator(id)→ 传入对象与 kwargs → 断言结果为真",失败即抛出utils.InvalidInputException(schema_utils.py)。从源码结构看,后端同一 id 直接以下划线命名,前端则以 camelCase 同名映射,这正是 README 第一条约定存在的意义——同一份 schema 可以在 Python 后端与 Angular 前端共享同一套校验语义。
五、前端消费链路:指令与 schema-based 编辑器
5.1 applyValidation 指令
apply-validation.directive.ts 是通用接入点:指令通过@Input() validators接收校验器数组,实现 AngularValidator接口并注册进NG_VALIDATORS。其validate()逻辑与validate()函数几乎一致(同样的下划线转驼峰、cloneDeep参数、合并错误),但它直接以指令形式挂载在任意表单控件上,典型用法是在模板中写[applyValidation]="schema.validators"。
5.2 schema-based 编辑器
以 schema-based-unicode-editor.component.ts 为例:组件实现ControlValueAccessor, Validator,其validate(control)直接委托给validate(control, this.validators, this.numberConversionService)。这意味着任何 schema-based 编辑器(unicode/int 等)声明了 validators 后,校验自动生效,无需逐组件重复实现。同类消费还包括 schema-based-int-editor.component.ts,它通常搭配isAtLeast/isAtMost/isInteger使用。
六、测试验证:schema-validators.spec.ts
schema-validators.spec.ts 用MockFormControl(继承AbstractControl的最小实现)对每个验证器做了参数化用例验证,可以当作行为规格说明书使用:
- 长度类:
hasLengthAtLeast({minValue: 3})对'123'、['1','2','3']通过,对'12'、null失败;非字符串抛异常; - 数值类:
isAtLeast({minValue: -2.0})对-2通过、-2.01失败;isAtMost语义相反; - 浮点:英文/法语双语言环境各 20+ 用例,含百分比、前后空格、非法串(
--1.23、=1.23、1.23a、2+3); - isUrlFragment:
charLimit: 20下完整覆盖非法字符集与长度超限; - isRegexMatched:通过/失败两组正则用例集。
测试同时印证了 README 的"参数信任"约定——测试直接向过滤器传入{minValue: 3}这类参数,从不对参数本身的合法性做校验。
七、注意事项与扩展边界
在 Oppia 中新增一个表单校验器时,需遵循以下既有约定:
- 命名同步:在 core/schema_utils.py 的
_Validators中新增下划线命名的静态方法,同时在 schema-validators.ts 的SchemaValidators类中新增同名 camelCase 方法,否则前端会静默跳过(TODO(#15190)尚未启用报错路径)。 - 参数自洽:前端不校验参数,因此 schema 中写错
minValue类型/范围不会得到前端警告,但后端normalize()会在断言失败时抛出InvalidInputException——开发时应优先保证 schema 参数正确。 - 本地化数字:涉及数值的校验(
isFloat/isAtLeast/isAtMost)依赖NumberConversionService的语言环境转换,当前实现只处理激活语言的分隔符,跨语言分隔符输入存在已知缺口(TODO(#26405)),设计表单时应避免依赖非本地分隔符。 - URL 片段约束:
isUrlFragment的字符集由全局常量VALID_URL_FRAGMENT_REGEX(^[a-z]+(-[a-z]+)*$)统一控制,前后端共用,修改该常量会影响所有使用此验证器的表单。
通过上述前后端一一对应的验证器体系,Oppia 得以让同一份 JSON schema 在 Python 服务端与 Angular 客户端给出完全一致的校验行为,这也是其大规模表单(交互配置、题目编辑器、自定义对象 schema)得以安全演进的基础设施之一。
- 教育
- 后端
- 前端
【免费下载链接】oppia
A free, online learning platform to make quality education accessible for all.
相关推荐
Toga Validators 输入校验指南:从内置校验器到自定义校验器
Toga Validators 输入校验指南:从内置校验器到自定义校验器 导读 本文系统讲解 Toga(Python 原生、各操作系统原生 GUI 工具包)中
跨平台UI组件桌面应用Django REST Framework 校验器(Validators)完全指南:从唯一性约束到自定义校验逻辑
Django REST Framework 校验器(Validators)完全指南:从唯一性约束到自定义校验逻辑 本指南围绕 Django REST Frame
后端API网关Web框架Tortoise ORM 字段校验器(Validators)完全指南:内置校验器、自定义校验与底层原理
Tortoise ORM 字段校验器(Validators)完全指南:内置校验器、自定义校验与底层原理 导读 本文聚焦 Tortoise ORM 的字段校验器(
数据库后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考