如何用suchjs来模拟数据
开发中,经常遇到数据接口没有准备好,前端无法进行正常开发的情况,所以一直有个想法,想把数据模拟的工作变得更简单、易扩展、易读、甚至能从一个已知结构的一条数据模拟出多条数据。当然这不是一个简单的工作,但我觉得这是件有意义的事情,至少设计的初衷让它具备自己的独特性,不会因为目前已有不少优秀的mock数据的库或框架,而让它变成一件重复造轮子的事情。
从易读性出发,因此对数据进行描述性的解读是个不错的想法。我试图提取一些数据通用的属性,用一些约定的符号来表达。于是便有了以下这些总结:
1. 中括号[a,b]
可以用来表示范围,对于数字就是数字的最小值,最大值;而对于字符串,就可以表示为数字的unicode码点范围。
2. 大括号{a,b}
可以用来表示长度,对于字符是长度大小,对于数组,也可表示数组的长度大小。
3. 斜杠/a/
用来表示正则表达式。
4. 且&./a/b
用来表示对数据的引用,也可以表示为其它路径的引用。
5. #[a=1,b=2]
定义一些变量值,可用来辅助数据生成。
6. @a|b
定义方法管道,可以对数据进行最后的处理。
7. 百分号%
用来设置数据的格式化,比如数字类型,时间类型。
8. 英文冒号:
用来标记类型,同时也作为各个参数之间的分隔符。
通过以上的这些想法,我需要一个简单的分析器来将这些配置字符做出分隔,这也就是源码中的parser所做的事情。每个配置类型都有自己的起始符,可能有自己的结束符,也可能是一个能匹配到结束符的正则。但这里开始符和结束符的组合必须是唯一的,否则容易造成错乱。最终这些解析每段配置的parser会被放到一个dispatcher的分配器里,分配器按照最长优先匹配的原则来判断最终使用哪个parser来解析。于是这些不同的配置被解析成了一个个规则对象,里面包含了配置数据所需的所有信息,为接下来的工作做准备。
接下来的工作就是要实现具体的数据类型了,上面已经获得了各种属性配置的值,具体类型使用的时候可能对具体的配置值有不同的要求,因此每个模拟类型里需要写一些自己的对配置验证的方法,以及针对配置如何生成结果的generate方法。上面说的有两个配置是比较特殊的,一个是#[]里面的变量配置,一个是@a|b这类的方法配置,所有的类型都支持这两种配置,对于变量配置,会有一个configOptions的数据验证配置,以保证设置的参数是符合规范的。其他的配置则通过添加rule的方式,在获取到配置后会对数据进行验证或更改。最终会在这些都完成后,在generate方法里生成最终的结果数据。
这里内置支持的类型有:
1. string
:string[97,120]:{10,20}
如上示例,表示一个字符串,长度为10到20之间,每个字符的码点范围都在97到120之间。注意类型和第一个属性配置之间的冒号是可以省略的。
2. number
:number[100,200]:%.2f
如上示例,表示一个数字,大小在100到200之间,注意默认得到的值都是一个浮点数,所以如果需要得到整数值,可以使用类似c的printf方法进行格式化,如%d。示例中表示将数字转化为一个带两位小数点的浮点数。
3. date
:date['2018-12-03 00:00:00','2018-12-05 00:00:00']:%yyyy-mm-dd HH\\:MM\\:ss
示例有点长,您应该已经看出来了,这表示一个时间范围在2018年12月3日和12月5日之间,最后格式化为年月日时分秒的一个日期。实际上日期范围内的参数除了支持能被Date实例化的合理日期格式,也支持像php方法strtotime方法类似tomorrow,+1 week这种日期写法,可以更灵活的使用。
4. regexp
:regexp/[\\u{4e00}-\\u{9afa}]{3,5}/u
表示一个匹配正则表达式的字符,这里支持的正则表达式flags包括u,s,i,由于是反解析正则,所以对一些功能有所限制,但也扩展支持了命名匹配的功能。可以查看reregexp这个包的说明。
5. id
:id#[start=2,step=2]
通常用在数组里,表示一个自增的值,可以设置start和step属性,来表示开始值和每次递增的大小,默认都是1。
6. ref
:ref&./a,./b:@join('|')
表示一个对数据结构和自身同级的字段a和字段b的引用,引用到数据后,可以通过方法配置来做进一步的处理。比如示例中的join方法,因为引用多个字段会转化为数组,所以可以调用数组的原生join方法。
以上就是目前所有内置类型,实际使用中,虽然基本可以使用正则来解决大部分的数据模拟,但在配置这些参数时会变得非常困难,做一些重复的工作,而且难以维护。所以需要提供一些入口,来让模拟基本数据之外的一些数据也变得更简单。
于是在开发中,提供了以下接口方法:
Such.define()
定义新类型,第一个参数是和string、number这些类似的类型名,第二个参数可以是一个方法;也可以是一个基础类型名,表示这个新类型是从基础类型扩展而来,不过它固定了某些参数配置;还可以是一个包含generate、init等方法的对象。
这里分别举个简单例子:
Such.define('boolean',(options) => { return options.such.utils.isOptional(); });
这里的options里包含了such,及全局的Such对象,Such对象上加入了静态属性utils,包含了系统内置使用的一些常用utils方法。同时包括datas,dpath这两个可通过数组作为key设置获取数据的类map对象。datas里保存了已模拟出来的数据,dpath保存了一个类似xpath的当前路径数组。有了这些数据,就能更灵活地对数据进行模拟了。
Such.define('integer', 'number', '%d')
模拟了一个整数类型,这个类型继承自number,不过它的format参数配置被固定为%d。
第三种方式定义的类型更全面,和定义原始类型方式是一致的。可以参考一下dict类型的实现。
以上就是define方法的简单介绍,也可以看看 such:recommend 里扩展的一些类型的写法。总之通过自定义类型,可以快速地扩展类型列表,对于node版本,你也可以把一些通用的类型整理开放出来,做成一个npm包,提供给他人使用。
Such.alias('short','long-short')
这个不用说,大家就知道这是用来表示别名的,比如上面扩展的integer类型,你觉得integer写得太费劲了,就可以Such.alias('int','integer'),记住前面写短的,后面写长的。可以想象类似bash里alias int=interger,千万别写反了。
有了以上两个方法,基本的类型扩展用起来算是比较方便了,如果您有更好的意见,也欢迎您在Issue提出来,为以后的功能扩展提供更多参考。
说到上面的node版本,为了让对类型扩展使用起来更方便,支持了扩展包载入的方式,方便大家可以共享一些类型扩展等。如果想要使用这些扩展类型该怎么做呢?
Such.config(config)
通过这个config方法,可以将config里的参数使用对应的Such全局方法将其注册进来。
{"extends":["such:recommend"],"types":{"integer":["number","%d"],alias:{"int":"integer"},"parsers":{}}
上面的config参数大概就长成这样了,对于node版本,上面的extends才是生效的,such:开头的表示是内置的扩展,其它的就可以根据包名填入了。当然,在node版本里,不需要我们自己来执行Such.config方法,而是在项目根目录配置一个such.config.js的配置文件就好了。
如果你还想将你要模拟的数据使用一个json文件替代,你可能需要创建一个文件目录来作为suchjs模拟的根目录,用来保存这些文件。如果你还想支持一个多行的dict文件,从里面随机取一条数据做展示的话,你还需要在suchjs根目录下创建一个数据文件目录。这样你就可以很规范地使用它们来输出模拟数据了。
详细的介绍可以查看项目的readme
当然,为了上述操作使用起来更方便,目前提供了一个乞丐版的cli(太简陋,只支持一个命令),所以你的操作可能是这样的。
1、进入你的node项目根目录
2、npm install --save-dev such-cli
3、such init
好吧,更多的细节说明放在这个不太详细的中文文档里了,感兴趣的同学可以看一看。
最后,贴一下项目的地址:https://github.com/suchjs/such
感谢大家花费宝贵的时间看到这最后,希望能对实际有数据模拟需求的同学有用。如果您尝试使用了它,遇到任何问题或者有任何想法,还请在issue里提出来,我一定会认真研究的。再次感谢!