/ async

基于 Promises 的表单验证

Promises 理念已经改变了编写异步 JavaScript 的方式。近一年来,许多框架都集成了 Promise 模式,让编写、阅读、维护异步代码更加容易。比如,jQuery 添加了  $.Deferred() API, NodeJS 有服务器端和客户端都能用的 Qjspromise 模块。客户端 MVC 框架,诸如 EmberJSAngularJS 也实现了自己的 Promises。

不止于此,我们可以重新考量原来的解决方案,将 Promises 应用到这些方案中。本文将使用 Promise 模式来验证一个表单,以展示这个 API。

什么是 Promise?

简言之,Promises 通知一个操作的结果,无论结果是成功还是失败。操作本身遵守某种简单的契约。这里选择使用 “契约(原文使用 contract)”一词,因为开发者可以按不同的方式设计这个约定。感谢地是,开发者社区对此达成一致并形成了名为 Promises/A+ 的规范。

仅当操作真正完成后,根据 Promises/A+ 约定通知结果。换句话说,无论操作结果如何,都“承诺”在操作完成的时候通知用户。

操作返回一个 promise 对象,开发者可以使用 done()fail() 方法添加回调函数。操作通过调用 promise.resolve()promise.reject() 实现结果通知。

promise-validation-promise

表单验证中使用 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() 显示错误信息。下面的是成功和失败时的截图:

promise-validation-demo-success

promise-validation-demo-failure

这个演示使用文章开头的 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() &lt; 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'
  }
};

总结

通过本文,希望能向开发者传达使用 Promises 解决问题的理念。对可能同步也可能异步运行操而言,基于 Promise 的方法是抽象化这些操作的极佳途径。

相关链接

via Promise-Based Validation | [演示文件](https://github.com/NETTUTS/Promise-Based-Validation)