NodeJS」タグアーカイブ

express + mongodb でページャ機能を実装

前提

要素 バージョン
node 7.4.0
express 4.14.0
mongodb 3.2.9

概要

  • expressで動作しているAPIサーバに対して、mongodbに格納されているデータをリクエストする際に、ページを指定して取得する要素を絞り込む。
  • ページ番号はクライアントからサーバに対して送信する
  • 1ページあたりのデータ件数はサーバ側で固定する(今回は5件)
  • expressでmongodbを利用するための前準備などは割愛する

使用するmongodbのメソッド

メソッド名 説明
find(query) queryで指定したデータを取得する
skip(n) データを取得する際に先頭N件をスキップする
limit(n) データを取得する際に最大N件までとする

これらを組み合わせて以下のようにチェインすることでページングが実装できる

find(検索条件).skip((ページ番号 – 1) * 1ページあたりの件数).limit(1ページあたりの件数)

実装

今回は、Photoコレクションに含まれる全ての写真データを対象に、ページ番号のみをクライアントから指定して取得する

/* リクエストからページ番号を取得(デフォルト1) */
let page = Number(req.body.page) || 1;

/* 1ページあたりのデータ件数を5件にする */
let numPerPage = 5;

/* 全ての写真を取得 */
let c = collection('photo').find({});

/* 写真の枚数を取得 */
c.count().then(function(count) {

  /* 総ページ数を計算 */
  let pageNum = Math.ceil(count / numPerPage);

  /* 指定されたページ番号が総ページを上回っていた場合総ページ番号で置き換え */
  if (page > pageNum) { page = pageNum; }

  /* ページングを実行 */
  c.skip((page - 1) * numPerPage).limit(numPerPage).toArray(function(err, docs) {
    /* 写真一覧及びページング情報をクライアントに返却 */
    res.send({
      page: page,
      pageNum: pageNum,
      count: count,
      photo: docs,
    });
  });
});

所感

  • ページング処理とSPAの相性は抜群。僅かな待機時間もなくページを切り替えることができて気持ちいい
  • countとtoArrayで非同期処理が2回続いているので、Promissを使えばもっとキレイなコードになると思うが、Promissの使い方が上手く掴めなかったので今回は普通に入れ子を利用した

参考

AngularJSからexpressに画像をアップロードする

前提

要素 バージョン
node 7.4.0
npm 4.1.2
express 4.14.0
AngularJS 1.4.1

概要

  • AngularJSのページから画像ファイルをアップロードし、express側で保存させる
  • AngularJSには(少なくとも標準モジュールには)ファイルアップロードの仕組みが存在しないので、ディレクティブを自作して対応する

1.クライアントサイド

添付ファイルをモデルで扱うためのディレクティブを定義

app.directive('fileModel', ['$parse', function($parse) {
  return function(scope, element, attrs) {
    const model = $parse(attrs.fileModel);
    element.bind('change', function() {
      scope.$apply(() => {
        model.assign(scope, element[0].files[0]);
      });
    });
  };
}]);

上記ディレクティブは、以下のように利用する

<input type="file" name="myfile" file-model="my-file">

上記ディレクティブをinput[type=file]にバインドすることで、ファイル選択時にファイルの内容をAngularモデルにバインディングする。
これによって、添付されたファイルをAngularJSのモデルとして操作できるようになる。

ファイルアップロード用のサービスを定義

app.factory('fileUpload' , ['$http' , function($http) {
  return {
    upload(photo) {
      let formData = new FormData();
      formData.append('file' , photo);
      $http.post('/rest/photo/put', formData, {
        headers: {'Content-Type': undefined} ,
        transformRequest: null
      });
    },
  };
}]);

APIがあるであろう、’/rest/photo/put’に対して添付ファイルをPOSTする。
Content-Typeをundefinedに明示的に設定しておくと、AngularJS側で良い感じに設定してくれるので今回はおまかせする。
transformRequestをnullにしておかないと、POSTデータがJSONに変換される可能性があるので、一応明示的にNULLにしておく。

上記サービスを、添付ファイルを指定して実行することで、APIサーバに添付ファイルがPOSTされる

