LoginSignup
5
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-03

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

はじめに

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

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

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

やりたいこと

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

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

誰でも試せるように無料でできる範囲にしたいので、LINE WORKS API のトーク 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
Visual Studio Code
LINE WORKS

使用する API

API は LINE WORKS API を利用します。
無料で使える Free Plan でも Bot 関係の API は使用できるので、勉強や検証するには便利なのです。
宣伝です(`・ω・´)キリッ

事前準備

  1. Node.js のインストール
  2. Visual Studio Code のインストール
  3. LINE WORKS 利用登録
  4. LINE WORKS Developers で API 認証情報を取得

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

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

事前準備がすべて終わったら Visual Studio Code(以後 VSC)を起動します。
[ファイル] - [フォルダを開く] で作業用フォルダを開きます。
作業用フォルダは何でも良いので、デスクトップに新しいフォルダを作成します。
ファイル名も何でも良いのですが、ここでは ApiRequestSample とでもしておきます。
1604543799.png

[ターミナル] - [新しいターミナル] でターミナルを起動して npm init -y します。
1604546930.png

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

  • 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 を使用して発行します。
事前準備の 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 認証情報を記入してくださいねー。

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 リストの照会 APIBOT の簡易リストを取得します。

ちなみに、BOT 登録がないと当たり前のように空のリストが返ってくるので、実際に試してみたい方は以下の記事を参考に適当な BOT を複数登録しておいてください。
【LINE WORKS】トーク BOT の登録方法

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 を取得してからコンソールに表示することもできました!

非同期処理、二回もやったので慣れてきましたね!ヾ(´∀`)ノ

asyncawait!(゚Д゚)ノ
asyncawait!(゚Д゚)ノ
呪文のように覚える。

もう非同期処理は完璧ですかね?
まだ不安ー!って方は、最後にもう1回やってみましょー!ヾ(´∀`)ノ

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

先ほど取得した BOT 簡易リストの情報で、各 BOT の botNo がわかりました。
botNo がわかればトーク Bot 詳細情報の照会 API を利用して、各 BOT の詳細情報を照会できます。
つまり、リストアップされた BOT の数だけ API Request をするんですね。

API Request を Loop 処理する

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

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

非同期処理で 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 } ]

descriptioncallbackUrl など、Bot の詳細情報を取得することができましたね!

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

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

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

コード全体

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

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
Windows10にVSCodeインストール

5
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3