こまぶろ

技術のこととか仕事のこととか。

Connpass APIをLambdaから扱う〜Lambdaの非同期呼び出しによる分散処理〜

Connpass APIを用いたボットの作成

先日、ConnpassのAPIをNode.jsから実行する方法について記事を書きました。

ky-yk-d.hatenablog.com

上の記事の末尾にも記載した通り、現在はConnpassのイベント新着情報をSlackに通知するボットを作成しています。

github.com

実装の方針としては、

  • 定期実行が必要
  • 1回1回はAPI呼び出し+通知だけ

ということで、AWS LambdaをCloudWatch Eventsを用いてスケジュール起動することを考えました。CloudWatch Eventsを使うのは初めてですが、LambdaでAPIコールを行うのは今までも何度もしてきたので、そこまで苦労はないと考えていました。

docs.aws.amazon.com

直面した問題

しかし、いざ開発を始めてみると、Lambda側で少々工夫が必要であることに気づきました。それは、以下のような問題に直面したからです。

  • 1個あたりの検索結果が増えるとConnpassのAPIでエラーになる
  • 検索結果の総量が増えるとLambdaのタイムアウトにひっかかる

これらについて順に説明します。

問題①:1個あたりの検索結果が増えるとConnpassのAPIでエラーになる

ConnpassのAPIでは、様々な検索条件をクエリ文字列に付加することができます。その中の一つとして、文字列で検索ワードのAND条件とOR条件を指定することができるため、ウォッチ対象のテーマをOR条件に付加していけば、1本のリクエストで複数のテーマのイベント情報を取得することができます。検索ワードごとに別々のリクエストを発行する必要はありません。

たた、あまりに多くの検索ワードをOR条件として付加してしまうと、膨大な数のイベント情報が取得されてしま うからか、適切にレスポンスが取得できずにエラーとなってしまいます。

問題②:検索結果の総量が増えるとLambdaのタイムアウトにひっかかる

ConnpassのAPIの問題以外に、Lambda側の問題もあります。検索結果の件数が総量として増えれば、それだけ処理にかかかる時間は増加します。そうなると、Lambdaのタイムアウト時間に抵触する恐れが出てきます。実際、タイムアウト時間を10秒に設定した場合には、エラーが発生してしまっていました。

考えた解決策

起動部のLambdaから本体のLambdaを複数回&非同期で呼び出す

以上のことから、次のような解決策を考えました。

  • 1回のリクエストで対象とする検索ワードを限定する(ConnpassのAPIでのエラー対策)
  • 1回のLambdaの実行ではタイムアウトしない程度の処理のみを行う(Lambdaのタイムアウト対策)
  • 上のLambda(本体)を呼び出すLambda(起動部)を作成し、そこで検索ワードを処理させる
  • 起動部から本体を非同期で呼び出す(同期呼び出しすると起動部がタイムアウトする)

図にすると以下のような形。

f:id:ky_yk_d:20180805193703p:plain

実装の一部

const AWS = require('aws-sdk');
const lambda = new AWS.Lambda();
// 中略
ArrayOfArrayOfWords.forEach((element,index)=>{  // 「『検索ワードの配列』の配列」を順に処理
      queries.keyword_or = element;
      const params = {
        FunctionName: targetLambdaArn,
        InvocationType: 'Event', // 非同期呼び出し。即座にステータスコード202で返ってくる
        Payload: JSON.stringify(queries)
      };
      lambda.invoke(params).promise().then(
        (res)=>{
          console.log(index,res);
        },
        (error)=>{
          console.log('Error:', index, error);
        });
    });

LambdaのInvocationTypeについて

ポイントは、Lambdaの呼び出しタイプとして'Event'を指定していることです。起動部と本体を切り分けたところで、本体を起動部が同期呼び出ししてしまうと、結局起動部がタイムアウトになってしまいます。

そこで、Lamdabの呼び出しタイプを指定します。パラメータでInvocationType: 'Event'と指定すると、Lambdaは非同期的呼び出しとなります。つまり、呼び出された側(本体)の処理が終わるのを待たずに、呼び出した側(起動部)は次の処理に進みます。

dev.classmethod.jp

このようにすることで、呼び出し側(起動部)は必要な回数だけ本体を呼び出し、本来の目的であるConnpassのAPIとの通信〜通知を本体に委ねて自分自身は処理を終えて終了します。したがって、API通信に時間がかかってもタイムアウトとなる心配はありません。

