Статьи

Проверка на основе обещаний

Концепция «Обещания» изменила способ написания асинхронного JavaScript. За прошедший год многие фреймворки включили некоторую форму шаблона Promise, чтобы сделать асинхронный код проще для написания, чтения и обслуживания. Например, jQuery добавил $ .Deferred () , а NodeJS имеет модули Q и jspromise, которые работают как на клиенте, так и на сервере. Платформы MVC на стороне клиента, такие как EmberJS и AngularJS , также реализуют свои собственные версии Promises.

Но это не должно останавливаться на достигнутом: мы можем переосмыслить старые решения и применить к ним Обещания. В этой статье мы сделаем именно это: проверим форму, используя шаблон Promise, чтобы предоставить супер простой API.


Обещания уведомляют о результате операции.

Проще говоря, Обещания уведомляют о результате операции. Результатом может быть успех или неудача, и сама операция может быть чем-то, что подчиняется простому контракту. Я решил использовать слово контракт, потому что вы можете разработать этот контракт несколькими различными способами. К счастью, сообщество разработчиков достигло консенсуса и создало спецификацию под названием Promises / A + .

Только операция действительно знает, когда она завершилась; как таковой, он несет ответственность за уведомление о своем результате с помощью контракта Promises / A +. Другими словами, он обещает сообщить вам окончательный результат по завершении.

Операция возвращает объект promise , и вы можете прикрепить к нему ваши обратные вызовы с помощью методов done() или fail() . Операция может уведомить о своем результате, вызывая promise.resolve() или promise.reject() соответственно. Это изображено на следующем рисунке:

Figure for Promises

Позвольте мне нарисовать правдоподобный сценарий.

Мы можем переосмыслить старые решения и применить к ним Обещания.

Проверка формы на стороне клиента всегда начинается с самых простых намерений. У вас может быть форма регистрации с полями « Имя» и « Электронная почта» , и вам необходимо убедиться, что пользователь предоставляет действительные данные для обоих полей. Это кажется довольно простым, и вы начинаете внедрять свое решение.

Затем вам сообщают, что адреса электронной почты должны быть уникальными, и вы решаете проверить адрес электронной почты на сервере. Таким образом, пользователь нажимает кнопку отправки, сервер проверяет уникальность электронной почты, и страница обновляется для отображения любых ошибок. Это похоже на правильный подход, верно? Нет. Ваш клиент хочет гладкого пользовательского опыта; посетители должны видеть любые сообщения об ошибках без обновления страницы.

Ваша форма имеет поле « Имя», которое не требует какой-либо поддержки на стороне сервера, но тогда у вас есть поле « Электронная почта», которое требует от вас отправки запроса на сервер. Запросы к серверу означают $.ajax() , поэтому вам придется выполнять проверку электронной почты в функции обратного вызова. Если ваша форма имеет несколько полей, требующих поддержки на стороне сервера, ваш код будет вложенным беспорядком $.ajax() в обратных вызовах. Обратные вызовы внутри обратных вызовов: «Добро пожаловать в ад обратного вызова! Мы надеемся, что вы остаетесь несчастным!».

Итак, как мы справимся с адом обратного вызова?

Сделайте шаг назад и подумайте об этой проблеме. У нас есть набор операций, которые могут быть успешными или неудачными. Любой из этих результатов может быть записан как Promise , и операции могут быть любыми: от простых проверок на стороне клиента до сложных проверок на стороне сервера. Обещания также дают вам дополнительное преимущество согласованности, а также позволяют избежать условной проверки типа проверки. Давайте посмотрим, как мы можем это сделать.

Как я отмечал ранее, существует несколько реализаций обещаний, но я сосредоточусь на реализации $ .Deferred () Promise в jQuery.

Мы создадим простую структуру проверки, в которой каждая проверка немедленно возвращает либо результат, либо обещание. Как пользователь этого фреймворка, вы должны помнить только одну вещь: «он всегда возвращает обещание» . Давайте начнем.

Я думаю, что проще оценить Обещания с точки зрения потребителя. Допустим, у меня есть форма с тремя полями: имя, адрес электронной почты и адрес:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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 нашей платформы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
var validationConfig = {
  ‘.name’: {
    checks: ‘required’,
    field: ‘Name’
  },
  ‘.email’: {
    checks: [‘required’],
    field: ‘Email’
  },
  ‘.address’: {
    checks: [‘random’, ‘required’],
    field: ‘Address’
  }
};

Ключи этого объекта конфигурации являются селекторами jQuery; их значения являются объектами со следующими двумя свойствами:

  • checks : строку или массив checks .
  • field : удобочитаемое имя поля, которое будет использоваться для сообщения об ошибках для этого поля

Мы можем вызвать наш валидатор, представленный как глобальная переменная V , следующим образом:

