Node.jsでWEBすくれいぴんぐ!(続編)

Node.jsでWEBすくれいぴんぐ!(続編)

コンニチワ、ワタナベデス。

今回は前回の続きとなります。

前回からしばらくJavaScriptでコードを書いていたのですが、Callback地獄に出くわしました。

コードがネストしていってどんどん右側へ・・・ 可読性も悪いし、何より書いていて楽しくない。

特にNode.jsでは入出力を非同期的に扱うのが特徴なので、無名関数を使うとファイルを一つ開くだけでもネストが一段下がります。

var fs = require('fs');

// ファイルを二つ開いて、その内容を使った処理をする場合
fs.readFile('/path/to/file', function (err, data) {
  if (err) throw err;
  fs.readFile('/path/to/file2', function (err, data2) {
    if (err) throw err;
    // dataとdata2を使った処理を行う
    console.log(data.toString(), data2.toString());
  });
});

callbackhell.comなんていうCallback地獄について書かれたサイトもあるみたいです。 何々・・・愛情を込めてCallback地獄と呼ばれている・・・オエッ。

目次

  1. Babel
    1. Babelのセットアップ
  2. スクレイピング
    1. 実行
  3. 番外編1. いくつかの落とし穴
    1. Async関数から返ってくるのはPromiseオブジェクト
    2. try-catchはしたほうが良い
  4. 番外編2. docker-selenium

Babel

嘔吐失礼しました……。 今回はそんなCallback地獄からの脱獄して前回のコードを見やすい形に修正していきます。

Babelは「Use next generation JavaScript, today.」とサイトにもあるように、 次世代の規格をもとに書いたJavaScriptコードを、現在の規格のコードへと変換してくれるコンパイラーです。

ES6で書かれたコードをES5に変換するなどができます。 (昔の名前が6to5だったらしいので、名前からもそれが伺えますね)

今回なぜBabelかというと、ES.nextの機能であるAsync Functionsが使いたいからです。 前記事余談でasync/awaitに簡単に触れましたが、これが本当に便利でCallbackを軒並み削ることができます。

// 上のコードをAsync関数で書き直したコード
require('babel-polyfill');
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));

async function main() {
  try {
    const data = await fs.readFileAsync('/path/to/file');
    const data2 = await fs.readFileAsync('/path/to/file2');

    // dataとdata2を使った処理を行う
    console.log(data.toString(), data2.toString());
  }
  catch (e) {
    console.error(e);
  }
}

main();

async/awaitはPromiseというデザインパターンを前提としているようです。 Node.jsではPromiseオブジェクトとして導入されていますが、Node.jsのfsモジュールなどの多くがPromiseオブジェクトを返すようにできていないため、bluebirdというモジュールを使ってPromiseオブジェクトを返す関数を作成しています(readFileAsyncなどのAsyncが末尾につく関数)。 Promiseについて知りたい方は、JavaScript Promiseの本(WEBサイト)に詳しく書かれているのでそちらを参照してみてください。

Babelのセットアップ

プロジェクト用のディレクトリの作成・初期化 今回は~/web_scrapingとします。

$ mkdir ~/web_scraping
$ cd !$ && npm init

Babelのインストール

# Babel
$ npm install --save-dev babel-cli
$ npm install --save-dev babel-polyfill

# ES6文法を変換するプラグイン集(preset)
$ npm install --save-dev babel-preset-es2015

# async/await使うのに必要なプラグイン
$ npm install --save-dev babel-plugin-syntax-async-functions 
$ npm install --save-dev babel-plugin-transform-regenerator

# その他、必要なモジュールをインストール
$ npm install --save bluebird
$ npm install --save webdriverio

Babelはプラグインで構成されており、プラグインを使用しないと本当に何もしてくれません(涙) 使い始め当初は、Babelさえ入れれば大丈夫だと思い、変換したら変換前のものがそのまま出力されてきて驚きました。 プラグインも一緒に入れましょう。

また、Babelは.babelrcというファイルを作成することによって、どのプラグインを使用するかを決めます。 ~/web_scraping/.babelrcを以下の内容で作成します。