ベストプラクティスは何なのか?

以上のような構成を採ることで、ConnpassのAPIの制限とLambdaのタイムアウトを回避することができました。しかし、この方法がベストプラクティスなのかどうかは疑問です。というのも、Lambdaを複数回実行するようにしたため課金的に不利になっているからです。そしてあまりエレガントに見えない。

サーバーレスで並列・分散処理というのはよく目にし、Lambda以外にもSNSやらSQSやら何やらと使えそうなものがたくさんありません。しかし、どうにもよくわかっていないというのが正直なところです。調べている過程で、StepFunctionsを使っている例にも接しましたが、いまいちピンときていません。

qiita.com

今回の例に限らず、AWSのサービスには少しずつ親しんできていますが、本格的な業務の構成を実際に触ったことがないので、どういう構成が良いもの(少なくとも、業務利用に耐えるもの)であるのかがわからないでいます。CloudDesignPatternというのもありますが、うーん、という感じ。有識者の方からのコメントをお待ちしています。。

7/27 WEBエンジニア勉強会 #08 でNode.jsについてLTした

7/27に渋谷で開催されたWEBエンジニア勉強会 #08で、LT(5分)登壇をしてきました。6/26の「カイゼン・ジャーニー・ライトニングトークス」に続いて2回目のLT登壇でした。

web-engineer-meetup.connpass.com

ky-yk-d.hatenablog.com

WEBエンジニア勉強会について