2.サーバサイド

Multerモジュールをインストール

Multerモジュールは、multipart/form-data形式のリクエストを扱うためのNodeJSのミドルウェア。これを用いることでexpress側で添付ファイルを処理することができる。

$ npm install --save multer

POSTされたファイルを保存する

Multerモジュールがインストールできたら、以下のコードをexpress側に追加する(※appはexpressオブジェクト)

var multer  = require('multer');
app.use(multer({ dest: './uploads/'}).any());

非常にシンプルだが、これだけで、アップロードファイルがPOSTされた際に、そのファイルを./uploadsに保存する処理が自動で行われる。

アップロードファイルの検証

アップロードされたファイルは、requestオブジェクトのfiles属性に含まれている。filesは配列であることから、複数ファイルの同時アップロードにも対応できる。

以下のコードでは、アップロードされたファイル(1件)の内容を出力し、無条件でsuccessを戻している。

app.post('/rest/photo/put' , function(req, res) {
  console.log(req.files[0]);
  res.send('success');
});

‘/rest/photo/put’に対して、画像ファイルを1件アップロードすると、以下のログが出力される

{ fieldname: 'file',
  originalname: '10.jpeg',
  encoding: '7bit',
  mimetype: 'image/jpeg',
  destination: './uploads/',
  filename: '652aa507d6e70b89e064bbddd3b33224',
  path: 'uploads/652aa507d6e70b89e064bbddd3b33224',
  size: 5297 }

ユニークなランダム文字列であるfilenameを含んだpathが既に出力に含まれているように、この時点で既に画像ファイルは保存されている。

もちろんファイル名を変更したり、保存する条件を指定したりもできるが、ここでは割愛する。

参考

ES6のファイルを自動で結合してミニファイする

前提

要素 バージョン 関連記事
node/npm 7.4.0/4.1.2 Debian8.7にMEAN開発環境を構築する
gulp 3.9.1 Babel+Gulpで始めるES6

概要

JavaScriptの次世代仕様ES6で記述された複数のJavaScirptファイルを、一つのファイルに結合し、ミニファイルする作業を自動化する。

ES6及びタスクを自動化させるgulpについては関連記事で触れているので割愛する。

JavaScriptファイルの結合について

HTTPでは基本的に1リクエスト1ファイルとなるため、クライント側で利用するJavaScriptファイルがN個あればN回余分にリクエストを発行することになってしまう。これは些細な違いとは言え、オーバーヘッドが積もりパフォーマンスの低下に繋がることもある。

そこで、複数のJavaScriptファイルを1ファイルに結合することで、JavaScriptファイルのリクエスト回数を1回に抑えることができる。

しかし、ファイルを一つにすることで、全てのファイルのスクリプトが集約するため、そのファイルの容量が増大してしまうデメリットがある。これについては、後述するミニファイの技術を用いることで緩和することができる。

JavaScriptファイルのミニファイについて

通常、プログラマがスクリプトを記述する際は、可読性などから、以下のように冗長的なコーディングを行う
– 適切な空白や改行
– 理解しやすい識別子
– コメント

しかし、これらは処理系にとっては無意味な要素であり、処理系の負担やファイルの容量を無駄に増やしてしまう。

そこで、以下の例のように、処理系にとって不要の情報を極限まで削る(ミニファイ)ことで、ファイルの容量を削減し、パフォーマンスを向上させることができる。

ミニファイ前

/* 2数の和を戻す関数 */
var getSum = function(number1 , number2) {
  return number1 + number2; // 計算結果を戻す
};

ミニファイ後

var getSum=function(b,a){return b+a};

ファイル結合とミニファイの自動化

ここでは、複数ファイルに分割して記述されたES6のコードを、タスクランナーツールgulpを用いて、以下の手順でファイルを変換する

  1. gulp-concatで一つのES6ファイルに結合する
  2. gulp-babelでES5ファイルに変換する
  3. gulp-uglifyでミニファイする

必要なnpmモジュールをインストール

node/npm/gulpインストール後、追加で下記のモジュールをインストールする

$ npm install --save-dev gulp-concat
$ npm install --save-dev gulp-uglify
$ npm install --save-dev gulp-rename

