Статьи

Мои Пять Обещающих Образцов

Я получал большие обещания за последний год. Я думаю, что два лучших ресурса, которые я извлек из сегодняшнего дня, — это выступление Forbes Lindesay на JSConf.EU 2013 и превосходная многообещающая статья Джейка Арчибальда на html5rocks .

Там были некоторые шаблоны, которые я использую снова и снова, поэтому я хотел поделиться и задокументировать их.

Обратите внимание, что используемые примеры в основном основаны на моем реальном коде, но были упрощены для демонстрационных целей.

1. Библиотека выбора

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

Как на стороне клиента, так и на стороне сервера, в стране узлов, поскольку обещания странным образом недоступны изначально, моя предпочтительная библиотека — это / обещание .

Я использовал RSVP в прошлом и слышал хорошие вещи о Bluebird .

RSVP кажется, что в основном это голые кости, но я узнал об prom.js ‘, denodeifyкоторый преобразует основанную на обратном вызове функцию в функцию обещания, которая может быть очень полезной.

Update @ 2014-11-19 15:30:00 RSVP также имеет denodeify, и Мэтт Эндрюс из The FT выпустил отдельный модуль denodeify .

2. Чистые мелкие цепи

Это означает, что мой начальный код обещания будет выглядеть так:

writeFile(filename, content)
  .then(addDBUser)
  .then(dns)
  .then(configureHeroku)
  .then(function () {
    console.log('All done');
  })

Это легко, если это все мои функции, но я также могу сделать это с помощью сторонних библиотек через denodeify(особенность библиотеки обещать, хотя большинство библиотек обещаний имеют что-то похожее) — превратить функцию шаблона обратного вызова в функцию, основанную на обещаниях :

var writeFile = Promise.denodeify(fs.writeFile):

writeFile(filename, content)
  .then(addDBUser)

Хотя я поймал одно место, denodeifyкогда метод опирается на контекст метода, что, как выясняется, является большинством вещей ( fsэто просто случайность того, что его методы не зависят от контекста), поэтому убедитесь, что по bindмере необходимости :

var addUser = Promise
  .denodeify(model.user.add)
  .bind(model.user) // multi line for blog readability

3. Предварительная выпечка

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

Я мог бы сделать это:

writeFile(filename, content)
  .then(function () {
    return addUserToDb('rem', 'password', 'some-db');
  })

Или то, что я обнаружил, что я более склонен делать сейчас, это предварительно addUserToDbвызвать вызов статическими аргументами:

var addUser = addUserToDb.bind(null, 'rem',
      'password', 'some-db');

writeFile(filename, content)
  .then(addUser)

Это также позволяет мне кодировать с мелкими цепочками, как указано выше, потому что (для меня) кажется немного многословным переходить в функцию, чтобы просто вернуть обещание, которое не зависит от какой-либо неизвестной переменной.

Следует обратить внимание на то, что если функция ведет себя по-разному, если есть больше аргументов, я должен холодно вызвать обещание.

Холодный вызов

Отказ от ответственности: этот шаблон требуется из-за моих собственных шаблонов предварительной выпечки и попыток (по иронии судьбы) упростить. Есть хороший шанс, что вам это не понадобится!

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

То, как функция может работать под капотом, выглядит примерно так (этот псевдокод):

Heroku.prototype.post = function (slug, options, callback) {
  // do some async thing
  this.request(slug, options, function (err, data) {

    // ** this line is how the dual functionality works ** //
    if (callback) callback(err, data);

    // else do something with promise
  });

  // return some promise created some place
  return this.promise;
}

Так что postможно назвать либо обещанием:

heroku.post(slug, opts).then(dostuff);

Или в качестве обратного вызова:

heroku.post(slug, opts, dostuff);

Но становится грязно, когда вы делаете это:

function configureHeroku(slug) {
  // prebake heroku app create promise
  var create = heroku.post.bind(heroku,
    '/apps',
    { name: 'example-' + slug }
  );

  // prebake domain config
  var domain = heroku.post.bind(heroku,
    '/apps/example-' + slug + '/domains',
    { hostname: slug + '.example.com' }
  );

  // ** this is where it goes wrong ** //
  return create().then(domain);
}