WEBエンジニア勉強会は、OSCA(@engineer_osca)さんが主催している勉強会で、「初心者でも参加できる勉強会」というコンセプトで運営されています。今回で8回目となる勉強会ですが、僕は前回(#07)に聴く側で参加したのが初参加でした。

twitter.com

techblog.oscasierra.net

OSCAさんとは、前回のときの懇親会の他に、別のイベントでもお話をする機会があり、登壇ウェルカムと言っていただいていました。また、アウトプットメンターのカカカカック(@kakakakakku)さんからも、「WEBエンジニア勉強会参加したんですね、次回は発表しませんか?」と義務付けられ提案されていたので、今回の登壇につながりました。

LTについて

スライド資料

テーマ

テーマは悩んだものの、AWS Lambdaを通じて少しずつ触るようになっているNode.jsについて話すことにしました。具体的な題材としてhttpモジュールを選ぶことで、「WEBエンジニア」勉強会という場にもふさわしいものにできるとも考えました。

内容としては、Node.jsのhttpモジュールの基本的なコードを読み解いていき、Node.jsの特徴を垣間見ることができれば、と考えました。httpsモジュールのサンプルコードをPromiseで書き換えるという前々回の記事の内容を掘り下げて、Node.jsの仕組みを理解し、それを共有することを目標としていました。

ky-yk-d.hatenablog.com

準備をする際に参考にした資料

準備としては、基本的にはNode.jsのAPIリファレンスを読んでいました。このリファレンスはあくまでAPIリファレンスなので、内部の実装がどうなっているかはわかりにくかったため、補う意味でGitHub上のソースコードも読むようにしていました。

github.com

また、ネット上の記事も多く参照しました。調査が難航したこともあり、非常にたくさんの記事を読んだので、全てを挙げることはできませんが、一部を下記に示しておきます。

書籍としては、下記のものを参照しました。古い本ですが、Node.jsの仕組みやコアモジュールについて記述が充実していたので非常に勉強になりました。

サーバサイドJavaScript Node.js入門 (アスキー書籍)

サーバサイドJavaScript Node.js入門 (アスキー書籍)

課題と反省点

内容について(技術ネタとして)

調査をしている中で、libuvというどうやらNode.jsにとって極めて重要らしい存在にまで辿り着きました。イベントループがNode.jsの基礎にあるのですが、これを担っているのがlibuvというC言語のライブラリというわけです。となると、これを理解することはNode.jsについてより深く理解することにつながるはずだったのですが、一筋縄ではいかず、満足に調査することができませんでした。

github.com

また、httpモジュールの理解という側面ではTCP、ソケットについても多少なりとも理解できればよかったのですが、これもまともに取り組むことができませんでした。ネットワークについては基礎的な部分もきちんと理解しておきたいと思いながらも、後回しにしてしまっています。さっさと『マスタリングTCP/IP』を読まねば……

マスタリングTCP/IP 入門編 第5版

マスタリングTCP/IP 入門編 第5版

形式について(アウトプットとして)

学生症候群の一言に尽きます。

今回、登壇する日程は1ヶ月前から決まっていました。準備に費やす時間はたくさんあったにも拘わらず、(技術面でのつまずきもあったものの)集中することができずに時間が経ってしまい、当日まで直し続けることになりました。

特に、発表としての準備に時間を割くことができなかったのは反省です。構成ももう少し練ることができたと思いますし、5分間という時間の制約への対応も不十分で、スーパー早口&時間オーバーとなってしまいました。

総括

正直に言って、今回のLTは、僕にとってはかなり苦い経験になりました。技術知識の伝達としても、発表としてもクオリティが低かったことを認めざるを得ません。聴き手のみなさんには申し訳ない気持ちでいっぱいです。

とはいえ、これで懲りてやめてしまっては、それこそ申し訳が立たないので、また機会を見つけて発表をしたいです。次回は、内容の固定を早期に仕上げることを意識しようと思います。

最後に

僕自身もそうですが、知識・経験に自信を持てず、「自分が人前で発表するなんて・・・」と思っている人は少なくないのではないでしょうか。しかし、「そういった人でも気軽に登壇にチャレンジできる会にしたい」というOSCAさんの思いで運営されているのがこの勉強会だと思いますので、興味が少しでもある方は登壇してみてはいかがでしょうか。

そして、最後の最後になりますが、登壇という機会を与えてくださったOSCAさん、運営に携わっていた方々、それにLTを聴いてくださったみなさん、ありがとうございました!

サーバサイドJavaScript Node.js入門 (アスキー書籍)

サーバサイドJavaScript Node.js入門 (アスキー書籍)

SOFT SKILLS ソフトウェア開発者の人生マニュアル

SOFT SKILLS ソフトウェア開発者の人生マニュアル

Node.jsからConnpassのAPIを叩こうとしてつまずいたこと

はじめに

この記事をご覧になっている方の多くは、技術系の勉強会に参加された経験があるのではないかと思います。僕はよく参加しています。

他の業界の友人と話をして、最も驚かれるのが「業後に勉強会に出ている」ということです。毎日のようにどこかで勉強会が開かれ、様々な企業の人が職業に関わる情報をやりとりしているのはこの業界の特色でしょう。

さて、そんな勉強会の情報は、いくつかのイベント情報共有サービスを通じて周知されているものがほとんどだと思います。ConnpassやDoorkeeper、ATNDなどが有名どころでしょうか。それらのサイトを訪問すれば、日々新しい勉強会の開催情報が公開されているのを見ることができます。

しかし、サイトを訪問したり、公式のメール配信に登録する以外にも、勉強会の情報を得る方法があります。それはAPIを利用することです。APIを利用することで、特定の条件を満たすイベントの情報を自動で取得でき、様々なツール(SlackやTwitter)に送信することもできます。

そこで今回は、

  • ConnpassのAPIを利用する手順
  • 利用するに当たってつまずいたこと

についてご紹介します。

Connpass APIは登録なしですぐ使える

今回利用するのは、ConnpassのAPIです。無料で公開されています。

WebサービスAPIの中には、登録が必要なものもありますが、Connpassの場合は利用登録は必要ありません。指定されたURL( https://connpass.com/api/v1/event/)に対してHTTPリクエスト(GET)を送信するだけで、イベント情報を取得することができます。

Connpass APIでイベントの情報を取得するサンプルコード

実際に叩いてみましょう。以下のような実装で取得することができます。まだ途中ですが、Lambdaで定期実行するようにしたいので、Lambda向きの書き方になっています。

const https = require('https');
const queryString = require('querystring');

const getResponse = async (opts, queries)=>{
  opts.path +='?' + queryString.stringify(queries);  // クエリ文字列を生成してパスの末尾に付加
  console.log('path:', opts.path);
  return new Promise((resolve, reject)=>{  // httpsモジュール、Promiseについては前回記事(下記)を参照
    https.get(opts, (response)=>{
      console.log('statusCode:', response.statusCode);
      console.log('statusMessage:', response.statusMessage);
      response.setEncoding('utf8'); // Buffer型ではなくUTF-8の文字列で取得する
      let chunkCount = 0;
      response.on('data', (chunk)=>{
        chunkCount ++;
        body += chunk;
      });
      response.on('end', ()=>{
        console.log('チャンクの個数',chunkCount);
        let bodyObj = JSON.parse(body); // レスポンスはJSON形式で返ってくるのでパースする
        console.log('検索結果件数:', bodyObj.results_available);
        console.log('うち取得件数:', bodyObj.results_returned);
        console.log('1つめのイベント:', bodyObj.events[0].title);
        resolve(bodyObj);
      });
    }).on('error', (err)=>{
      console.log('error:', err);
      reject(err);
    });
  });
};

exports.handler = async ()=>{
  let opts = {
      hostname: 'connpass.com',
      path: '/api/v1/event/',
      headers: {
        'User-Agent': 'Node/8.10'  // これがないと拒否される
      }
  };
  const queries = {
    'keyword_or': [
      'アジャイル',
      'javascript'
    ]
  };
  let result = await getResponse(opts, queries);
  return result;
};

参考(httpsモジュールによるリクエストの送信)

ky-yk-d.hatenablog.com

つまずきポイント①403エラーとUser-Agentヘッダー

今回、ConnpassのAPIを利用しようとして、最初につまずいたのは、403 Forbiddenになってしまうという事象でした。Web APIを利用した経験がほぼゼロであるため、解決方法が分からずしばらく苦しみましたが、以下の記事に行き着きました。

上の記事は、GitHubAPIへのリクエストが403 Forbiddenとなっていたのを、User-Agentヘッダーを付与することで解決したというものです。User-Agentについては、以下の記事が端的でわかりやすかったです。リクエストを送信したのが何者なのかをサーバ側に教えてあげるためのものというわけですね。

www.atmarkit.co.jp

上記のQiita記事を読んだときに、「たぶんこれだ」と直感し、https.request()の第一引数のoptionsに、User-Agentヘッダーを付加して送信するように指定してあげました。すると思った通り、200 OKが返ってくるようになったではありませんか。Web APIを叩くときは要注意ですね。

なお、Node.jsからの実行なので、試しにNode/8.10と記載してみましたが、書き方をご存知の方がいらっしゃればご教示いただけると幸いです。

修正前

let opts = {
    hostname: 'connpass.com',
    path: '/api/v1/event/'
};

修正後

let opts = {
    hostname: 'connpass.com',
    path: '/api/v1/event/',
    //  ---以下を付加---
    headers: {
        'User-Agent': 'Node/8.10'
    }
    // ---追加部分終わり---
};

つまずきポイント②日本語のクエリ文字列のエンコード

つまずいた第二のポイントは、日本語のクエリ文字列のエンコードです。

ConnpassのAPIは、検索条件をクエリ文字列としてリクエストに付加して利用するのですが、そのなかの検索ワードは文字列で送信することになります。英数字であれば、何も考えずに文字列として付加して送信すればよいのですが、日本語文字列を何も考えずに送信すると失敗します200 OKにはなりますが、検索文字列として機能しないので有意味なレスポンス本文が返ってきません。

1. 何も考えずに日本語文字列を送る例

最初、このようなやり方をしていて首を傾げていました。日本語文字列がそのままパスに含められて送信されています。①と違って、HTTPリクエストとしてエラーになるわけではありませんが、レスポンス本文が取得できていないため、本文から検索結果の情報を取得するところでエラーとなります。

HTTPによる通信に慣れている人であれば絶対につまずかない箇所なのだと思いますが、生成したクエリ文字列を含むパスをChromeのURL入力欄に放り込むとちゃんと検索結果が(エンコードされていない状態で)得られるので、自分が間違ったことをしていることに気づきませんでした。

ソースコード(一部)

opts.path +='?' + 'keyword=アジャイル';

実行結果

2018-07-22T13:30:53.242Z 74a3205b-8db3-11e8-80e4-430677743f08    path: /api/v1/event/?keyword=アジャイル
2018-07-22T13:30:53.547Z    74a3205b-8db3-11e8-80e4-430677743f08    statusCode: 200
2018-07-22T13:30:53.547Z    74a3205b-8db3-11e8-80e4-430677743f08    statusMessage: OK
2018-07-22T13:30:53.550Z    74a3205b-8db3-11e8-80e4-430677743f08    チャンクの個数 1
2018-07-22T13:30:53.586Z    74a3205b-8db3-11e8-80e4-430677743f08    検索結果件数: 0
2018-07-22T13:30:53.586Z    74a3205b-8db3-11e8-80e4-430677743f08    うち取得件数: 0
2018-07-22T13:30:53.587Z    74a3205b-8db3-11e8-80e4-430677743f08    TypeError: Cannot read property 'title' of undefined
    at IncomingMessage.response.on (/var/task/index.js:24:52)
    at emitNone (events.js:111:20)
    at IncomingMessage.emit (events.js:208:7)
    at endReadableNT (_stream_readable.js:1064:12)
    at _combinedTickCallback (internal/process/next_tick.js:138:11)
    at process._tickDomainCallback (internal/process/next_tick.js:218:9)

2. 自分でエンコードして送る例

日本語文字列を含める場合は、エンコードが必要になるということでした。そのための関数はちゃんと用意されていて、下記のようなやり方で適切な形に変換することができました。実行結果を見ると、きちんと日本語文字列がエンコーディングされて送信されていることがわかります。結果もちゃんと返ってきています。

ソースコード(一部)

opts.path +='?' + 'keyword='+ encodeURIComponent('アジャイル');

実行結果

2018-07-22T13:31:57.688Z 9b0f3f00-8db3-11e8-ab20-c3947964f9d8    path: /api/v1/event/?keyword=%E3%82%A2%E3%82%B8%E3%83%A3%E3%82%A4%E3%83%AB
2018-07-22T13:31:59.441Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    statusCode: 200
2018-07-22T13:31:59.446Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    statusMessage: OK
2018-07-22T13:31:59.468Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    チャンクの個数 8
2018-07-22T13:31:59.487Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    検索結果件数: 775
2018-07-22T13:31:59.487Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    うち取得件数: 10
2018-07-22T13:31:59.487Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    1つめのイベント: 第2回enPiT-Proスマートエスイーセミナー: アジャイル品質保証と組織変革

3. querystringモジュールを利用する例

理屈の上では、2のようなやり方で足りますが、クエリ文字列の数が増えてくるとめんどくさそうです。エンコーディングについても、あまり意識したくはありません。これらの悩みを解決してくれるのが、Node.jsのモジュールであるquerystringモジュールです。

今回は、querystring.stringify()メソッドを利用しています。このメソッドでは、

  • オブジェクトからクエリ文字列への変換
  • パーセントエンコーディング(デフォルト。第四引数として関数を渡すことで変更することも可能)

を行ってくれます。また、下記の例では一つだけですが、keyword': ['aaa', 'bbb']と配列で指定してやると自動的に展開していい感じに処理してくれます。優れものですね。

ソースコード(一部)

const queryString = require('querystring');
let queries = {
    keyword: 'アジャイル'
};
opts.path +='?' + queryString.stringify(queries);

実行結果

2018-07-22T13:35:03.055Z 097210d9-8db4-11e8-a137-69efa6eedcfd    path: /api/v1/event/?keyword=%E3%82%A2%E3%82%B8%E3%83%A3%E3%82%A4%E3%83%AB
2018-07-22T13:35:03.468Z    097210d9-8db4-11e8-a137-69efa6eedcfd    statusCode: 200
2018-07-22T13:35:03.486Z    097210d9-8db4-11e8-a137-69efa6eedcfd    statusMessage: OK
2018-07-22T13:35:03.508Z    097210d9-8db4-11e8-a137-69efa6eedcfd    チャンクの個数 9
2018-07-22T13:35:03.527Z    097210d9-8db4-11e8-a137-69efa6eedcfd    検索結果件数: 775
2018-07-22T13:35:03.527Z    097210d9-8db4-11e8-a137-69efa6eedcfd    うち取得件数: 10
2018-07-22T13:35:03.527Z    097210d9-8db4-11e8-a137-69efa6eedcfd    1つめのイベント: 第2回enPiT-Proスマートエスイーセミナー: アジャイル品質保証と組織変革

おわりに

以上、ConnpassのAPIの叩き方のサンプルとつまずきポイントをご紹介しました。AWSAPI GatewayとLambdaを用いてAPIを作り、それをコールするというのは何度かやったことがありましたが、他人が作成したAPIを叩くというのは初めてだったので、思いの外つまずき、そして勉強することがありました。

現在、ConnpassのAPIを使ったボットを作成しようとしています。時間の使い方が下手でなかなか作業が進められておらず、まだ動くものになっていませんが、ソースはGitHubで公開していますので、ご興味ある方は眺めてみてください。

github.com

Node.jsのhttpsモジュールを用いた通信処理をPromiseで書き直して解読してみた

JS初心者がよくわからないまま書いたLambda

以前、このような記事を書きました。LINEのMessaging APIを用いた簡単なbotの実装の紹介記事です。実はこちらのブログで最も多くのブクマ(16ブクマ)を集めている記事となっています(2018.07.15現在)。

ky-yk-d.hatenablog.com

表題の通り、この記事を書いたのはJavaScriptを触り始めたころです。全体的に、見よう見まねでコーディングしていたのですが、中でも理解できていなかったのはhttpリクエストを送っている箇所です。この部分については完全にコピペ、意味も「あーなんか本文くっつけて送ってんだなー」くらいの理解でした。

記事の執筆から2ヶ月弱が経ち、この箇所について多少なりともわかってきたので、かつての自分と同レベルの方(来月くらいには自分もまたそこに戻っているかもしれない・・・)のためにメモを残しておくことにしました。httpリクエストを送信する部分を書き換えているので、そこを見ていきます。

書き換え(httpリクエスト送信部)

Before

書き換え前のコード

let https = require('https');
 
exports.handler = (event, context, callback) => {
// ---中略---
    var req = https.request(opts, function(res) {
        res.on('replyData', function(res) {
            console.log(res.toString());
        }).on('error', function(e) {
            console.log('ERROR: ' + e.stack);
        });
    });
    callback(null, replyData);
    req.write(replyData);
    req.end();
}

問題点/当時の理解レベル

  • requireは「httpsを使うためのおまじない」だった(Node.jsのhttpsを利用している、そしてそれがモジュールだという意識はなかった)
  • exportsを「なんか最初に実行されるやつでしょこれ」という認識だった(モジュールがわかっていないので外部公開も当然わかっていなかった)
  • on()でイベント登録をしている箇所で、bodyだったところをreplyDataに書き換えてしまっていた(イベントというものを理解していなかった)
  • 処理の途中でcallbackしていた(最後でなきゃいけないという決まりはないが、通信の結果を返すようにすべきだと今は思う)
  • callback()の第一引数がなぜnullなのかわかっていなかった
  • req.write()は「本文書き込んでいるんしょ」という認識(間違っていないけどWritable Streamということは意識していない)

After

書き換え後のコード

exports.handler = (event, context, callback) => {
// ---中略---
    let promise = sendRequest(opts, replyData).then((res)=>{
        console.log('---DONE---');
        console.log('typeof:', typeof(res));
        callback(null, res);
    },(err)=>{
        console.log('---ERROR---');
        callback(err, 'errorMsg' + err.stack);
    });
    console.log('typeof promise:', typeof(promise));
    console.log('promise:', promise);
}

// リクエスト送信部を切り出した関数
async function sendRequest(opts,replyData){
    return new Promise(((resolve,reject)=>{
        console.log('Promiseの引数の関数開始');
        let req = https.request(opts, (response) => {
            console.log('---response---');
            response.setEncoding('utf8');
            let body = '';
            response.on('data', (chunk)=>{
                console.log('chunk:', chunk);
                body += chunk;
            });
            response.on('end', ()=>{
                console.log('end:', body);
                resolve(body);
            });
        }).on('error', (err)=>{
            console.log('error:', err.stack);
            reject(err);
        });
        req.write(replyData);
        req.end();
        console.log('Promiseの引数の関数終了')
    }));
};

修正箇所

  • リクエスト送信部をハンドラ関数から切り出した。
  • httpsモジュールを用いた通信をPromiseでラップした.
  • 意識的にイベント登録をした。
  • 非同期処理が終わった後にcallbackが呼び出されるようにした。

処理の流れと解読

動作を確認するために余計なコードがたくさん含まれているのですが、このLambda関数の挙動は下記のような流れになっています(成功した場合)。順に解読していきます。ログも載せておきます。

テスト実行ログ

メッセージの場合
データ作成
---START---
Promiseの引数の関数開始
Promiseの引数の関数終了
typeof promise: object
promise: Promise { <pending> }
---response---
chunk: {"message":"Invalid reply token"}
end: {"message":"Invalid reply token"}
---DONE---
typeof: string

1. sendRequest()が呼び出され、引数の処理が行われる

ハンドラ関数がsendRequest()を呼び出すと、当然ながらnew Promise()が実行されます。引数に渡した関数の処理は、この時点で実行されるようですね。https.request()ClientRequestオブジェクトを返します(ここで接続=非同期処理が開始されていることになります)。

そして、ClientRequestオブジェクトに対して、on('error', ...)とすることでerrorイベントへのリスナーを登録しています。ClientRequestは、Writable Streamインターフェースを実装した出力ストリームの一つで、EventEmitterでもあります。EventEmitterは、Node.jsの特徴の一つであるイベントループの重要な構成要素と言ってよいと思います。

ちなみに、http(s).request()に第二引数(オプション)としてコールバック関数を渡すことで、responseイベントへのリスナーも登録していることになります。意味的にはonce('response', (...)=>{...})としても同じになるはずです。

The optional callback parameter will be added as a one-time listener for the 'response' event.

(参考)HTTP | Node.js v10.6.0 Documentation

そして、req.write()で本文を送信し、req.end()で出力ストリームを閉じています。いずれも、Writable Streamインタフェースで宣言されているメソッドです。ここまで終わると、Promiseオブジェクトが生成され、呼び出し元に返却されます。

2. 呼び出し元の処理が実行される

Promiseが返ってきたら、呼び出し元の処理が走ります。返却されてきたPromiseに対して、then()メソッドで成功ハンドラと失敗ハンドラを付加しています。Promiseを返した非同期処理の側でresolve()が実行された場合は第一引数の関数が、reject()が実行された場合は第二引数の関数が実行されることになります。

ハンドラの登録まで実行してから、promise変数に代入をし、型と内容を表示しています。この時点では、まだPromiseの中身の処理は行われておらず、Promiseは未解決(pending)となります。通常なら、awaitを付けてasync関数を呼び出し、解決後の値を変数に代入するのでしょうが、今回は動きの確認のためPromiseをそのまま代入しています。

3. 非同期処理の結果を受け取る

ここまでの間に、https.request()で開始した通信が行われています。JavaScriptはシングルスレッドなので、同時に複数の処理を実行することはできないのですが、時間のかかる処理を外部に依頼してしまって、終わるまでは別の処理を実行しておくことはできます。これが非同期処理ということですね。細かい理屈は現在調査中なので、後日まとめられればと思っています。

しばらく経つと、リクエストに対するレスポンスが返ってきます=responseイベントが発生します。このresponseイベントに対するリスナーが、http(s).request()の第二引数で登録したコールバック関数です。コールバック関数の引数(コールバックを渡された側が、呼び出し時に渡して返してくれるもの)は、IncomingMessageオブジェクトで、これはReadable Streamインタフェースを実装した入力ストリームです。送信するリクエストがWritable Streamだったのに対し、こちらはReadableとなるわけですね。

こちらもClientRequestと同様、EventEmitterなので、イベントリスナーを登録することができます。ここでは、dataイベントとendイベントのリスナーを登録しています。なお、これらのイベントは、IncomingMessage固有のものではなく、Readable Streamのものです。また、setEncodeing()エンコーディングを設定していますが、これもReadable Streamのものです。

二つのイベントの意味は、レスポンス本文が送られてきたら(dataイベントが発生したら)、順次body変数に格納し、全て取得し終えたら(endイベントが発生したら)resolve(body)を実行しろ、ということです。resolve()が実行された時点で、呼び出し元でpendingとなっていたPromiseが解決されます。

4. then()で登録しておいた成功ハンドラが実行される

Promiseが解決されると、呼び出し元であらかじめthen()で登録しておいた処理が実行され、解決した値(resolve()の引数)であるbodyの値が、resとして渡されてきます。ログにはresがstring型であると出力されますし、callbackした結果としても下記の通り、レスポンスの本文が表示されます。

f:id:ky_yk_d:20180716010212p:plain

ここまで実行されて、Lambda関数の処理が全て終了したことになります。めでたしめでたし。

今後の課題

書き換えて処理を追ったらずいぶん長文になってしまいました。簡潔にまとめるつもりだったのですが、書きながら裏取りのつもりで調べていたらその過程で学ぶことが多くありすぎ、まとめることを放棄しました(汗)

今回の書き換えと本記事の執筆のなかで、Promiseやasync/await、Node.jsのI/Oの仕組みなどに期せずして接しました。せっかく学ぶきっかけが得られたので、これからもう少し深く勉強して、後日自分なりにまとめてみようと思います!

Vue Routerのナビゲーションガードによるアクセス制限を試した&コードを読み解いた

前回の記事に引き続き、Vue Routerについての記事となります。

ky-yk-d.hatenablog.com

Vue Routerでアクセス制限を実現する

今回は、ナビゲーションガードを利用したアクセス制限を実現するコードを解読していきます。実現したいのは、「特定のパスに対してアクセス制限をかける」ことです。

今回は、コードの中核部分を『基礎から学ぶVue.js』(およびそのサポートページ)に負っています。初心者にわかりやすく、また実践的でもあり、そしてサポートページが実に充実している書籍です。ありがたい。

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

今回のコードを置いているGitHubリポジトリはこちら(すでに更新が加わっています)。このリポジトリに限らず、コードの書き方等に指摘あればぜひお知らせください!

github.com

Vue Routerの設定とナビゲーションガードの導入

前回、Vuer Routerの設定は以下のようにしていました。

let router = new VueRouter({
  routes: [
    {
      path: '',
      component: Top,
      children: [
        {
          path: '',
          component: ChildA,
          name: 'childA'
        },
        {
          path: '/childB',
          component: ChildB,
          name: 'childB',
          meta: {
            requiresAuth: true
          }
        }
      ]
    },
    {
      path: '/helloworld/:msg',
      component: HelloWorld,
      name: 'helloworld'
    }
  ]
});

/childBというパスに対して、meta要素にrequiresAuth: trueを指定しています。この要素を利用して、認証が必要なパスと不要なパスとを区分していきます。

ナビゲーションガード(グローバルガード)のコード

ナビゲーションガードを導入します。利用するのは「グローバルガード」です。

今回記載したソースコードは下記のようなものです。こちら、ほぼ『基礎から学ぶVue.js』記載のコードとなっています。

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!store.state.isLogin) {
      next({
        path: '/',
        query: {
          redirect: to.fullPath
        }
      })
    } else {
      next();
    }
  } else {
    next(); 
  }
});