gulp-renameについては後述

gulpによる自動変換

gulpfile.js

var gulp = require('gulp'),
    concat = require("gulp-concat"),
    uglify = require("gulp-uglify"),
    babel = require('gulp-babel'),
    rename = require('gulp-rename');

gulp.task('js' , function() {
  gulp.src('scripts/*.js')
    .pipe(concat('main.js'))
    .pipe(babel({presets: ['es2015']}))
    .pipe(uglify())
    .pipe(rename('main.min.js'))
    .pipe(gulp.dest('build/'));
});

gulp.task('watch' , function() {
  gulp.watch('scripts/*.js' , ['js']);
});

gulp.task('default' , ['js' , 'watch']);

上記gulpfileは、gulp実行時及びscriptsディレクトリ内のjsファイルに変更があった場合に、jsタスクを実行する。

jsタスクは、scriptsディレクトリ内のjsファイルをconcatモジュールで結合し、babelモジュールでES5に変換後、uglifyモジュールでミニファイしている。

uglifyモジュールは、JavaScriptファイルをミニファイする機能を持っているが、それを別名で保存する機能を持っていない。そこで。renameモジュールを前項で別途インストールしておいたので、それを用いてファイル名を変換する。

実行例を以下に示す

実行前

hoge.js

/* カウンタを備えたモジュール */
let myModule = (function() {
  let n = 0;
  return {
    add: () => n++,
    get: () => n,
  };
})();

fuga.js

/* 2数の和を求める関数 */
let myFunction = (numberA , numberB) => numberA + numberB;

実行後

main.min.js

"use strict";var myModule=function(){var c=0;return{add:function b(){return c++},get:function a(){return c}}}();var myFunction=function myFunction(b,a){return b+a};

処理系にとっては等価であるが、グット軽量化できたことがわかる

所感

  • AngularJSなどのJavaScriptフレームワークを用いていると、MVC分離のためにファイル数がどんどん増えていってしまうので、やはりこういった結合・ミニファイの技術は必須だ
  • ミニファイされたコードを用いて動作確認を行うと、バグが発生した際にブラウザでコードを確認しづらい問題がある
    — これについては、ミニファイ前のファイルも生成することで、開発中はミニファイ前を使用する運用にすれば解決する
  • ファイルが増えてもスクリプトをロードするタグを追加する必要がないので楽になる
  • JavaScriptファイル間で複雑な依存関係がある場合は、別途依存対策を取る必要がありそう
  • 当たり前だが、この技術はJavaScriptだけじゃなくCSSでも適用できるはず
    — 複数のLESSファイルを一つに結合し、CSSに変換してミニファイ

参考

AngularJSとNode+express間でJSONデータのやり取りを行う

前提

要素 バージョン
node 7.4.0
express 4.14.0
AngularJS 1.4.1
Chrome 55.0x

概要

AngularJSアプリで、特定のデータをNode+expressで稼働しているAPIサーバにJSON形式でアップロードする。
一見単純そうだったが、ググった単一の情報では上手く動かなく、色々調べてやっと動いたので備忘録として残す。

AngularJSによるJSON文字列のPOST

AngularJSの$httpサービスを用いて、非同期でAPIサーバにPOSTする。その際、POSTデータのContent-Typeをapplication/jsonにする必要がある。

転送するデータは、便宜上定数で用意したuserとする。$httpサービスの機能で、JavaScriptオブジェクトは暗黙的にJSONに変換されPOSTできる。

正常にレスポンスが返却されると、successメソッドで指定したコールバック関数が呼び出されるので、そこでレスポンスボディを標準出力している。

app.factory('my_controller' , ['$http' , function($http) {
  let user = {name: 'sasaki' , age: 24};
  return {
    upload() {
      $http(
        url: '/rest/user/post',
        method: 'POST',
        data: user,
        headers: {'Content-Type': 'application/json; charset=utf-8'}
      ).success(function(data, status, headers, config) {
        console.log(data);
      });
    }
  };
}]);

Node+expressによるPOSTデータの受信

body-parserのインストール

