Node.js Async Await в ES7

Одной из наиболее интересных функций JavaScript (и, следовательно, Node.js) является синтаксис async / await, представленный в ES7 [https://github.com/hemanth/es7-features]. Хотя в основном это просто синтаксический сахар поверх обещаний, сами по себе эти два ключевых слова должны сделать написание асинхронного кода в Node гораздо более терпимым. Это почти устраняет проблему ада обратных вызовов [/ избегая-callback-hell-in-node-js /], и даже позволяет нам использовать структуры потока управления вокруг нашего асинхронного кода. Thro

Одна из самых интересных особенностей JavaScript (и, следовательно, Node.js) - это async / await , представленный в ES7 . Хотя в основном это просто синтаксический сахар поверх обещаний, сами по себе эти два ключевых слова должны сделать написание асинхронного кода в Node гораздо более терпимым. Это почти устраняет проблему ада обратных вызовов , и даже давайте использовать структуры потока управления вокруг нашего асинхронного кода.

В этой статье мы рассмотрим, что не так с Promises, как новая await может помочь и как вы можете начать использовать ее прямо сейчас .

Проблема с обещаниями

Концепция «обещания» в JavaScript существует уже некоторое время, и ее можно использовать уже много лет благодаря сторонним библиотекам, таким как Bluebird и q , не говоря уже о недавно добавленной встроенной поддержке в ES6.

Они были отличным решением проблемы ада обратных вызовов, но, к сожалению, они не решают всех асинхронных проблем. Будучи большим улучшением, Promises заставляет нас хотеть еще большего упрощения.

Допустим, вы хотите использовать REST API Github, чтобы узнать количество звезд, которые имеет проект. В этом случае вы, вероятно, воспользуетесь прекрасной библиотекой запросов и обещаний. Используя подход, основанный на обещаниях, вы должны сделать запрос и вернуть результат в обратном вызове, который вы передаете в .then() , например:

 var request = require('request-promise'); 
 
 var options = { 
 url: 'https://api.github.com/repos/scottwrobinson/camo', 
 headers: { 
 'User-Agent': 'YOUR-GITHUB-USERNAME' 
 } 
 }; 
 
 request.get(options).then(function(body) { 
 var json = JSON.parse(body); 
 console.log('Camo has', json.stargazers_count, 'stars!'); 
 }); 

Это напечатает что-то вроде:

 $ node index.js 
 Camo has 1,000,000 stars! 

Ладно, может быть, это число немного преувеличено, но суть вы поняли;)

Выполнить всего один такой запрос с Promises не так уж сложно, но что, если мы хотим сделать один и тот же запрос для множества разных репозиториев на GitHub? А что произойдет, если нам нужно добавить поток управления (например, условные обозначения или циклы) вокруг запросов? По мере того, как ваши требования становятся более сложными, с промисами становится все труднее работать, и они по-прежнему усложняют ваш код. Они по-прежнему лучше, чем обычные обратные вызовы, поскольку у вас нет неограниченного вложения, но они не решают всех ваших проблем.

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

 "use strict"; 
 
 var request = require('request-promise'); 
 
 var headers = { 
 'User-Agent': 'YOUR-GITHUB-USERNAME' 
 }; 
 
 var repos = [ 
 'scottwrobinson/camo', 
 'facebook/react', 
 'scottwrobinson/twentyjs', 
 'moment/moment', 
 'nodejs/node', 
 'lodash/lodash' 
 ]; 
 
 var issueTitles = []; 
 
 var reqs = Promise.resolve(); 
 
 repos.forEach(function(r) { 
 var options = { url: 'https://api.github.com/repos/' + r, headers: headers }; 
 
 reqs = reqs.then(function() { 
 return request.get(options); 
 }).then(function(body) { 
 var json = JSON.parse(body); 
 
 var p = Promise.resolve(); 
 
 // Only make request if it has open issues 
 if (json.has_issues) { 
 var issuesOptions = { url: 'https://api.github.com/repos/' + r + '/issues', headers: headers }; 
 p = request.get(issuesOptions).then(function(ibody) { 
 var issuesJson = JSON.parse(ibody); 
 
 if (issuesJson[0]) { 
 issueTitles.push(issuesJson[0].title); 
 } 
 }); 
 } 
 
 return p; 
 }); 
 }); 
 
 reqs.then(function() { 
 console.log('Issue titles:'); 
 issueTitles.forEach(function(t) { 
 console.log(t); 
 }); 
 }); 

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

На момент написания этой статьи выполнение этого кода привело бы к следующему:

 $ node index.js 
 Issue titles: 
 feature request: bulk create/save support 
 Made renderIntoDocument tests asynchronous. 
 moment issue template 
 test: robust handling of env for npm-test-install 

Простое добавление for и оператора if в наш асинхронный код значительно усложняет чтение и понимание. Такого рода сложность может сохраняться лишь до тех пор, пока с ней не станет слишком сложно работать.

Глядя на код, можете ли вы сразу сказать мне, где на самом деле выполняются запросы или в каком порядке выполняется каждый блок кода? Наверное, не без внимательного прочтения.

Упрощение с помощью Async / Await

Новый async / await позволяет по-прежнему использовать Promises, но устраняет необходимость в обеспечении обратного вызова для связанных методов then() Значение, которое было бы отправлено в then() вместо этого возвращается непосредственно из асинхронной функции, как если бы это была синхронная функция блокировки.

 let value = await myPromisifiedFunction(); 