1
2
3
4
5
6
7
V.validate(validationConfig)
  .done(function () {
      // Success
  })
  .fail(function (errors) {
      // Validations failed.
  });

Обратите внимание на использование обратных вызовов done() и fail() ; это обратные вызовы по умолчанию для передачи результата Обещания. Если нам случится добавить больше полей формы, вы можете просто расширить объект validationConfig не нарушая остальную часть настройки ( принцип Open-Closed в действии). Фактически, мы можем добавить другие проверки, такие как ограничение уникальности для адресов электронной почты, расширив структуру валидатора (которую мы увидим позже).

Так что это потребительский API для платформы валидатора. Теперь давайте погрузимся и посмотрим, как это работает под капотом.

Валидатор представлен как объект с двумя свойствами:

  • type : содержит различные виды проверок, а также служит точкой расширения для добавления дополнительных данных.
  • validate : основной метод, который выполняет проверки на основе предоставленного объекта конфигурации.

Общая структура может быть обобщена как:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
   * {
   * ‘<jquery-selector>’: 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. Нормализуйте объект конфигурации.

Здесь мы проходим наш объект конфигурации и преобразуем его во внутреннее представление. Это в основном для сбора всей информации, необходимой нам для проверки и сообщения об ошибках, если это необходимо:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function normalizeConfig(config) {
  config = config ||
 
  var validations = [];
 
  $.each(config, function (selector, obj) {
 
    // make an array for simplified checking
    var checks = $.isArray(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;
}

Этот код перебирает ключи в объекте config и создает внутреннее представление проверки. Мы будем использовать это представление в методе validate .

getValidator() извлекает объект валидатора из хеша type . Если мы не найдем его, мы noCheck валидатор noCheck который всегда возвращает true.

2. Преобразуйте каждую проверку в Обещание.

Здесь мы гарантируем, что каждая проверка является Обещанием, проверяя возвращаемое значение validation.ok() . Если он содержит метод then() , мы знаем, что это Promise (это соответствует спецификации Promises / A +). Если нет, мы создаем специальное обещание, которое разрешается или отклоняется в зависимости от возвращаемого значения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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. Заверните в Обещание мастера.

Мы создали массив Обещаний на предыдущем шаге. Когда все они успешны, мы хотим или решить один раз или потерпеть неудачу с подробной информацией об ошибках. Мы можем сделать это, упаковав все Обещания в одно Обещание и распространяя результат. Если все идет хорошо, мы просто принимаем обещание мастера.

Для ошибок мы можем прочитать из нашего внутреннего представления проверки и использовать его для отчетов. Поскольку может быть несколько сбоев проверки, мы перебираем массив promises и читаем результат state() . Мы собираем все отклоненные обещания в failed массив и вызываем reject() для основного обещания:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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. Верните мастер-обещание.

Наконец, мы возвращаем главное обещание из метода validate() . Это обещание, на котором клиентский код устанавливает обратные вызовы done() и fail() .

Шаги два и три — суть этой структуры. Нормализуя проверки в Обещание, мы можем обрабатывать их последовательно. У нас больше контроля над основным объектом Promise, и мы можем добавить дополнительную контекстную информацию, которая может быть полезна для конечного пользователя.


См. Демонстрационный файл для полного использования платформы валидатора. Мы используем обратный вызов done() чтобы сообщить об успехе, и fail() чтобы показать список ошибок для каждого из полей. На скриншотах ниже показаны состояния успеха и неудачи:

Demo showing Success
Demo showing failures

Демонстрация использует тот же HTML и конфигурацию проверки, упомянутую ранее в этой статье. Единственным дополнением является код, который отображает оповещения. Обратите внимание на использование обратных вызовов done() и fail() для обработки результатов проверки.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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() чтобы сделать проверку асинхронной. Вы также можете думать об этом как о моделировании задержки в сети.
  • Вернуть Promise из метода ok() .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// 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.
};

В демоверсии я использовал эту проверку в поле Address следующим образом:

1
2
3
4
5
6
7
8
var validationConfig = {
  /* cilpped for brevity */
 
  ‘.address’: {
    checks: [‘random’, ‘required’],
    field: ‘Address’
  }
};

Я надеюсь, что эта статья дала вам хорошее представление о том, как вы можете применить Обещания к старым проблемам и построить вокруг них собственную структуру. Подход, основанный на обещаниях, является фантастическим решением для абстрактных операций, которые могут выполняться или не выполняться синхронно. Вы также можете связывать обратные вызовы и даже составлять обещания более высокого порядка из набора других обещаний.

Шаблон «Обещание» применим в различных сценариях, и вы, надеюсь, столкнетесь с некоторыми из них и сразу увидите совпадение!