Node側で、application/jsonのPOSTを裁くには、別途Nodeモジュールのbody-parserが必要になるため、以下のコマンドでインストールする。

npm install -D body-parser

POSTデータの受信

前項でインストールしたbody-parserを用いて、JSONを捌けるようにする。

var bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());

expressのルーティング機能を用いて、JSONを受け取る

reqオブジェクトのbodyに、JSONが文字列形式で含まれているのでそれを標準出力。
レスポンスは適当にsuccessと文字列を返しているだけ。

app.post('/rest/blog/post' , function(req , res) {
  console.log(req.body);
  res.send('success');
});

参考

PhantomJS × CasperJSによるブラウザテスト

前提

本記事の内容は以下の環境で行った

  • Debian 8.7
  • nodejs 7.4.0
  • npm 4.1.2

PhantomJSとは

JavaScriptでかけるヘッドレスブラウザ。レンダリングエンジンには、safariなどか多くのソフトウェアで用いられているWebKitが搭載されているため、基本的なJavaScriptを動かすことができる。

CasperJSとは

PhantomJSの用いてテストを作成できるテストフレームワーク。CasperJSはPhantomJSの他にも、SlimerJSというヘッドレスブラウザにも対応しているが、今回はPhantomJSと組み合わせることにする。

PhantomJS × CasperJS でできること

一言で言うなら、Webアプリケーションの自動テストが効率的に行える。PhantomJSの機能を用いてウェブブラウザの動作をしシュミレートし、各画面遷移が正常に行えるか、正しい値が表示されているかなどを、CasperJSの機能を用いて検証する。

これによって、従来は手作業でブラウザを操作し、正常動作しているかを検証していたものを自動化・高速化させることができるようになり、機能追加による既存機能のデグレーションをすぐに検知することができる。

PhantomJS / CasperJS のインストール

どちらもnpmで最新版をインストール可能

$ sudo npm install casperjs -g
$ sudo npm install phantomjs -g

コマンドで呼び出せることを確認

$ which casperjs
/usr/local/bin/casperjs
$ casperjs --version
1.1.3
$ which phantomjs
/usr/local/bin/phantomjs
$ phantomjs -v
2.1.1

ブラウザ操作 (基本編)

テストについては一旦後回しにして、まずはPhantomJSを用いたWebスクレイピングの例を以下に示す。
以下のスクリプトでは、boketeにアクセスし、トップページで取り上げられているボケの回答を標準出力する

// casper準備
var casper = require('casper').create();
// boketeにアクセス
casper.start("http://bokete.jp",function(){
  // 表示されたWebページ内から特定のDOMのテキストを取得
  var text = this.evaluate(function(){
    return document.querySelector('h3').innerText;
  });
  // 結果を標準出力
  this.echo(text);
});
// casper開始
casper.run();

実行。boketeのトップページに掲載されるボケはランダムなので、実行ごとに結果が異なる。ちなみに画像は取得していないため、回答だけ見るとモヤモヤする。

$ casperjs test.js
番組中、ふなっしーのファスナーが壊れて中の人達がうつるという放送事故発生
$ casperjs test.js
百人一首
$ casperjs test.js
事故物件の地縛霊が可愛い

最も重要なのが、this.evaluateのブロックだ。this.evaluateで指定した関数内は、現在開いているWebページをスコープにしている。つまり、関数内で書いたスクリプトは、Webページを対象に実行される。そのため、document.querySelector(‘h3’).innerText;で対象Webページ内のDOMを取得することができる。

ブラウザ操作 (応用編)

もう一つブラウザの例を以下に示す。ここでは、Twitterにアクセスし、ログインフォームにID/PWを入力してログインし、適当な文字列をツイートするところまでを自動化する。

// TwitterのID/PWを定義
var id = ‘hogehoge’;
var pw = ‘fugafuga’;

// Twitterのログイン画面にアクセスし、認証する
casper.start("https://twitter.com/?lang=ja",function(){
  this.fill('form.LoginForm' , {'session[username_or_email]': id, 'session[password]': pw} , true);
});

