複数のWeb APIを叩いたりするときに複数の非同期処理をハンドリングする必要があったので、適当に書いた後どんどん変えていったら別物になりました。
今後のPromise活用に使うかもしれないので基本部分からまとめておきます。
準備
今回は非同期処理の中でも利用頻度の高いWeb APIを叩くことを考えます。
今回はサンプルとして使いやすいGithub APIを対象にいくつかリクエストを投げる。
https://developer.github.com/v3/search/
連続でAPIを叩くことになるのでリミットを確認。未認証で10 req/minのようです。
APIを叩くためにfetchを使いますがNode.jsで使えないためnode-fetchを使います。
1 2 |
npm i node-fetch yarn add node-fetch |
基本的な非同期処理
後々のことを考えてAPI部分はモジュール化しておきます。
とりあえずNode.jsで使いやすいcommonJS形式で書きますがimport / export
の方が統一感があって気持ちいい気もする。
1 2 3 4 5 6 7 8 9 |
const fetch = require("node-fetch"); const base = "https://api.github.com/search/repositories"; exports.q = query => { return fetch(`${base}?q=${query}`).then(r => r.json()); }; exports.q2 = async query => { const result = await fetch(`${base}?q=${query}`); return result.json(); }; |
1 2 3 4 5 6 7 8 9 10 11 |
const api = require("./api"); const dofunc = () => { api.q("java").then(r => console.log(r.total_count)); }; const dofunc2 = async () => { let r = await api.q2("javascript"); console.log(r.total_count); }; dofunc(); dofunc2(); |
Promise
のままとasync / await
で書いてますが、個人的には非常にシンプルなもの(api.js q()
)はthen
チェーン、数行以上書くならawait
がやりやすいです。
正直new Promise((resolve, reject) => {...})
とかはもう忘れていい気がする。
複数の非同期処理:直列
非同期処理1の結果を用いて非同期処理2を動かして…のようなパターン。
非同期処理が変に長引く原因になりますし使う機会はそんなにない。
1 2 3 4 5 6 7 8 9 10 11 12 |
someAsyncFunc1().then(r1 => { someAsyncFunc2(r1).then(r2 => { someAsyncFunc3(r2).then(r3 => console.log(r3)); }); }); (async () => { const r1 = await someAsyncFunc1(); const r2 = await someAsyncFunc2(r1); const r3 = await someAsyncFunc3(r2); console.log(r3); })(); |
書きやすさ・わかりやすさでは圧倒的にasync /await
ですね。
PromiseではなくCallback引数で指定するタイプの関数が混じっているとthenチェーンの方が書きやすいこともある。わりと気分次第。
複数の非同期処理:並列
複数の非同期処理を同時に行って全部終わったら結果を変えるパターン。
複数PromiseをPromise.all()
で1つのPromiseにまとめて結果を得ます。
1 2 3 4 5 6 7 |
(async () => { const p1 = someAsyncFunc1(); const p2 = someAsyncFunc2(); const p3 = someAsyncFunc3(); const results = await Promise.all([p1, p2, p3]); console.log(results); })(); |
非同期処理を待つのは1回なので書き方を気にすることはあまりないけど、少しでもインデントが浅い方が好きなのでasync/await
を使う方が多い。
今回はこのパターンのAPI処理からごちゃごちゃして使いやすい形に落とします。
変形、工夫、改造
まずPromise.then()
がPromise
を返すのを利用して前処理を行う。
ヒット数だけ返すようにした。
1 2 3 4 5 6 7 8 9 |
const api = require("./api"); (async () => { const p1 = api.q("php").then(r => r.total_count); const p2 = api.q("java").then(r => r.total_count); const p3 = api.q("golang").then(r => r.total_count); const results = await Promise.all([p1, p2, p3]); console.log(results); })(); |
似たような処理が続くためクエリを抜き出してArray.map()
でPromise配列にする。
1 2 3 4 5 6 7 8 9 |
const queries = ["php", "java", "golang"]; (async () => { const ps = queries.map(query => api.q(query).then(r => r.total_count)); const results = await Promise.all(ps); console.log(results); })(); /* [ 537673, 1279384, 114378 ] */ |
結果の入った配列が何を表しているかわからないのが不満、というか後々の処理に回すときに不便になりそう。
対処法の1つとして前処理のところでオブジェクトにしてしまう方法があります。
1 2 3 4 5 6 7 8 9 |
const ps = queries.map(query => api.q(query).then(r => ({ query: query, result: r.total_count }))); ... /* [ { query: 'php', result: 537673 }, { query: 'java', result: 1279384 }, { query: 'golang', result: 114378 } ] */ |
使う側でArray.filter()
など必要になるかもしれませんが、かなりわかりやすい。
ちなみに式だと{}
を省略できることを利用していたのでそのままオブジェクトを返そうとすると文だと認識されてエラーになります。
1 2 3 4 5 6 |
OK: r => ({ query: query, result: r.total_count });//Expression OK: r => new Object({ query: query, result: r.total_count });//Expression,意味的に↑と同じ NG: r => { query: query, result: r.total_count };//Error, objectでなくStatementとみなされる OK: r => { return { query: query, result: r.total_count };//Statementならこうする }; |
もう1つは配列の分割代入からオブジェクトに設定する方法です。
1 2 3 4 5 |
const [php, java, golang] = await Promise.all(ps); console.log({ php, java, golang }); /* { php: 537677, java: 1279399, golang: 114380 } */ |
あとは変数定義の手動部分をArray.reduce()
とスプレッド構文(...acc
)、変数値をキーにする書き方([queries[idx]]
)で対応。
1 2 3 |
const rs = await Promise.all(ps); const obj = rs.reduce((acc, cur, idx) => ({ ...acc, [queries[idx]]: cur }), {}); console.log(obj); |
クエリ(string)とプロミスと結果の配列が同数同順なのでこういう書き方ができます。
この場合、使う側はドット表記(.key
)でなくブラケット表記(['key']
)がよさそう。
ここまでくるとES6機能満載な感じになってきて割と好きです。
蛇足ですがクエリ列と結果格納を対応させる部分も一般化できそう。
1 2 3 4 5 6 7 8 9 |
const api = require("./api"); const queries = ["php", "java", "golang"]; const reducer = arr => (acc, cur, idx) => ({ ...acc, [arr[idx]]: cur }); (async () => { const ps = queries.map(query => api.q(query).then(r => r.total_count)); const rs = await Promise.all(ps); const obj = rs.reduce(reducer(queries), {}); console.log(obj); })(); |
配列を渡すと関数を返す関数を作り、他でも使えるようにしておきます。
読みやすさは変わらないけどコメントは付けやすくなりました。
こうして見返してみると変更前の方が読みやすい気もしてきましたが使いやすくはなりました。見やすさを向上させるならthen()
内部の関数を外に出したり。