Несмотря на кажущуюся простоту, это огромное упрощение дизайна асинхронного кода JavaScript. Единственный дополнительный синтаксис, необходимый для этого, - это ключевое слово await Итак, если вы понимаете, как работают промисы, тогда не составит труда понять, как использовать эти новые ключевые слова, поскольку они основаны на концепции промисов. Все , что вам действительно нужно знать, что любое обещание может быть await -ed. Значения также можно await -ed, так же как Promise может .resolve() для целого числа или строки.

Давайте сравним метод на основе Promise с ключевым словом await

Обещания

 var request = require('request-promise'); 
 
 request.get('https://api.github.com/repos/scottwrobinson/camo').then(function(body) { 
 console.log('Body:', body); 
 }); 

Ждите

 var request = require('request-promise'); 
 
 async function main() { 
 var body = await request.get('https://api.github.com/repos/scottwrobinson/camo'); 
 console.log('Body:', body); 
 } 
 main(); 

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

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

Вот простой пример его использования (обратите внимание на изменение определения функции):

 async function getCamoJson() { 
 var options = { 
 url: 'https://api.github.com/repos/scottwrobinson/camo', 
 headers: { 
 'User-Agent': 'YOUR-GITHUB-USERNAME' 
 } 
 }; 
 return await request.get(options); 
 } 
 
 var body = await getCamoJson(); 

Теперь, когда мы знаем, как использовать async и await вместе, давайте посмотрим, как выглядит более сложный код на основе Promise из предыдущего:

 "use strict"; 
 
 var request = require('request-promise'); 
 
 var headers = { 
 'User-Agent': 'scottwrobinson' 
 }; 
 
 var repos = [ 
 'scottwrobinson/camo', 
 'facebook/react', 
 'scottwrobinson/twentyjs', 
 'moment/moment', 
 'nodejs/node', 
 'lodash/lodash' 
 ]; 
 
 var issueTitles = []; 
 
 async function main() { 
 for (let i = 0; i < repos.length; i++) { 
 let options = { url: 'https://api.github.com/repos/' + repos[i], headers: headers }; 
 let body = await request.get(options); 
 let json = JSON.parse(body); 
 
 if (json.has_issues) { 
 let issuesOptions = { url: 'https://api.github.com/repos/' + repos[i] + '/issues', headers: headers }; 
 let ibody = await request.get(issuesOptions); 
 let issuesJson = JSON.parse(ibody); 
 
 if (issuesJson[0]) { 
 issueTitles.push(issuesJson[0].title); 
 } 
 } 
 } 
 
 console.log('Issue titles:'); 
 issueTitles.forEach(function(t) { 
 console.log(t); 
 }); 
 } 
 
 main(); 

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

Теперь единственная проблема заключается в том, что каждый request.get() выполняется последовательно (это означает, что каждый вызов должен ждать завершения предыдущего вызова перед выполнением), поэтому нам нужно дольше ждать, пока код завершит выполнение, прежде чем получить наши результаты. . Лучшим вариантом было бы параллельное выполнение HTTP-запросов GET. Это все еще можно сделать с помощью Promise.all() как мы делали это раньше. Просто замените цикл for .map() и отправьте полученный массив Promises в Promise.all() , например:

 // Init code omitted... 
 
 async function main() { 
 let reqs = repos.map(async function(r) { 
 let options = { url: 'https://api.github.com/repos/' + r, headers: headers }; 
 let body = await request.get(options); 
 let json = JSON.parse(body); 
 
 if (json.has_issues) { 
 let issuesOptions = { url: 'https://api.github.com/repos/' + r + '/issues', headers: headers }; 
 let ibody = await request.get(issuesOptions); 
 let issuesJson = JSON.parse(ibody); 
 
 if (issuesJson[0]) { 
 issueTitles.push(issuesJson[0].title); 
 } 
 } 
 }); 
 
 await Promise.all(reqs); 
 } 
 
 main(); 

Таким образом, вы можете воспользоваться преимуществами скорости параллельного выполнения и простоты await .

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

Итак, теперь это ...

 request.get('https://api.github.com/repos/scottwrobinson/camo').then(function(body) { 
 console.log(body); 
 }).catch(function(err) { 
 console.log('Got an error:', err.message); 
 }); 
 
 // Got an error: 403 - "Request forbidden by administrative rules. Please make sure your request has a User-Agent header..." 

... можно выразить так:

 try { 
 var body = await request.get('https://api.github.com/repos/scottwrobinson/camo'); 
 console.log(body); 
 } catch(err) { 
 console.log('Got an error:', err.message) 
 } 
 
 // Got an error: 403 - "Request forbidden by administrative rules. Please make sure your request has a User-Agent header..." 

Хотя это примерно такой же объем кода, его гораздо легче читать и понимать тем, кто переходит на JavaScript с другого языка.

Использование Async прямо сейчас

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

V8

Хотя он еще не дошел до Node, команда V8 публично заявила о своем намерении реализовать функцию async / await Они даже уже завершили реализацию прототипа среды выполнения, а это значит, что поддержка гармонии не должна сильно отставать.

Вавилон

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

Регенератор

У проекта регенератора от Facebook не так много функций, как у Babel, но это более простой способ заставить работать асинхронную транспиляцию.

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

Traceur

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

asyncawait

Большинство доступных вам опций включают либо транспилирование, либо использование ночной сборки V8 для async работы. Другой вариант - использовать пакет asyncawait , который предоставляет функцию для разрешения обещаний аналогично функции await . Это отличный ванильный ES5 способ получить похожий синтаксис.

Заключение

Вот и все! Лично мне больше всего нравится эта функция в ES7, но в ES7 есть и другие замечательные функции, которые вам стоит попробовать, например, декораторы классов и свойства.

Вы используете транспилированный код ES7? Если да, то какая функция была наиболее полезной для вашей работы? Дайте нам знать об этом в комментариях!

comments powered by Disqus