这篇文章里没有过多的技巧和经验,记录的是一个想法从诞生到实现的过程javascript
在上一篇文章 构建大型 Mobx 应用的几个建议 中,我提到过使用 schema 来约定数据结构。但遗憾的事情是,在浏览器端,我一直没有能找到合适的 schmea 类库,因此只能用 Immutable.js 中的 Record 代替。前端
若是你还不了解什么是 schema,在这里简单解释一下: 在应用内部的不一样组件之间,应用端与服务端之间,都须要使用消息进行通讯,而随着应用复杂度增加,消息的数据结构也变得复杂和庞大。对每一类须要使用的消息或者对象提早定义 schema,有利于确保通讯的正确性,防止传入不存在的字段,或者传入字段的类型不正确;同时也具备自解释的文档的做用,有利于从此的维护。咱们以 joi 类库为例java
const Joi = require('joi');
const schema = Joi.object().keys({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/),
access_token: [Joi.string(), Joi.number()],
birthyear: Joi.number().integer().min(1900).max(2013),
email: Joi.string().email({ minDomainAtoms: 2 })
}).with('username', 'birthyear').without('password', 'access_token');
// Return result.
const result = Joi.validate({ username: 'abc', birthyear: 1994 }, schema);
// result.error === null -> valid
// You can also pass a callback which will be called synchronously with the validation result.
Joi.validate({ username: 'abc', birthyear: 1994 }, schema, function (err, value) { }); // err === null -> valid
复制代码
就像能在 npm 上能找到的全部 schema 类库相似,它们始终在采起一种“过后验证”机制,即事先定义 schema 以后,再将须要验证的对象交给 schema 进行验证,这是让我不满意的。我更但愿采起 Reacord 的方式:git
const Person = Record({
name: '',
age: ''
})
const person = new Person({
name: 'Lee',
age: 22,
})
const team = new List(jsonData).map(Person) // => List<Person>
复制代码
在上面的例子中,schema 俨然拥有了相似于“类”的功能,你可以使用它建立指定数据结构的实例。若是你在建立实例时传入的属性没有事先定义便会报错。可是美中不足的是,Record 不支持更进一步的对每一个字段进行约束:指定类型、最大值和最小值等,就像在 joi 里看到的那样。github
介于找不到满意的 schema 类库,不如咱们本身编写一个。综上它须要具有如下两种能力:npm
在开发以前,咱们须要考虑而且约定未来如何使用它。关于这一点在上一小节中已经得出初步的结论了。json
假设类库名为 Schema
api
const PersonSchema = Schema({
name: '',
age: ''
})
复制代码
虽然咱们支持对字段约束,可是你能够不须要约束。那么采用以上的方式便可,仅仅约定了 schema 的字段名词,以及默认值数组
const person = PersonSchema({
name: 'Lee',
age: 22
})
复制代码
const PersonSchema = Schema({
name: Types().string().default('').required(),
age: Types().number().required()
})
复制代码
解释一下,理想状态下应该使用 React 中PropTypes
的方式对字段进行约束,例如PropTypes.func.isRequired
,可是一时想不到如何实现,因而提供Types
类辅佐以链式调用的方式曲线救国,能够约束的条件以下:浏览器
string()
: 仅限字符串类型number()
: 仅限数字类型boolean()
: 仅限布尔类型array()
: 仅限数组类型object()
: 仅限对象类型required()
: 该字段建立实例时必传default(value)
: 该字段的默认值valueof(value1, value2, value3)
: 该字段值必须是 value1, value2, value3 值之一固然还能够添加其余种类的约束,好比min()
、max()
、regex()
等等,这些二期再实现,以上才是目前来讲看来是最重要
const PersonSchema = Schema({
name: Types().string().default('').required(),
age: Types().number().required(),
job: Schema({
title: '',
company: ''
})
})
复制代码
Types
关于 Types 的链式调用 Types().string().required()
让我想到了什么?jQuery. jQuery 是如何实现链式调用的?函数调用的结束始终返回对 jQuery 的引用。
Types
是一个类,Types()
用于生成一个实例。你可能注意到没有使用关键词new
,由于我认为使用关键词new
是很鸡肋很累赘的事情。技术上不使用new
关键词生成实例也很容易,只要 1) 使用函数而不是 class
定义类; 2) 在构造函数中添加对实例的判断:
function Types() {
if (!(this instanceof Types)) {
return new Types();
}
}
复制代码
而至于对各类数据类型的验证,咱们借助而且封装lodash
的方法进行实现。用户每执行一个约束(.string()
)函数,咱们会生成一个内部的验证函数,存储在 Types
实例的 validators
变量中,用于未来对该字段值的判断
import _ from 'lodash'
const lodashWrap = fn => {
return value => {
return fn.call(this, value);
};
};
function Types() {
if (!(this instanceof Types)) {
return new Types();
}
this.validators = []
}
Types.prototype = {
string: function() {
this.validators.push(lodashWrap(_.isString));
return this;
},
复制代码
同理,咱们也实现了default
、required
和valueof
function Types() {
if (!(this instanceof Types)) {
return new Types();
}
this.validators = [];
this.isRequired = false;
this.defaultValue = void 0;
this.possibleValues = [];
}
Types.prototype = {
default: function(defaultValue) {
this.defaultValue = defaultValue;
return this;
},
required: function() {
this.isRequired = true;
return this;
},
valueOf: function() {
this.possibleValues = _.flattenDeep(Array.from(arguments));
return this
复制代码
Schema
经过咱们以前约定的 Schema()
的用法不难判断出 Schema
的基本结构应该以下:
export const Schema = definition => {
return function(inputObj = {}) {
return {}
}
}
复制代码
Schema
的代码实现中绝大部分并无什么特别的,基本上就是经过遍历 definition
来得到不一样字段的各类约束信息:
export const Schema = definition => {
const fieldValidator = {};
const fieldDefaults = {};
const fieldPossibleValues = {};
const fieldSchemas = {};
复制代码
上述代码中的fieldValidator
、fieldDefaults
都是“词典”,用于归类存储不一样字段的各类约束信息
在 definition
中咱们获取到了 schema 的定义,即对每一个字段(key)的约束。经过对字段值的各类判断,就能获得用于想表达的约束信息:
Types
的实例,表示用户只是定义了字段,但并无对它进行约束,同时当前值也是默认值。在建立实例或者对实例进行写操做时不须要任何校验Types
实例,那么咱们就能从实例的属性里取得各类约束信息,就是以前Types
定义里的意义validators
、defaultValue
、isRequired
、possibleValues
承接以上代码:
const fields = Object.keys(definition);
fields.forEach(field => {
const fieldValue = definition[field];
if (_.isFunction(fieldValue)) {
fieldSchemas[field] = fieldValue;
return;
}
if (!(fieldValue instanceof Types)) {
fieldDefaults[field] = fieldValue;
return;
}
if (fieldValue.validators.length) {
fieldValidator[field] = fieldValue.validators;
}
if (typeof fieldValue.defaultValue !== "undefined") {
fieldDefaults[field] = fieldValue.defaultValue;
}
if (fieldValue.possibleValues && fieldValue.possibleValues.length) {
fieldPossibleValues[field] = fieldValue.possibleValues;
}
});
复制代码
Schema
类的实现关键在于如何实现set
访问器,即如何在用户给字段赋值时进行校验,校验经过以后才容许赋值成功。关于如何实现访问器,咱们有两种方案进行选择:
Object.defineProperty
定义对象的访问器Object.defineProperty
的本质是对对象进行修改(固然你也可以深度拷贝一份原对象再进行修改,以免污染);而 Proxy 从“语义”上来讲更适合这个场景,也不存在污染的问题。而且在同时尝试了两个方案以后,使用 Proxy 的成本更低。因而决定使用 Proxy 机制,那么代码结构大体变为:
export const Schema = definition => {
return function(inputObj = {}) {
const proxyHandler = {
get: (target, prop) => {
return target[prop];
},
set: (target, prop, value) => {
// LOTS OF TODO
}
}
return new Proxy(Object.assign({}, inputObj), proxyHandler);
}
}
复制代码
而 set
方法中省略的则是循序渐进的各类判断代码了
本文的源码在 github.com/hh54188/sch…
你能够拷贝它,和它玩耍,测试它,修改它。但千万不要将它用在生产环境中,它尚未通过充分的测试,以及还有不少细枝末节和边界状况须要处理
欢迎经过 pull request 和 issues 提出更多的建议
本文同时也发布在个人 知乎前端专栏,欢迎你们关注