Проблема в том, что когда domainвызывается, он на самом деле вызывается с предварительно запеченными аргументами слага и опций, а также с разрешенным значением из create()— так что получается третий аргумент .

Этот третий аргумент является разрешенным результатом, create()который рассматривается как callbackаргумент и как объект функции, поэтому код будет пытаться вызвать его — вызывая исключение.

Мое решение состоит в том, чтобы заключить в холодный вызов — то есть вновь созданную функцию, которая вызывает мой метод без аргументов. Подобно bind, но затем никогда не допускайте никаких новых аргументов, также известных как curry (вот простая демонстрация типа вещи curry / частичный / seal ):

function coldcall(fn) {
  return function () {
    fn();
  };
}

function configureHeroku(slug) {
  // prebake heroku app create promise
  // ...


  // ** now it works ** //
  return create().then(coldcall(domain));
}

Примечание: вы можете сделать это используя curry , т.е. lodash.curry .

Теперь domainвызов работает, потому что он вызывается, предотвращая добавление дополнительных аргументов.

4. Брось явный отказ

Вместо:

// compare password & input password
return new Promise(function (resolve) {
  bcrypt.compare(input, password, function (error, result) {
    if (err || !result) {
      // reject and early exit
      return reject(err);
    }

    resolve(result);
  });
});

Я выброшу вместо отклонения

// compare password & input password
return new Promise(function (resolve) {
  bcrypt.compare(input, password, function (error, result) {
    if (err) {
      throw err;
    }

    if (!result) {
      throw new Error('Passwords did not match.');
    }

    resolve(result);
  });
});

Это может быть немного спорным. На самом деле, когда я выбросил это в твиттер, большинство людей вернулось с чем-то вроде:

Отклоняйте всякий раз, когда это возможно, это более производительно, потому что throw разбивает стек.

Это вполне может быть так, но в моем коде есть несколько ключевых преимуществ:

  1. Я привык к обработке первой ошибки, и довольно часто я случайно получаю rejectв качестве первого аргумента, что приводит к большой путанице. Таким образом, я только когда-либо принимаю в resolveкачестве аргумента.
  2. Я не должен помнить return reject. Я видел код, который не возвращается при отклонении, и затем он переходит к resolveзначению. Некоторые библиотеки выполняют, некоторые отклоняют, некоторые выдают новые ошибки. Бросив ошибку полностью избегает этого.
  3. Это также согласуется с тем, как я буду исправлять ошибки в последующих thenвызовах:
// compare password & input password
utils.compare(input, password)
  .then(function () {
    if (!validUsername(username)) {
      throw new Error('Username is not valid');
    }
    // continues...
  })
  .then(etc)

Джейк также добавил пару полезных ответов:

Отклонение должно быть аналогичным броску, но асинхронным. Так что отвергайте то, что вы выбросили (что обычно является ошибкой)

Затем ссылка на его пост с «в ES7 асинхронные функции отклонить это бросить» . Это также подтверждает, что вы хотите отклонить с реальной ошибкой, а не строкой.

5. Всегда заканчивайте добычей

Я нередко проверяю http-запрос с обещанием, и он просто никогда не возвращается …

Проблема в том, что обещание где-то было отклонено и его не поймали. Поэтому я всегда заканчиваю добычей . Даже если это дамп на консоль, я знаю, что что-то не получилось.

writeFile(filename, content)
  .then(addDBUser)
  .then(dns)
  .then(configureHeroku)
  .then(function () {
    console.log('All done');
  })
  .catch(function (error) {
    console.error(error.stack);
  });

Этот последний улов позволяет мне увидеть полную трассировку стека относительно того, что пошло не так, и , что важно, что-то пошло не так .

Примечание: .catch()только в спецификации ES6 и не появляется в Promises / A +, поэтому в некоторых реализациях библиотеки отсутствует .catch()поддержка (как я обнаружил в mongoose, поскольку это зависит от библиотеки mPromise ).

резюмировать

Итак, это все:

  • Неглубокие цепи
  • Предварительная выпечка, где я могу, и холодный звонок, если необходимо
  • Всегда бросай
  • Всегда ловить

Довольно просто Мне было бы интересно услышать, какие шаблоны появляются в вашем рабочем процессе.