このコードを読み解いてみます。

Vue Routerの様々なオブジェクト

router.beforeEach()は、ルーターインスタンスのメソッドで、画面遷移前に実行される処理を引数に記載します。処理の中では、遷移先をnext()で指定します。上の例の場合、一定の条件下でリダイレクトさせるために利用しています。

次に、ifの条件となっているto.matched.some(record => record.meta.requiresAuth)についてです。toは、遷移先を示すルートオブジェクトです。単なる文字列ではありません。公式ドキュメントの記述を引用します。

遷移先のURLの、現在の URL をパースした情報と、その URL とマッチしたルートレコードを保持しています。ルートレコードはroutes設定の配列 (とchildren配列) 内のオブジェクトのコピーです。

matchedは、ルートポブジェクトのプロパティで、この「現在のルートのネストされた全パスセグメントに対しての ルートレコード を保持している配列」です。

この場合、VueRouterのコンストラクタに渡しているオブジェクトのroutes配列の1つ目の要素(path: ''の部分)と、その中のchildren配列の2つ目の要素(path: '/childB'の部分)とを要素とする配列ということになります。

Array.prototype.some()

そして、some()メソッドはArray.prototype.some()です。

developer.mozilla.org

some()メソッドは、to.matchedという配列の各要素をそれぞれrecordとして扱い、record.meta.requiresAuthが真となる要素があれば真を返します。今回の場合、childB側の要素が条件に合致するため、some()メソッドが真を返し、認証の確認部分が実行されるというわけですね。

