背景知识
我已经担任前端开发人员一年了。 我的第一个项目是“敌人”后端。 建立通信时,这并不是一个大问题。
但是在我们的情况下并非如此。
我们开发了代码,它依赖于后端向我们发送某些数据,某种结构和某种格式的事实。 而后端认为正常即可更改响应的内容-无需警告。 结果,我们花了几个小时才能确定网站的某些部分为何停止工作。
我们意识到,在依赖后端发送给我们的数据之前,需要检查后端返回的内容。 我们从前端创建了一个任务,用于研究数据验证问题。
这项研究是委托给我的。
我已经列出了我想用于数据验证的工具清单。
最重要的选择点是以下几点:
- 验证的说明性描述(方案),它转换为返回true / false(有效,无效)的验证器函数
- 低进入门槛;
- 验证数据与验证描述的相似性;
- 易于集成定制验证;
- 易于集成自定义错误消息。
结果,我查看了TOP-5(ajv,joi,roi ...),发现了许多验证库。 他们都很好。 但是在我看来,为了解决5%的复杂案件-他们注定95%的最常见案件相当冗长和笨重。
因此,我想:为什么不自己做一些适合我的事情。
四个月后,我的四方验证库的第七个版本问世了。
这是一个稳定的版本,经过全面测试,在npm上下载了11000次。 我们在一个广告系列中的三个项目中使用了三个月。
这三个月起了非常有用的作用。 四方展示了其所有优势。 后端没有数据问题。 每次他们更改答案时,我们都会立即出错。 查找错误原因的时间已大大减少。 几乎没有数据错误。
但是也发现了缺陷。
因此,我决定对其进行分析,并发布一个新版本,其中对开发过程中发生的所有错误进行了更正。
我将在下面讨论这些体系结构错误及其解决方案。
建筑耙
“ Stroko”-该方案的典型语言
我将以该人的对象为例,介绍该方案的旧版本。
const personSchema = { name: 'string', age: 'number', linkedin: ['string', 'null'] }
此方案验证具有三个属性的对象:名称-必须是字符串,年龄-必须是数字,链接到LinkedIn中的帐户-必须为null(如果没有帐户)或字符串(如果有帐户)。
该方案满足了我对可读性,与经过验证的数据的相似性的要求,并且我认为学习编写此类方案的入门门槛并不高。 此外,可以使用打字稿中的类型定义轻松编写这样的方案:
type Person = { name: string age: number linkedin: string | null }
(如您所见-这些更改很可能是装饰性的)
当我做出决定时,最常用的验证选项应使用什么(例如,上面使用的那些选项)。 我选择使用-字符串,即验证者的名称。
但是字符串的问题在于它们不能用于编译器或错误分析器。 它们的字符串“数字”与“数字”没有太大区别。
解决方案
四重奏 8.0.0的新版本。 我决定从四重奏中删除-使用字符串作为方案中验证器的名称。
现在该图如下所示:
const personSchema = { name: v.string age: v.number, linkedin: [v.string, null] }
此更改具有两个主要优点:
- 编译器或错误分析器-能够检测方法名称是否拼写错误。
- 行-不再用作架构元素。 这意味着对于他们来说,您可以在库中选择新功能,下面将对其进行描述。
TypeScript支持
通常,前七个版本是使用纯Javascript开发的。 当切换到带有Typescript的项目时,需要以某种方式适应它的库。 因此,编写了库的类型声明。
但这是一个缺点-添加功能或更改库的某些元素时,总是很容易忘记更新类型声明。
这类不便之处还包括:
const checkPerson = v(personSchema)
当我们在第(0)行上创建对象验证器时。 我们想检查第(1)行中来自后端的真实答案并处理错误。 在第(2)行中,该person
类型为Person。 但这没有发生。 不幸的是,这种检查不是类型保护。
解决方案
我决定将整个四方库重写为Typescript,以便编译器可以检查该库与类型的对应关系。 在此过程中,我们在返回编译的验证器的函数中添加了一个类型参数,该参数将确定此类型的验证器是什么类型的防护器。
一个示例如下所示:
const checkPerson = v<Person>(personSchema)
现在在第(2)行上, person
属于Person
类型。
易读性
在两种情况下,代码读取效果很差:检查是否符合一组特定的值(检查枚举)和检查对象的其他属性。
a)检查枚举
最初,有一个主意,我认为是个好主意。 我们将通过在我们的对象中添加字段“性别”进行演示。
电路的旧版本如下所示:
const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum('male', 'female') }
该选项可读性强。 但是像往常一样,一切都超出了计划。
例如,在程序中有一个已声明的枚举:
enum Sex { Male = 'male', Female = 'female' }
自然,我想在电路内部使用它。 因此,当更改其中一个值(例如“ male”->“ m”,“ female”->“ f”)时,验证方案也应更改。
因此,几乎总是枚举验证是这样写的:
const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum(...Object.values(Sex)) }
看起来很笨重。
b)验证对象的剩余属性
假设我们在对象中添加了这样的特征-它可以具有其他字段,但是所有字段都必须是指向社交网络的链接-这意味着它们必须为null
或字符串。
旧方案如下所示:
const personSchema = { name: 'string', age: 'number', linkedin: ['null', 'string'], sex: v.enum(...Object.values(Sex)), ...v.rest(['null', 'string'])
此项突出显示了剩余的属性-已列出的属性。 使用散布运算符更可能使想要了解此方案的人感到困惑。
解决方案
如上所述,字符串不再是验证方案的一部分。 仅有三种类型的Javascript值保留为验证方案。 对象-描述对象的验证方案。 描述数组-几个有效性选项。 功能(库生成或自定义)-用于所有其他验证选项。
此规定允许添加功能,从而可以多次提高电路的可读性。
实际上,如果我们想将值与字符串“ male”进行比较该怎么办。 除了值本身和字符串“ male”之外,我们真的需要了解其他信息吗?
因此,决定添加基本类型的值作为电路的元素。 因此,在方案中遇到原始值的地方,这意味着这是根据此方案创建的验证器应检查的有效值。 我最好举个例子:
如果我们需要检查数字是否等于42。 然后我们这样写:
const check42 = v(42) check42(42)
让我们看看这如何影响人员模式(不考虑其他属性):
const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string],
使用预定义的枚举,我们可以这样重写它:
const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], sex: Object.values(Sex)
在这种情况下,使用枚举方法和使用散布运算符将对象中的有效值作为参数插入此方法的形式,消除了不必要的礼节。
什么是原始值:数字,字符串,字符, true
, false
, null
和undefined
。
也就是说,如果我们需要将这些值与它们进行比较,则只需使用这些值即可。 还有一个验证库-它会创建一个验证器,该验证器严格将值与方案中指定的值进行比较。
为了验证残差属性,选择对对象的所有其他字段使用特殊属性:
const personSchema = { name: v.string, age: v.number, linkedin: [null, v.string], sex: Object.values(Sex), [v.rest]: [null, v.string] }
因此,电路看起来更具可读性。 还有更多类似Typescript广告的广告。
验证器与创建它的功能有关
在旧版本中,错误说明不属于验证程序。 它们被添加到v
函数内部的数组中。
以前,为了获得验证错误的解释,您必须拥有一个验证器(以进行检查)和v(以获取无效性的解释)。 所有这些看起来如下:
a)我们在图中添加说明
const checkPerson = v({ name: v('string', 'wrong name') age: v('number', 'wrong age'), linkedin: v(['null', 'string'], 'wrong linkedin'), sex: v( v.enum(...Object.values(Sex)), 'wrong sex value' ), ...v.rest( v( ['null', 'string'], 'wrong social networks link' ) )
对于电路的任何元素-您可以使用v编译器函数的第二个参数添加对错误的解释。
b)清除说明数组
在验证之前,有必要清除此全局数组,在验证过程中所有说明都记录在其中。
v.clearContext()
c)验证
const isPersonValid = checkPerson(person)
在此检查过程中,如果发现有效性,则在创建电路的阶段-给出了解释,该解释位于v.explanation
全局数组中。
d)错误处理
if (!isPersonValid) { throw new TypeError('Invalid person response: ' + v.explanation.join('; ')) }
如您所见,这里有一个大问题。 因为如果我们要使用验证器而不是创建它的地方。 我们不仅需要将其传递给参数,还要传递创建它的函数。 因为数组位于其中,所以将在其中添加说明。
解决方案
该问题的解决方法如下:解释成为验证功能本身的一部分。 从其类型可以理解:
类型Validator =(值:任何,解释?:任何[])=>布尔值
现在,如果您需要对错误进行解释,则将要添加解释的数组传递给该数组。
因此,验证器成为一个独立的单元。 还添加了一种方法,该方法可以将验证函数转换为以下函数:如果该值有效,则返回null;如果该值无效,则返回说明数组。
现在,带有说明的验证如下所示:
const checkPerson = v<Person>({ name: v(v.string, 'wrong name'), age: v(v.number, 'wrong age'), linkedin: v([null, v.string], 'wrong linkedin') sex: v(Object.values(Sex), 'wrong sex') [v.rest]: v([null, v.string], 'wrong social network') })
后记
我强调了三个前提,因此必须重写所有内容:
- 希望人们在写台词时不会弄错
- 使用全局变量(在这种情况下,为v.explanation数组)
- 在开发过程中使用小示例进行测试-没有显示在真正的大案例中使用时出现的问题。
但是我很高兴对这些问题进行了分析,并且该版本已经在我们的项目中使用。 我希望它对我们有用,不少于上一个。
谢谢大家的阅读,希望我的经验对您有所帮助。