【Node.js】非同期処理に慣れるためのハンズオン

Node.js Advent Calendar 4日目！
どうぞよろしくお願いいたします。m(_　_)m

# はじめに

Node.js で `async await` について書くんですが、独学で理解した気になっていた私は、いざ実装となったときに『あれれ？』ってなりました。
知識が経験として定着してなかったんですね。反省(=_=)

そこで、読んだ方が試せるようになるべく細かく書いてみました！
自身の復習と定着にもなりますしね～♪(*‘∀‘)

所用時間は事前準備で 15 分、実作業で 15 分の、合計 30分程度です。
Node.js の非同期処理にまだ慣れていない方は、是非ともやってみてください♪

# やりたいこと

やりたいことは**非同期処理の勉強！**
…なんですけど、そうではなくて**どんなプログラムを作るか**ですね。(;^ω^)

今回は非同期処理の練習なので **API Request** をして情報を取得し、情報リストを作成していきたいと思います。

誰でも試せるように**無料でできる範囲**にしたいので、[LINE WORKS API](https://developers.worksmobile.com/jp/document?lang=ja) のトーク Bot API 縛りでお送りしたいと思います(*´Д｀)

トーク Bot API 縛りだと・・・**今まで作成した Bot リスト**が作れそうですね。
Bot リスト自体は練習なのであんまり使い道がないですが、有料プランとかだとユーザデータやカレンダー情報などを API Request で取得してリスト化する！とかもできます。

リスト作りは汎用性が高いので練習には持ってこないんですよ～。(ﾟДﾟ)

# ロードマップ

こんな感じで進めて行こうと思います。

1. API を利用するための Access Token を発行する
2. 発行した Access Token を使用して BOT リストの照会 API を Request する（**非同期処理**）
3. さらに、Loop 処理で各 BOT の詳細情報照会 API をいっぺんに Request する
4. Response を受け取ってくっつけて、リストを作る

#### この通りやりたいんだけど・・・！

ご存じの通り API Request はその性質上 Response を受け取るまで多少の時間がかかります。
しかし、Node.js は処理を同期させずに行う**ノンブロッキング I/O を利用している**ので Response を待たずに次の処理を行います。
必要な情報が手に入っていないのに、次の手順へと進んでしまうのですね。
これはまずい(; ･`д･´)

そこで**非同期処理**と**再起処理**の記述が必要になってきます。
これらを順番に学んでいきたいと思います。

## 開発環境

Windows 10
[Node.js](https://nodejs.org/ja/)
[Visual Studio Code](https://azure.microsoft.com/ja-jp/products/visual-studio-code/)
[LINE WORKS](https://line.worksmobile.com/jp/)

#### 使用する API

API は [LINE WORKS API](https://developers.worksmobile.com/jp/document?lang=ja) を利用します。
[無料で使える Free Plan](https://join.worksmobile.com/jp/joinup/step1) でも Bot 関係の API は使用できるので、勉強や検証するには便利なのです。
宣伝です(｀・ω・´)ｷﾘｯ

## 事前準備

1. [Node.js のインストール](https://qiita.com/kunihiros/items/eb22912f7bf41abf0161)
2. [Visual Studio Code のインストール](https://qiita.com/MtBigYashi/items/a840865a6908de044724)
3. [LINE WORKS 利用登録](https://guide.worksmobile.com/ja/start/registration)
4. [LINE WORKS Developers で API 認証情報を取得](https://qiita.com/kunihiros/items/54853e910ed1dff9ebdc)

それぞれ解説しているサイトの URL を貼っておきます。
取り敢えずやり方だけ見たいって方は先に進んじゃってくださいませ。

## 各モジュールのインストール

事前準備がすべて終わったら Visual Studio Code（以後 VSC）を起動します。
[ファイル] - [フォルダを開く] で作業用フォルダを開きます。
作業用フォルダは何でも良いので、デスクトップに新しいフォルダを作成します。
ファイル名も何でも良いのですが、ここでは `ApiRequestSample` とでもしておきます。
![1604543799.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363367/fde63183-532d-d1c1-b5ed-024ec07d4fdb.png)

[ターミナル] - [新しいターミナル] でターミナルを起動して `npm init -y` します。
![1604546930.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363367/34fa8999-90c7-27b1-8f15-f5e0b9b5d0d5.png)

`package.json` ファイルがフォルダに作成されたら、使用するモジュールをインストールしていきます。
今回使うのはこの４つ。

- request
- request-promise
- promise
- jsonwebtoken

ターミナルで順番にインストールしていきます。

```
> npm install request --save
> npm install request-promise --save
> npm install promise --save
> npm install jsonwebtoken --save
```

これで準備完了です。

ではでは、実際にコードを書いていきましょう！

## 1. API を利用するために Access Token を発行する

LINE WORKS API を利用するためは Access Token が必要です。
[LINE WORKS 認証サーバーへの Token リクエスト API](https://developers.worksmobile.com/jp/document/1002002?lang=ja) を使用して発行します。
事前準備の 4. で取得した LINE WORKS 認証情報を使いますので、ご用意を！

API Request で Token を取得するのですが、**Response を待って次の処理を行う必要がある**ので、ここで**非同期処理**をする必要があります。

では、Node.js の非同期処理である `async function` を使っていきましょーヾ(*´∀｀*)ﾉ

### async function のお約束

非同期処理をしてくれる、とっても便利な関数。
今回、私が理解したお約束はこんな感じ。

- `async function` では `Promise` オブジェクトを値を入れて `return` する。
- `Promise` オブジェクトに値を入れるには `resolve` を使う。
- `async function` がエラー値を `throw` した場合は `reject` を使う。

### await を使って非同期処理！

`await` さんは `async function` が結果を返すまで処理を待ってくれる心の広いお方！
`await` さんとのお約束はこんな感じ。

- `await` を頭につけて `async function` を使う。
- `await` を使えるのは `async function` の中でだけ。
- なので main プログラム内で `await` したいときは予め `async function main()` を宣言しておく。

まぁ、文字で読んでもイメージ沸きませんよね。( ﾟДﾟ)
なので、上記のことを踏まえてコードを見ていきましょう。

### Access Token を発行するコード

使用するときは最初の `CONSTS` のところに事前準備の 4. で取得した LINE WORKS 認証情報を記入してくださいねー。

```js:main.js
// LINE WORKS 認証情報
const CONSTS = {
    API_ID : "xxxxxxxx",
    CONSUMER_KEY : "xxxxxxxxxx",
    SERVER_ID : "xxxxxxxxxx",
    PRIVATEKEY : "xxxxxxxxxxxxxxxxxxxxxxyyzz" // 認証キー
}
// module
const request = require("request-promise");

// program start
main();

/**
 * 非同期処理にて Access Token を取得して表示
 */
async function main() {
    const token = await getServerToken();
    console.log(token);
}
/**
 * LINE WORKS の Server Token を取得
 * @return {object} Server Token
 */
async function getServerToken() {
    const jwt = require("jsonwebtoken");
    const iss = CONSTS.SERVER_ID;
    const iat = Math.floor(Date.now() / 1000);
    const exp = iat + 60 * 60; // 有効期間を 1時間に設定
    const cert = CONSTS.PRIVATEKEY;
    const options = {
        method: "POST",
        url: `https://auth.worksmobile.com/b/${CONSTS.API_ID}/server/token`,
        headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
        form: {
            grant_type: encodeURIComponent("urn:ietf:params:oauth:grant-type:jwt-bearer"),
            assertion: jwt.sign({ iss: iss, iat: iat, exp: exp }, cert, { algorithm: "RS256" }),
        }
    };
    return new Promise((resolve, reject) => {
        request(options).then((res) => {
            const result = JSON.parse(res);
            if (result.message) throw "Could not get token";
            resolve(result);
        }).catch((error) => {
            console.log(`Auth Error: ${error}`);
            reject(error);
        });
    });
}
```

```
> node .\main.js
{ access_token: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  token_type: 'Bearer',
  expires_in: 86400 }
```

無事に取得して表示することができました！ヾ(*´∀｀*)ﾉ
次は、取得した Access Token を使用して API を Request していきましょう。

## 2. 発行した Access Token を使用して BOT リストの照会 API を Request する

Access Token が手に入ったので API Request し放題ですね！( ﾟДﾟ)

今回の目的は、トーク BOT のリストを作ることなので、まずは [トーク Bot リストの照会 API](https://developers.worksmobile.com/jp/document/1005006?lang=ja) で **BOT の簡易リスト**を取得します。

ちなみに、BOT 登録がないと当たり前のように空のリストが返ってくるので、実際に試してみたい方は以下の記事を参考に適当な BOT を複数登録しておいてください。
[【LINE WORKS】トーク BOT の登録方法](https://qiita.com/kunihiros/items/465e99e491b8be4cabdb)

#### API Request するときは非同期処理をしよう！

繰り返しになりますが、API Request はその性質上 Response を受け取るまで多少の時間がかかります。
しかし、Node.js は**ノンブロッキング I/O を利用している**ので Response を待たずに次の処理を行います。
なので、これも**非同期処理をしないとろくな結果になりません**(・ω・)

ではでは、軽くおさらいしながらコードを書いていきましょー。

#### BOT の簡易リストを取得するコード

BOT の簡易リストを取得する関数、`getBotList()` を**非同期処理**の `async function` で宣言します。
`main()` の中で呼び出すときは `await` を頭につけて呼び出します。

```main.js
// 前略
main();

async function main() {
    const token = await getServerToken();
    const bots = await getBotList(token);
    console.log(bots);
}
// getServerToken() の内容は省略
/**
 * LINE WORKS のトーク Bot リストを取得
 * @param {object} token LINE WORKS Server Token
 * @return {Promise object} トーク Bot リスト
 */
async function getBotList(token) {
    const options = {
        method: "GET",
        url: `https://apis.worksmobile.com/r/${CONSTS.API_ID}/message/v1/bot`,
        headers: {
            "Content-Type": "application/JSON",
            "consumerKey": CONSTS.CONSUMER_KEY,
            "Authorization": `${token.token_type} ${token.access_token}`
        }
    };
    return new Promise((resolve, reject) => {
        request(options).then((res) => {
            const result = JSON.parse(res);
            resolve(result.bots);
        }).catch((error) => {
            console.log(`Bot List Request Error:\n${error}`);
            reject(error);
        });
    });
}
```

```
> node .\main.js
[ { botNo: xx6040,
    name: 'お問い合わせフォーム君',
    photoUrl: 'https://jp1-common.worksmobile.com/gateway/image/view?path=/bot_profile/xxxxxx.png',
    i18nNames: [],
    i18nPhotoUrls: [] },
  { botNo: xx6250,
    name: 'facebook_bot',
    photoUrl: 'https://jp1-common.worksmobile.com/gateway/image/view?path=/bot_profile/xxxxxx.png',
    i18nNames: [],
    i18nPhotoUrls: [] },
  { botNo: xx6261,
    name: 'LW_bot',
    photoUrl: 'https://jp1-common.worksmobile.com/gateway/image/view?path=/bot_profile/xxxxxx.png',
    i18nNames: [],
    i18nPhotoUrls: [] },
  { botNo: xx6682,
    name: 'GAS 連携 Bot',
    photoUrl: 'https://jp1-common.worksmobile.com/gateway/image/view?path=/bot_profile/xxxxxx.png',
    i18nNames: [],
    i18nPhotoUrls: [] }]
```

無事に `Access Token` を取得してから API を呼び出すことができました。
また、`botList` を取得してからコンソールに表示することもできました！

非同期処理、二回もやったので慣れてきましたね！ヾ(*´∀｀*)ﾉ

`async`！`await`！(ﾟДﾟ)ﾉ
`async`！`await`！(ﾟДﾟ)ﾉ
呪文のように覚える。

もう非同期処理は完璧ですかね？
まだ不安ー！って方は、最後にもう１回やってみましょー！ヾ(*´∀｀*)ﾉ

## 3. さらに、Loop 処理で各 BOT の詳細情報照会 API をいっぺんに Request する

先ほど取得した BOT 簡易リストの情報で、各 BOT の `botNo` がわかりました。
`botNo` がわかれば[トーク Bot 詳細情報の照会 API](https://developers.worksmobile.com/jp/document/1005007?lang=ja) を利用して、各 BOT の詳細情報を照会できます。
つまり、リストアップされた BOT の数だけ API Request をするんですね。

#### API Request を Loop 処理する

繰り返しになりますが、API Request はその性質上 Response を受け取るまで多少の時間がかかります。
しかし、Node.js は**ノンブロッキング I/O を利用している**ので Response を待たずに次の処理を行います。
**Loop 処理なんかはその最たるもの**で、普通に `for` や `forEach` で回そうものなら処理順が**ぐっちゃぐっちゃ**になります。( ;∀;)

と、言うわけで！もはや耳タコな**非同期処理**を使って綺麗なループ処理をやっていきましょー！(*‘∀‘)

#### 非同期処理で Loop するコード

```main.js
// 前略
main();

async function main() {
    const token = await getServerToken();
    const bots = await getBotList(token);
    let botLists = [];
    for(let i = 0; i < bots.length; i++){
        botLists.push(await getBotInfo(bots[i].botNo, token));
    }
    console.log(botLists);
}
// getServerToken() の内容は省略
// getBotList(token) の内容は省略

/**
 * LINE WORKS トーク Bot の詳細情報を加えたリストを作成
 * @param {object} bots LINE WORKS トーク Bot の botNo リスト
 * @param {object} token LINE WORKS Server Token
 * @return {Promise object} 詳細なトーク Bot リスト
 */
async function getBotInfo(botNo, token) {
    const options = {
        method: "GET",
        url: `https://apis.worksmobile.com/r/${CONSTS.API_ID}/message/v1/bot/${botNo}`,
        headers: {
            "Content-Type": "application/JSON",
            "consumerKey": CONSTS.CONSUMER_KEY,
            "Authorization": `${token.token_type} ${token.access_token}`
        }
    };
    return new Promise((resolve, reject) => {
        request(options).then((res) => {
            let result = JSON.parse(res);
            result.botNo = botNo;
            resolve(result);
        }).catch((error) => {
            console.log(`Bot Info Request Error:\n${error}`);
            reject(error);
        });
    });
}
```

```
> node .\main.js
[ { name: 'お問い合わせフォーム君',
    photoUrl: 'https://jp1-common.worksmobile.com/gateway/image/view?path=/bot_profile/xxxxxx.png',
    useCallback: false,
    tenantId: xxxxxxxx,
    createdTime: 1544425101360,
    modifiedTime: 1544425101360,
    description: 'googleのフォームからのメッセージを受けてサンプル太郎に送信します。',
    managers: [ 'xxxx@yyy-zzz' ],
    submanagers: [],
    domainInfos: [ [Object] ],
    useGroupJoin: true,
    useDomainScope: false,
    i18nNames: [],
    i18nPhotoUrls: [],
    i18nDescriptions: [],
    botNo: xxxxxxxx },
// 中略
  { name: '湯婆婆',
    photoUrl: 'https://jp1-common.worksmobile.com/gateway/image/view?path=/bot_profile/xxxxxx.png',
    useCallback: true,
    callbackUrl: 'https://callback.com/',
    callbackEvents: [ 'text' ],
    tenantId: xxxxxxxx,
    createdTime: 1605684025704,
    modifiedTime: 1605684176017,
    description: '流行りの湯婆婆をBOTで作ってみました。',
    managers: [ 'xxxx@yyy-zzz' ],
    submanagers: [],
    domainInfos: [ [Object] ],
    useGroupJoin: false,
    useDomainScope: false,
    i18nNames: [],
    i18nPhotoUrls: [],
    i18nDescriptions: [],
    botNo: xxxxxxxx } ]
```

`description` や `callbackUrl` など、Bot の詳細情報を取得することができましたね！

三回も非同期処理の練習をしたので、これでもうバッチリ OK 大丈夫！

右手に `async`！ 左手に `await`！
合体させて**非同期処理！**

はい、お粗末様でしたー( ﾟДﾟ)

# コード全体

最後に、コード全体をさらしておきます。

```js:main.js
// LINE WORKS 認証情報
const CONSTS = {
    API_ID : "xxxxxxxx",
    CONSUMER_KEY : "xxxxxxxxxx",
    SERVER_ID : "xxxxxxxxxx",
    PRIVATEKEY : "xxxxxxxxxxxxxxxxxxxxxxyyzz" // 認証キー
}
// module
const request = require("request-promise");

// main
main();

async function main() {
    const token = await getServerToken();
    const bots = await getBotLists(token);
    let botLists = [];
    for(let i = 0; i < bots.length; i++){
        botLists.push(await getBotInfo(bots[i].botNo, token));
    }
    console.log(botLists);
}
/**
 * LINE WORKS の Server Token を取得
 * @return {Promise object} Server Token
 */
async function getServerToken() {
    const jwt = require("jsonwebtoken");
    const iss = CONSTS.SERVER_ID;
    const iat = Math.floor(Date.now() / 1000);
    const exp = iat + 60 * 60; // 有効期間を 1時間に設定
    const cert = CONSTS.PRIVATEKEY;
    const options = {
        method: "POST",
        url: `https://auth.worksmobile.com/b/${CONSTS.API_ID}/server/token`,
        headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
        form: {
            grant_type: encodeURIComponent("urn:ietf:params:oauth:grant-type:jwt-bearer"),
            assertion: jwt.sign({ iss: iss, iat: iat, exp: exp }, cert, { algorithm: "RS256" }),
        }
    };
    return new Promise((resolve, reject) => {
        request(options).then((res) => {
            const result = JSON.parse(res);
            if (result.message) throw "Could not get token";
            resolve(result);
        }).catch((error) => {
            console.log(`Auth Error: ${error}`);
            reject(error);
        });
    });
}
/**
 * LINE WORKS のトーク Bot リストを取得
 * @param {object} token LINE WORKS Server Token
 * @return {Promise object} トーク Bot リスト
 */
async function getBotLists(token) {
    const options = {
        method: "GET",
        url: `https://apis.worksmobile.com/r/${CONSTS.API_ID}/message/v1/bot`,
        headers: {
            "Content-Type": "application/JSON",
            "consumerKey": CONSTS.CONSUMER_KEY,
            "Authorization": `${token.token_type} ${token.access_token}`
        }
    };
    return new Promise((resolve, reject) => {
        request(options).then((res) => {
            const result = JSON.parse(res);
            resolve(result.bots);
        }).catch((error) => {
            console.log(`Bot List Request Error:\n${error}`);
            reject(error);
        });
    });
}
/**
 * LINE WORKS トーク Bot の詳細情報を加えたリストを作成
 * @param {object} bots LINE WORKS トーク Bot の botNo リスト
 * @param {object} token LINE WORKS Server Token
 * @return {Promise object} 詳細なトーク Bot リスト
 */
async function getBotInfo(botNo, token) {
    const options = {
        method: "GET",
        url: `https://apis.worksmobile.com/r/${CONSTS.API_ID}/message/v1/bot/${botNo}`,
        headers: {
            "Content-Type": "application/JSON",
            "consumerKey": CONSTS.CONSUMER_KEY,
            "Authorization": `${token.token_type} ${token.access_token}`
        }
    };
    return new Promise((resolve, reject) => {
        request(options).then((res) => {
            let result = JSON.parse(res);
            result.botNo = botNo;
            resolve(result);
        }).catch((error) => {
            console.log(`Bot Info Request Error:\n${error}`);
            reject(error);
        });
    });
}
```

# おわりに

ここまでお付き合いいただきありがとうございました。

早いもので、独学で Node.js を勉強し始めてから 2年が経ちました。
非同期処理さんとも、何だかようやく仲良くなれた気がします♪

最近はソロ開発ばかりではなく先輩と組むこともあり、色々と吸収させてもらっています。
ほんと、良い先輩に恵まれた！
メキメキ成長して、恩返しをしたい所存であります。

次は何を書こうかな～。
ではまた！(^^)/

# 参考にさせていただきました。

[LINE WORKS Developers](https://developers.worksmobile.com/jp/document?lang=ja)
[Windows10にVSCodeインストール](https://qiita.com/MtBigYashi/items/a840865a6908de044724)