※今回の例では、requiresAuth: trueと指定しているchildBがネストの最下位ですが、some()を利用することで、上位のパスにrequire sAuth: trueを指定されていれば認証確認の対象となります。

認証の確認とリダイレクト、そしてVuexへ

認証確認部分はシンプルです。if(!store.state.isLogin)でログインしているかどうかを確認し、ログインしていなければnext()の引数(ルートオブジェクト)で認証不要なパスを指定します。上記の例では、クエリ文字列でリダイレクト元(本来の、許可されなかった遷移先)の情報を付加しています。

f:id:ky_yk_d:20180707184943p:plain

以上によって、「特定のパスにアクセス制限をかける」ことが実現できました。そのまま使えるサンプルコードや、親切な解説書の存在はとてもありがたいですが、その先で自力でAPIドキュメントを読んでみると、理解度が全然違いますね。Array.prototype.some()も、今回はじめて知りました。

今回は、Vue Routerのナビゲーションガードを用いて特定のパスにアクセス制限をかけてみました。今回用いたのはグローバルガードと呼ばれる最も基本的なナビゲーションガードでしたが、他の種類のナビゲーションガード、あるいはナビゲーションガード以外のVue Routerの機能も少しずつ試していきたいと思います。

また、ちらっと登場したstore.state.isLoginは、実はVuexを利用しています(アクセスの仕方がこれでいいのかは不明・・・)。まだVuexは消化不良なのですが、ある程度の段階で記事にまとめようと思います!

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js