ES2015のPromise/Promise.all()を使用して、複数サイトのRSSを取得してみます。今回はNode.jsのExpressを使用します。

並行処理と並列処理の違い

本筋の説明を始める前に並行処理と並列処理の違いをご説明します。よくPromiseがマルチスレッドで動作すると勘違いしている方がいるのですが、Javascriptはシングルスレッドです。マルチスレッドとして動作させたいときはブラウザのWeb Workerを使用するか、Node.jsのWorker Threadsを使用することになりますが、今回は話がややこしくなるのでWeb WorkerやWorker Threadsの解説は行いません。

しかし、Promiseを学ぶうえで「並行処理」と「並列処理」の違いは明確にしておく必要があります。Web WorkerやWorker Threadsで行うのは「並列処理」であり、Promiseで行うのは「並行処理」になります。

「並行処理」はあくまでもシングルスレッドによる実行なので、1つのスレッド内で割り込みを発生させながら同時に動いているように見せかけているだけです。これは、処理を速くするというより、単純に同時にやることあるいは他の処理を待たせないことが目的です。

シングルスレッドのイメージ
single-thread

「並列処理」はマルチコアCPUを使用しているときに、新しいスレッドに処理を任せて負荷分散することができます。これは処理を速く行うための方法です。

マルチスレッドのイメージ
multi-thread

ご理解いただけたでしょうか?
さて、ここから本筋のお話になります。今回はPromiseを使用するための簡易WebサーバとしてExpressを使用します。まぁ、ApacheやNginXなど、Webサーバであれば何でも大丈夫なんですが、試しに使用してみたかったという理由で採用しております。ちなみにExpressはNode.jsのMVCフレームワークも兼ねていますが、今回はMVCについての解説はせず、あくまでWebサーバとしてだけ使用します。

Express-generatorでプログラムの雛形を作る

まずはexpress-generatorをインストールし、Expressを使用したプログラムの雛形を作ります。
npmでexpress-generatorをグローバルインストールします。

npm install express-generator -g

続いて、プログラムの雛形を作成します。今回作成するプログラムではテンプレートエンジン(表示を制御するための機能)は使用しないので、テンプレートエンジンは何を選択しても良いです。今回はejsを選択します。

express --view=ejs myapp

依存関係のモジュールをインストールします。

cd myapp
npm install

expressを起動します。

DEBUG=myapp:* npm start

ブラウザからhttp://localhost:3000/にアクセスし、「Welcome to Express」と表示されることを確認します。

表示が確認できたらCtrl + cでExpressを停止させましょう。

pm2でExpressをデーモン化する

pm2を使ってExpressをデーモン化(常駐ソフト化)します。デーモン化するメリットはたくさんありますが、最も恩恵を感じるのはコードを修正した際にpm2がファイルの変更を検知して自動でExpressを再起動してくれることです。

npmからpm2をグローバルインストールします。

npm install pm2 -g

pm2でexpressを起動します。--watchを付けることでファイルの変更を監視し、変更があった場合はpm2が自動でexpressを再起動してくれるようになります。

pm2 start bin/www --watch

ログを表示してページの状態が分かるようにします。

pm2 log

PromiseでRSSを取得する

RSSの取得とパースをするためにrss-parserという便利モジュールがあるのでインストールしておきます。

npm install rss-parser --save

routes/index.jsにコードを記述します。これはreddit.comとwired.comのRSSの記事タイトルと記事URLを取得して画面に出力します。

var express = require('express');
var router = express.Router();

//モジュールの読み込み
var rssParser = require('rss-parser');

/* GET home page. */
router.get('/', function(req, res, next) {

  //reddit.comとwired.comのRSS
  var urls = [
    "https://www.reddit.com/.rss",
    "https://www.wired.com/feed/rss"
  ];

  Promise.all(urls.map(function(url) {
    return new Promise(function(resolve, reject){
      var parser = new rssParser();
      var feed = parser.parseURL(url);
      //処理の終わり(解決)を通知する
      resolve(feed);
    });
  })).then(function(feeds) {
  
    //取得したRSSデータの整形
    var result = "";
    feeds.forEach(function(feed) {
       feed.items.forEach(function(item) {
         result += item.title + ':' + item.link + ' ';
       });
    });
    
    //画面に出力する
    res.send(result);
  });

});

module.exports = router;

記述ができたらhttp://localhost:3000/ にアクセスし、RSSが取得できることを確認してください。

コードの解説

以下のコードはExpress独自の書き方です。ルート(http://localhost:3000/ )にアクセスしたときにブロック内の処理を実行するようにしています。

router.get('/', function(req, res, next) {
    //ここに処理を書く
});

処理のおおまかな流れを日本語で書いてみます。

  1. ウェブサイトのRSSを取得するためのURLを用意し、urls配列に格納します。
  2. urls配列に格納したURLをmap関数で内部関数に渡します。
  3. 内部関数でPromiseインスタンスを用意します。Promiseインスタンスには引数としてresolve()reject()を受け取る関数(function(resolve, reject){...})を与えます。与えた関数の中で処理の成否を決めるためにresolve()(解決)とreject()(拒否)を呼び出します。resolve()reject()には引数として、その後の処理に引き継ぎたい値を与えることができます。今回はfeed(サイトから取得したRSS)を与えています。今回は「解決」できる前提で書いているのでreject()は使用していません。
  4. returnされたPromiseインスタンスはmap関数によって、配列にまとめられ、Promise.all()に渡されます。
  5. Promise.all()に渡された配列の中の全てのPromiseインスタンスがresolve()を実行すると.then(function(feeds){...});が実行されます。引数feedsにはresolve(feed)で渡されたfeedの配列が渡されます。
  6. RSSのタイトルとリンクを整形して画面に出力します。

このコードを理解するためのポイントは以下の3点です。

  • 全てのサイトのRSSの取得が完了してから画面に出力しなければいけないので非同期処理を書く必要があります。そのため、Promise.all()を使用します。
  • Promise.all()に与える引数はPromiseインスタンスを格納した配列でなければなりません。従って、urls配列にmap関数を使用して、内部関数に対象サイトのURLを渡します。そして、urls配列の要素数分のPromiseインスタンスを配列にまとめてからPromise.all()に返します。
  • Promise.all()は配列として返された全てのPromiseインスタンスがresolve()を実行したタイミングで同期が完了したとみなし、.then(function(){...});を実行します。

Javascriptに慣れていない初心者でも読みやすいように、今回はアロー関数は使用せずに書きました。余力があれば、アロー関数で書き直してみてください。

参考
Express - Node.js Web アプリケーション・フレームワーク
Express のアプリケーション生成プログラム