{
  "presets": [
    "es2015"
  ],

  "plugins": [
    "syntax-async-functions",
    "transform-regenerator"
  ]
}

ちなみに、async/awaitは記事投稿時点ではまだES.stage3です。

ES.nextのプロポーザルには5段階あるようです: - stage0: Strawman アイデア - stage1: Proposal 提案 - stage2: Draft ドラフト - stage3: Candidate 仕様書と同じ形式 - stage4: Finished 策定完了

stage4にならないと次世代の仕様には入らないらしいので、ES2017には入らないようです。

スクレイピング

以上の手順が終了したら前回の処理を少し書き換えてみます。

~/web_scraping/index.js

require('babel-polyfill');
const webdriverio = require('webdriverio');

const options = {
  desiredCapabilities: {
    seleniumAddress: 'http://localhost:4444/wd/hub',
    browserName: 'firefox',
  },
  // logLevel: 'verbose'
};

const client = webdriverio.remote(options);

async function main() {
  try {
    await client.init();

    const title = await client.url('http://www.google.com').pause(3000).title();
    console.log(`Title was: ${title.value}`);
  }
  catch (e) {
    console.error(e);
  }
  finally {
    await client.end();
  }
}

main();

実行

$ ./node_modules/.bin/babel-node index.js

番外編1. いくつかの落とし穴

今回、リファクタリングする上でいくつかハマってしまったところがあったので紹介します。

Async関数から返ってくるのはPromiseオブジェクト

require('');
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));

async function getfile(path) {
  const data = await fs.readFileAsync(path);
  return data.toString();
}

console.log(getfile('/path/to/file'));

上記のコードですが、getfile関数でファイルの内容が入ったdatatoStringして返しています。 普通の関数だったら戻り値として文字列であることが期待できますが、実際に帰ってくるのはPromiseオブジェクトです。

require('babel-polyfill');
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));

async function getfile(path) {
  const data = await fs.readFileAsync(path);
  return data.toString();
}

// 修正1
getfile('/path/to/file').then((data) => {
  console.log(data);
})

// 修正2
async function main() {
  console.log(await getfile('/path/to/file'));
}
main();

文字列を期待して修正前のコードを実行すると

Promise {
  _c: [],
  _a: undefined,
  _s: 0,
  _d: false,
  _v: undefined,
  _h: 0,
  _n: false }

みたいな文字列が出力されて、( ゚д゚)ポカーンとなりますのでご注意を。

try-catchはしたほうが良い

require('babel-polyfill');

async function getfile(path) {
  throw new Error('エラー');
}

async function main() {
  const data = await getfile('/path/to/file');
  console.log(data);
}

main();

上の実験用のコードですがgetfile関数の中で例外を投げていますが、getfile関数を使用しているmain関数で例外をキャッチしていません。普通の関数だと例外が出力されますが、Async関数を使っている上のコードは実行すると音もなく(エラーなど何も出力されず)死にます。ご注意ください。

番外編2. docker-selenium

今回はSeleniumサーバーを動かすためにDockerを使用しました。Dockerは包み紙みたいにポイッと捨てられるのがいいですね。 Dockerを使う理由としては、WEBブラウザを動かすためにSeleniumサーバーとHTTP通信してるだけなので、分離したほうが良さそうだったためです。

Node.js ⇔ Seleniumサーバー ⇔ WEBブラウザ Node.js ⇔ | Seleniumサーバー ⇔ WEBブラウザ |

Dockerがインストールされていれば、コマンド一つでSeleniumサーバーを起動でき、ChromeやFirefoxなどの見知ったブラウザを動かすことができるようになるため便利です。前回紹介したXvfbも使う必要がありません。

$ sudo docker run -d -p 4444:4444 selenium/standalone-firefox:2.52.0

CentOS6だとDockerが公式にサポートされてないようなので、CentOS7以降かUbuntuなどのほかのディストリビューションを使ったほうが良さそうです。

今回はasync/awaitで前回のコードのリファクタリングを行いました。 地獄から抜け出るとハッピーなJSライフが送れるので是非試して見てください。

それでは、また次回よろしくお願いします~。