Promises 理念已经改变了编写异步 JavaScript 的方式。近一年来,许多框架都集成了 Promise 模式,让编写、阅读、维护异步代码更加容易。比如,jQuery 添加了 $.Deferred() API, NodeJS 有服务器端和客户端都能用的 Q 和 jspromise 模块。客户端 MVC 框架,诸如 EmberJS 、 AngularJS 也实现了自己的 Promises。
不止于此,我们可以重新考量原来的解决方案,将 Promises 应用到这些方案中。本文将使用 Promise 模式来验证一个表单,以展示这个 API。
什么是 Promise?
简言之,Promises 通知一个操作的结果,无论结果是成功还是失败。操作本身遵守某种简单的契约。这里选择使用 “契约(原文使用 contract)”一词,因为开发者可以按不同的方式设计这个约定。感谢地是,开发者社区对此达成一致并形成了名为 Promises/A+ 的规范。仅当操作真正完成后,根据 Promises/A+ 约定通知结果。换句话说,无论操作结果如何,都“承诺”在操作完成的时候通知用户。
操作返回一个 promise
对象,开发者可以使用 done()
或 fail()
方法添加回调函数。操作通过调用 promise.resolve()
或 promise.reject()
实现结果通知。
表单验证中使用 Promises
传统的表单验证中,一般是每一个域编写一个验证程序,这个验证程序中包含校验规则、返回给用户的提示信息。当需要验证的域越来越多时,就需要不断添加验证程序,混乱不堪。对于效率、维护、代码重用都是一个问题。基于 Promise 的方案
再来看表单验证问题,这是一系列可能成功也可能失败的操作。这些结果可以捕获为一个Promise
。下面将基于 jQuery 的 $.Deferred() Promise 实现创建一个校验框架。
基于 Promises 的验证框架
假如表单有三个域: Name、Email 、Address。<form> <div class="row"> <div class="large-4 columns"> <label>Name</label> <input type="text" class="name"/> </div> </div><div class="row">
<div class="large-4 columns">
<label>Email</label>
<input type="text" class="email"/>
</div>
</div><div class="row">
<div class="large-4 columns">
<label>Address</label>
<input type="text" class="address"/>
</div>
</div></form>
首先将校验项定义到配置对象中,这也是验证框架的 API:
var validationConfig = { '.name': { checks: 'required', field: 'Name' }, '.email': { checks: ['required'], field: 'Email' }, '.address': { checks: ['random', 'required'], field: 'Address' } };
这个保存配置信息的对象中,键(key)是 jQuery 选择符,值是有以下两个属性的对象:
checks
:字符串或数组,要验证的项。field
:用户可读的表单域名称,在显示错误是会用到。
V.validate(validationConfig) .done(function () { // Success }) .fail(function (errors) { // Validations failed. errors has the details });
done()
、fail()
是处理 Promise 结果的默认回调函数。如果需要验证更多的域,只要扩展validationConfig
对象即可,不涉及到其他的设置(开闭原则-Open-Closed Principle) 。实际上,可以通过扩展验证程序框架(稍后将看到)来添加其他验证,比如 email 唯一性。
这就是验证程序框架的面向用户的 API,现在,来看一下底层操作。
底层验证程序
验证程序是有两个属性的对象:type
:包含不同的校验项(如必填、email格式为两个校验项),同时是添加更多校验项的扩展接口。validate
:根据配置执行验证的核心方法。
var V = (function ($) {var validator = {
/*
- Extension point - just add to this hash
- V.type['my-validator'] = {
- ok: function(value){ return true; },
- message: 'Failure message for my-validator'
- }
*/
type: {
'required': {
ok: function (value) {
// is valid ?
},
message: 'This field is required'
},
...
},
/**
*
- @param config
- {
- '': string | object | [ string ]
- }
*/
validate: function (config) {
// 1. Normalize the configuration object
// 2. Convert each validation to a promise
// 3. Wrap into a master promise
// 4. Return the master promise
}
};
})(jQuery);
validate
方法是这个验证程序框架的基础,如上面的注释里所示,验证通过四个步骤实现:
1. 标准化配置对象
function normalizeConfig(config) { config = config || {}; var validations = []; $.each(config, function (selector, obj) { // make an array for simplified checking var checks = $.isArray(obj.checks) ? obj.checks : [obj.checks]; $.each(checks, function (idx, check) { validations.push({ control: $(selector), check: getValidator(check), checkName: check, field: obj.field }); }); }); return validations; } function getValidator(type) { if ($.type(type) === 'string' && validator.type[type]) return validator.type[type]; return validator.noCheck; }
循环处理配置对象,以在 validate
方法中使用。
通过 getValidator()
辅助程序确定验证程序,如果不需要验证,则返回始终返回真的 noCheck
验证程序。
2. 将校验项转换 Promise
通过检查 validation.ok()
的返回值,确保每一个校验都是一个 Promise。如果返回值包含 then()
方法,根据 Promises/A+ 规范,可以确定这就是一个 Promise;否则创建一个临时的 Promise ,接受或者驳回由返回值决定。
validate: function (config) { // 1. Normalize the configuration object config = normalizeConfig(config); var promises = [], checks = []; // 2. Convert each validation to a promise $.each(config, function (idx, v) { var value = v.control.val(); var retVal = v.check.ok(value); // Make a promise, check is based on Promises/A+ spec if (retVal.then) { promises.push(retVal); } else { var p = $.Deferred(); if (retVal) p.resolve(); else p.reject(); promises.push(p.promise()); } checks.push(v); }); // 3. Wrap into a master promise // 4. Return the master promise }
3. 包装到 master Promise
上一步创建了一个 Promises 数组,接下来,将所有 Promises 包装到单一的 Promise 中,然后传递结果。如果全部通过验证,则在 master promise 上调用 resolve() 。
如果发生错误,则循环处理 promises
数组,读取 state()
结果,然后将驳回的 promises 收集到 failed
数组中,并调用 master promise 的 reject()
方法:
// 3. Wrap into a master promise var masterPromise = $.Deferred(); $.when.apply(null, promises) .done(function () { masterPromise.resolve(); }) .fail(function () { var failed = []; $.each(promises, function (idx, x) { if (x.state() === 'rejected') { var failedCheck = checks[idx]; var error = { check: failedCheck.checkName, error: failedCheck.check.message, field: failedCheck.field, control: failedCheck.control }; failed.push(error); } }); masterPromise.reject(failed); }); // 4. Return the master promise return masterPromise.promise();
4. 返回 master promise
最终从 validate()
方法返回 master promise。这就是客户端代码设置 done()
及 fail()
回调函数的 Promise。
第二、三步是这个框架的关键所在。通过把每一个校验标准化到一个 Promise,就可以进行一致地处理。使用一个 master Promise 对象,可以更好的控制,而且可以附加额外的上下文信息,这些信息对终端用户来说可能是有用的。
使用验证程序
演示 文件是对这个验证框架的完整应用,使用done()
回调函数报告成功信息,使用 fail()
显示错误信息。下面的是成功和失败时的截图:
这个演示使用文章开头的 HTML 结构和验证配置,增加的代码用来显示提示信息。
function showAlerts(errors) { var alertContainer = $('.alert'); $('.error').remove(); if (!errors) { alertContainer.html('<small class="label success">All Passed</small>'); } else { $.each(errors, function (idx, err) { var msg = $('<small></small>') .addClass('error') .text(err.error); err.control.parent().append(msg); }); } } $('.validate').click(function () { $('.indicator').show(); $('.alert').empty(); V.validate(validationConfig) .done(function () { $('.indicator').hide(); showAlerts(); }) .fail(function (errors) { $('.indicator').hide(); showAlerts(errors); }); });
扩展验证程序
文章开头提到可以通过扩展验证程序的type
散列来添加更多的校验。以 random
验证程序为例,这是一个随机成功或失败的校验,没有实际意义,不过可以用来理解如何实现扩展验证程序:
- 使用
setTimeout()
实现校验异步,可以当作是模拟网络延迟。 - 从
ok()
方法中返回一个 Promise。
// Extend with a random validator V.type['random'] = { ok: function (value) { var deferred = $.Deferred();setTimeout(function () { var result = Math.random() < 0.5; if (result) deferred.resolve(); else deferred.reject(); }, 1000); return deferred.promise();
},
message: 'Failed randomly. No hard feelings.'
};
演示文件中,在 Address 域上使用了 random 校验:
var validationConfig = { /* cilpped for brevity */ '.address': { checks: ['random', 'required'], field: 'Address' } };