// 認証完了を待ち、Twitterに再度アクセス
casper.waitForUrl('https://twitter.com', function(){
  // ツイート投稿ページに移動
  this.thenOpen('https://twitter.com/intent/tweet', function(){
    // ツイートフォームにテキストを入力し、submit
    this.fill('#update-form', { 'status': 'CasperJSのテスト投稿' }, true);
  });
});

// casperを開始
casper.run();

実行すると、ちゃんと「CasperJSのテスト投稿」とツイートされることが確認できる。

このように、フォームへの入力や、POSTなども通常のブラウザと同じように行うことができる。

テスト

ブラウザテストを行う前に、簡単な単体テストの例を以下に示す。

var getA = function() {
  return 'A';
};

casper.test.begin('getAの単体テスト' , function(test) {
    test.assert(getA() === 'A' , 'Aが返却される');
    test.done();
});

上記のコードは、常に’A’を戻すという仕様の関数getAに対する単体テストである。
test.assertメソッドがテストの本体で、そこに真偽値の式と、テスト名を記述する。

これを実行すると、以下のようになる。なお、テストを行う場合はcasperjsコマンドとファイル名の間に”test”を挟む必要があるので注意。

$ casperjs test test.js
Test file: test.js
# getAの単体テスト
PASS Aが返却される
PASS getAの単体テスト (1 test)
PASS 1 test executed in 0.027s, 1 passed, 0 failed, 0 dubious, 0 skipped.

単体テストが成功したことがわかる。

次に、getA関数に下記のように意図的にバグを混入してもう一度テストする。

var getA = function() {
  return 'B';
};

以下のようにテストは失敗となり、バグが発生していることを確認することができる。

$ casperjs test test.js
Test file: test.js
# getAの単体テスト
FAIL Aが返却される
# type: assert
# file: test.js
# subject: false
FAIL 1 test executed in 0.032s, 0 passed, 1 failed, 0 dubious, 0 skipped.

Details for the 1 failed test:

In test.js
getAの単体テスト
assert: Aが返却される

ブラウザテスト

最後に、ブラウザ操作とテストを組み合わせたブラウザテストの例を示す。

例では、今日は平成何年?今現在は?を対象に、以下についてテストする

  • HTTP Statusが200(正常)である
  • ページタイトルが正常である
  • 表示される情報が正しい

以上をテストするコードを以下に示す

casper.test.begin('サンプルテスト' , 3 , function(test) {

  casper.start('https://date.yonelabo.com/' , function() {
    test.assertHttpStatus(200 , 'ステータスコードが200である');
    test.assertTitle('今日は平成何年?今現在は?' , 'ページタイトルが正しく取得できている');
  });

  casper.then(function() {
    var year = this.fetchText('#main');
    test.assert(year === '平成29年' , '年が正しく取得できる');
  });

  casper.run(function() {
    test.done();
  });

});

テストを実行すると、前述の3種類のテストが全て通っていることが確認できる。

$ casperjs test test.js
Test file: test.js
# サンプルテスト
PASS サンプルテスト (3 tests)
PASS ステータスコードが200である
PASS ページタイトルが正しく取得できている
PASS 年が正しく取得できる
PASS 3 tests executed in 0.477s, 3 passed, 0 failed, 0 dubious, 0 skipped.

上記の例のように、test.asertの他にも、test.assertHttpStatusや、test.assertTitleなど、よく用いられるアサーションが幾つか用意されている。これらを用いて、Webページが正しく表示できているかを検証することができる。

所感

今回、PhantomJS × CasperJSによるブラウザテストを試してみたが、使いやすいと思う部分、使いづらいと思う部分ともにいくつかあった。特に感じた部分は以下の通り。

  • 導入は比較的簡単だった
  • Node上でJavaScriptで書けるので、MEANのとの相性は良し
  • テストでなく、Webスクレイピングのみの使い方もできるのは嬉しい
  • 公式含めドキュメントが少なく、サンプルコードを書くだけでも苦労した
  • 立ち上がりが遅い
  • テストコード内にバグがある場合に気づきにくい
  • 入れ子が多くなり、可読性を損ないやすい
  • ↑故か、CoffeeScriptで書かれている例が多い

※ 所感の多くは、普段使っているテストフレームワークであるRspecとくらべて比較した場合の感想