JavaScript」カテゴリーアーカイブ

Zaim.jsをAPI ver2.xが使えるように改良してみた

前提

  • NodeによるZaimAPIの利用 の続き
  • 内部を書き換えるだけなので、基本的な使い方は上記記事と同様
  • 元のモジュール(Zaim.js)がMITライセンスなのでそれに準ずる

概要

npmでインストールできる、zaim.jsは、Zaim API ver1を前提としているため、ver2の機能を利用することができない。
そこで、zaim.jsのソースコードをベースに、API ver2を利用できるように改変する。

OAuth用URLの差し替え

OAuthで認証してzaimオブジェクトを作成する部分の、認証用URLをver2のURLに差し替え

変更前

this.client = new oauth.OAuth(
  'https://api.zaim.net/v1/auth/request',
  'https://api.zaim.net/v1/auth/access',
  this.consumerKey,
  this.consumerSecret,
  '1.0',
  this.callback,
  "HMAC-SHA1"
);

変更後

this.client = new oauth.OAuth(
  'https://api.zaim.net/v2/auth/request',
  'https://api.zaim.net/v2/auth/access',
  this.consumerKey,
  this.consumerSecret,
  '1.0',
  this.callback,
  "HMAC-SHA1"
);

リクエストトークン取得用のURLを差し替える

変更前

this.client.getOAuthRequestToken(function(err, token, secret) {
  that.token = token;
  that.secret = secret;
  callback('https://www.zaim.net/users/auth?oauth_token=' + that.token);
});

変更後

this.client.getOAuthRequestToken(function(err, token, secret) {
  that.token = token;
  that.secret = secret;
  callback('https://auth.zaim.net/users/auth?oauth_token=' + that.token);
});

入力データ取得用APIのURLを差し替える

ver1ではPOSTだったが、ver2ではGETになるため、それについても書き換える
変更前

Zaim.prototype.getMoney = function(params, callback) {
  var url = "https://api.zaim.net/v1/money/index.json";
  if (arguments.length === 1) {
    callback = params;
    params = {};
  }
  this._httpPost(url, params, callback);
};

変更後

Zaim.prototype.getMoney = function(params, callback) {
  var url = "https://api.zaim.net/v2/home/money";
  if (arguments.length === 1) {
    callback = params;
    params = {};
  }
  this._httpGet(url, params, callback);
};

httpGetメソッドで、パラメータを付与できるように改変する

オリジナルのhttpGetメソッドは、パラメータの付与に対応していなかったので、対応できるように修正する
変更前

Zaim.prototype._httpGet = function(url, callback) {
  if (!this.token || !this.secret) {
    throw new Error("accessToken and tokenSecret must be configured.");
  }
  this.client.get(url, this.token, this.secret, function(err, data) {
    if (err) {
      throw err;
    } else {
      callback(typeof data === 'string' ? JSON.parse(data) : data);
    }
  });
};

変更後

Zaim.prototype._httpGet = function(url, params , callback) {
  url += '?';
  Object.keys(params).forEach(function(key) {
    url += key + '=' + params[key] + '&';
  });
  if (!this.token || !this.secret) {
    throw new Error("accessToken and tokenSecret must be configured.");
  }
  this.client.get(url, this.token, this.secret, function(err, data) {
    if (err) {
      throw err;
    } else {
      callback(typeof data === 'string' ? JSON.parse(data) : data);
    }
  });
};

Zaim API ver2特有の機能を使ってみる

ここまでで、getMoneyメソッドに関してはver2のAPIを呼び出せるようになったので、ver2でしか利用できない機能を使ってみる。
下記コードは、2017年にAmazonで購入した商品の一覧を取得する。

ソースコード

zaim.getMoney({
  place: 'Amazon',
  start_date: '2017-01-01'
} , function(data) {
  console.log(data.money.map(function(m) {
    return [m.date , m.place , m.amount , m.comment];
  }));
});

実行結果

$ node zaim.js
[ [ '2017-02-12', 'Amazon', 1976, 'コーディングを支える技術' ],
  [ '2017-02-04', 'amazon', 2597, 'かじり木、チモシー、ペレット、消臭、砂' ],
  [ '2017-02-04', 'Amazon', 702, 'リンゴ酢' ],
  [ '2017-02-01', 'Amazon', 980, 'Kindle Unlimited 201702' ],
  [ '2017-01-14', 'Amazon', 1734, '野菜ジュース12本セット' ],
  [ '2017-01-09', 'Amazon', 258, '応用情報技術者の本中古' ],
  [ '2017-01-04', 'Amazon', 2880, 'データベーススペシャリスト教科書' ],
  [ '2017-01-01', 'Amazon', 18820, 'FIRE HDとそのカバーとその保証' ],
  [ '2017-01-01', 'Amazon', 980, 'Kindle Unlimited 201701' ] ]
$

購入店舗に関する情報はver2から追加された機能で、ver1ではできなかった、店舗によるフィルタリングができたことがわかる。また、ver1では支出額は負数だったが、ver2では収支に関わらず正数となっている。

また、上記実行結果には関係ないが、ver1では最大取得件数が100件だったのに対し、ver2では無制限となっており汎用性も高まっている。

他のメソッドについて

同様に他のメソッドについても、APIのURLをver1のURLからver2のURLに差し替えることで使用可能となるが、
個人的にgetMoneyメソッドさえ使えれば今は困らないのでコレ以上の改変は必要に応じて行うことにする。

参考

NodeによるZaimAPIの利用 と同様

NodeによるZaimAPIの利用

本記事で使用したZaim.jsは、最終更新が2013年で、最新のAPIを利用することができません。そのため、Zaim.jsのソースコードを直接編集し、最新のAPIを利用できるように書き換えたものを利用しています。

書き換えについては、Zaim.jsをAPI ver2.xが使えるように改良してみたをご参考ください

前提

  • node/npmが使える状態
  • Zaimのアカウントを保有しており、最低限の利用記録がある状態
  • Zaim API用のカスタマーキー、アクセストークンを何らかの手段(OAuth)で取得している状態

かなり限定的な状態が対象なので限りなく個人備忘録用

概要

クラウド型の家計簿サービスであるZaimを、Node上でAPI経由で読み書きする。
本記事ではZaim API用のカスタマーキー及びアクセストークンを取得済みで、APIを利用できる状態にあることを前提とする

Zaimとは

Zaimは、おそらく国内シェアナンバー1のクラウド型家計簿サービスであり、Web/iOS/Androidそれぞれから利用することができる。他の家計簿ソフトと同様に、会計簿の記入、集計、検索、予算管理など、家計簿サービスとしての基本的な機能を備えている他、以下の特徴的な機能を持っている。

  • レシートを撮影することで全自動で入力が行われる。精度はお察し
  • 各金融機関と連携し、入出金を自動入力。金融機関の認証情報を民間企業に渡すとかどうなんだろ
  • 年末調整とかそれなりに自動でやってくれる。正確に家計簿を記入していないと無意味

などと特徴的な機能はあんまり使いものにならないので、私は普通に家計簿の入出力にしか使っていない。(※上記は私的意見です)

Zaim API とは

OAuthにてZaimのアカウントの認証を受け、API経由で家計簿のCRUDを行うことができる。

Zaim APIはRESTFULLに実装されているので、そのまま利用することも簡単であるが、今回はZaim APIをラッピングしたNodeモジュールであるZaim.jsを利用する。

Zaim.jsのインストール

Nodeモジュールなのでnpmを用いてインストール

npm install zaim -D

Zaimオブジェクトの作成

なんらかの手段で取得したカスタマーキーとアクセスキーを指定して、Zaimオブジェクトを作成する

var Zaim = require('zaim');

var zaim = new Zaim({
  consumerKey: 'hogehoge',
  consumerSecret: 'fugafuga',
  accessToken: 'foofoo',
  accessTokenSecret: 'barbar',
});

とりあえず入力履歴を取得

getMoneyメソッドで入力履歴を取得。デフォルトでは最新20件を取得するので、とりあえず2/17の記録を全て取得する。
ちょっと恥ずかしいが、実行結果は生データ。

ソースコード

zaim.getMoney({
  start_date: '2017-02-17',
  end_date: '2017-02-17'
} , function(data , err) {
  console.log(data.money);
});

実行結果

$ node zaim.js
[ { id: 1581817347,
    user_id: 3061609,
    date: '2017-02-17',
    category_id: '101',
    genre_id: '10105',
    comment: 'サラダ、ドレッシング、卵とじ丼',
    name: '',
    active: 1,
    created: '2017-02-17 19:43:22',
    currency_code: 'JPY',
    type: 'pay',
    price: '-341' },
  { id: 1581291695,
    user_id: 3061609,
    date: '2017-02-17',
    category_id: '101',
    genre_id: '10104',
    comment: '月見大盛り',
    name: '',
    active: 1,
    created: '2017-02-17 14:06:50',
    currency_code: 'JPY',
    type: 'pay',
    price: '-350' } ]

入力データの構成

前項では、2/17の記録2件の入力データを取得した。入力データはオブジェクト形式で取得でき、以下の構成になっている。

キー 内容
id 入力ID(ユニーク)
user_id ユーザID(ユニーク)
date 記録日時
category_id カテゴリ(食費/日用雑貨などの大分類)のID
genre_id ジャンル(朝食/昼食/夕食などの小分類)のID
comment 記録に対する自由記入欄(購入したモノなどを記入する)
name 不明(外部連携とかで使う??)
active 多分論理削除すると0になる
created データの登録日
currency_code お金の種類
type 支出(pay) or 収入(income)
price 金額(支出の場合負数)

特定の入力データを取得する

ここでは、2016年に「ゲーム」で使った金額の一覧を取得する

ジャンル一覧を取得

Zaimでは、「ゲーム」は、「エンターテイメント」カテゴリに属するジャンルとしてデフォルトで登録されている。
Zaim.jsでは、genre_idで指定する必要があるので、「ゲーム」のジャンルIDがいくつなのかを調べるために、以下のコードで支出のジャンル一覧を取得する。

ソースコード

zaim.getPayGenres({
  lang: 'ja',
} , function(data) {
  console.log(data);

実行結果

$ node zaim.js
{ genres:
   [ { id: 10101, category_id: 101, title: '食料品' },
     { id: 10102, category_id: 101, title: 'カフェ' },
     { id: 10103, category_id: 101, title: '朝ご飯' },
     { id: 10104, category_id: 101, title: '昼ご飯' },
     { id: 10105, category_id: 101, title: '晩ご飯' },
     { id: 10199, category_id: 101, title: 'その他' },
(中略)
     { id: 9047786, category_id: 108, title: 'ゲーム' },
(以下省略)

ゲームの支出履歴を取得

「ゲーム」のジャンルIDが9047786であることがわかったので、これを指定して2016年のゲームの支出一覧を取得する。
全部出力してしまうと長いので、価格とコメントのみmapして出力する。

ソースコード

zaim.getMoney({
  start_date: '2016-01-01',
  end_date: '2016-12-31',
  genre_id: '9047786',
} , function(data , err) {
  console.log(data.money.map(function(m) { return [m.price , m.comment]}));
});

実行結果

$ node zaim.js
[ [ '-1080', '桃鉄' ],
  [ '-1200', 'ポケモンGO' ],
  [ '-1200', 'ポケモンGO' ],
  [ '-1200', 'ポケモンGO' ],
  [ '-600', 'ポケモンGO' ],
  [ '-600', 'ポケモンGO' ],
  [ '-600', 'ポケモンGO' ],
  [ '-950', 'ポケモン空の探検隊' ],
  [ '-706', 'マジカルバケーション' ] ]

ポケモンGOにほどよく課金していたのがわかる。

参考

ESLintによるJavaScriptコーディングルールの強制

前提

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

概要

構文チェックツール”ESLint”を用いて、JavaScriptのコーディングルールを強制し、コード品質を高める。

また、gulpによってESLintの自動実行することで、リアルタイムにコードを検証する。

JavaScript構文チェックツールについて

JavaScript構文チェックツールとは、JavaScriptコード中に潜む、シンタックスエラーやバグの元となりえる構文を静的に解析して通知してくれるツールのこと。

例えば以下のJavaScriptコードは、JavaScriptの仕様上では正常に動作する。

var hoge = 3;
var fuga = function(a , b , a) {
console.log(a + b);
}
fuga(1 , 2 , 3)

しかし、上記コードは以下の問題を含んでいる

  • 変数hogeは使用されていない(1行目)
  • 関数fugaに使用されていない第三引数がある(2行目)
  • 関数fugaの第一引数と第三引数の名前が重複している(2行目)
  • インデントされるべき行でされていない(3行目)
  • セミコロンが挿入されていない(5行目)

このような、実行時のエラーとはならないが、潜在的なバグの原因となったり、可読性を損ねるようなコードが含まれている場合にそれを通知するのが構文チェックツールの役割だ。

ESLintについて

ESLintは、他の構文チェックツールと比べて以下の特徴を持っている

  • 全ての検証ルールについて、検証のON/OFFを選択できる
  • 独自の検証ルールを作成することができる
  • ルールごとのドキュメントが充実している

今回はNode環境上にESLintを導入し、推奨の設定に加えていくつかの検証ルールを追加でONにすることにする。

ESLintのインストール

本記事では、gulp上でESLintを利用するので、gulp-eslintをnpmでインストールすることで、依存先のeslintもまとめてインストールする。

npm install -D gulp-eslint

.eslintrc.jsonの作成

eslintの設定は、隠しファイルである.eslintrc.jsonに記述する。今回作成したeslintrcを以下に示す。

{
    "extends": ["eslint:recommended"],
    "plugins": [],
    "parserOptions": {},
    "env": {
      "browser": true,
      "es6": true
    },
    "globals": {},
    "rules": {
      "no-var": "error",
      "semi": "error",
      "block-spacing": "error",
      "indent": ["error" , 2],
      "no-mixed-spaces-and-tabs": "error",
      "no-multiple-empty-lines": ["error" , {"max": 1}],
      "no-trailing-spaces": "error",
      "space-infix-ops": "error",
      "dot-notation": "error",
      "eqeqeq": "error",
      "no-else-return": "error",
      "no-loop-func": "error",
      "arrow-parens": "error",
      "arrow-spacing": "error",
      "no-console": "warn",
      "no-undef": "off"
    }
}

eslintrcの主な構成は以下の通り

  • extendsでベースとなる設定を指定し、ここではeslintの推奨設定を継承する(2行目)
  • eslintの対象コードの実行環境を指定し、ここではブラウザ上で動作し、ES6を利用していることを記述する(5行目)
  • 追加ルールを指定する。ベースとなる推奨設定に含まれるルールを記述する必要はない(10行目)

ruleは{ルール名: 設定}の形式で記述する。基本的に設定は、以下の3択で指定し、場合によっては追加設定情報を配列で与える。

  • “error”: ルールに反している場合に警告を出力する(強調)
  • “warn”: ルールに反している場合に警告を出力する(控えめ)
  • “off”: 警告を出力しない

上記eslintrcに含まれるルールは以下の通り

ルール名 ルール内容 異常系 正常系
no-var varの使用を禁止 var hoge let hoge
semi セミコロンの省略を禁止 let hoge let hoge;
block-spacing 単一行ブロックで内側スペースの挿入を強制 if(hoge) {fuga();} if(hoge) { fuga(); }
indent(2) インデントをスペース2字の強制
no-mixed-spaces-and-tabs タブとスペースのインデントの混在を禁止
no-multiple-empty-lines(1) 2行以上連続した空行を禁止
no-trailing-spaces 行の末尾へのスペースの付与を禁止
space-infix-ops 演算子の前後にスペースを強制 a=b+c; a = b + c;
dot-notation ドット表記可能な場合にドット表記を強制 hoge[‘fuga’] hoge.fuga
eqeqeq ==の使用を禁止 a == b a === b
no-else-return else文での不要なreturnを禁止
no-loop-func ループ内で関数を定義することを禁止
arrow-parens アロー関数の特定フォーマットを強制 (a) => a * 2; a => a * 2
arrow-spacing アロー関数の矢印の前後にスペースを強制 a=>a * 2; a => a * 2
no-console console.logの禁止
no-undef 宣言されていない変数の使用を禁止

※ no-consoleはデバッグでは使用したいため、errorではなくwarningに設定
※ no-undefは、外部ライブラリで宣言された変数を利用することがあるのでoffに設定

gulpfileを編集

var gulp = require('gulp'),
    eslint = require('gulp-eslint');

gulp.task('eslint' , function() {
  return gulp.src('app/scripts/*.js')
    .pipe(eslint())
    .pipe(eslint.format())
});

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

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

上記gulpfileにて、gulp実行時及びapp/scripts/*.jsに変更が生じた場合に、eslintを実行し、errorかwarningが発生した場合に警告を出力する。

デモ

画面左側でJavaScriptのコーディングを行い、画面右側でESLintを実行するgulpが動いている。
デモ内では、初めに「varの使用を禁止」「セミコロンの使用を強制」「参照されない変数の宣言禁止」に引っかかるコードを記述し、ESLintに指摘を受けながらそれを修正している


(クリックで拡大)

所感

  • 自分の中でコーディングルールを定めでも、それを実行できてないことも少なくない。ESLintを導入すると大リーグボール養成ギブスを付けているかのような感覚でコーディングすることになり、初めは息苦しくも感じるが、それが自然とできるようになってくると、コーディングスタイルの一貫性が取れるようになり良いコードを無意識で書けるようになると思う。
  • 例によってgulpとの相性は最高に良い。デモのように画面を分割してリアルタイムで検証できるのは、まるで統合開発環境を使ってるようで気持ちが良い
  • 今回始めてgifアニメーションを撮ってみたが、結果的にテイク10ぐらいかかった。何故録画してるとタイポが増えるのだろうか。撮影は大変だが、百聞は一見に如かずなので今後も無駄にテキスト増やすよりもデモを用意するようにしよう

参考

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');
});

参考

CasperJSチートシート

前提

PhantomJS × CasperJSによるブラウザテスト の実質的な続き。

本記事について

CasperJSを用いたテストコードのうち、再利用性が高そうなものを整理する。本記事は定期的に加筆修正を行う

テストの雛形

1024×690のブラウザで特定URLにアクセス、ステータスコードとページタイトルを検証した後にスクリーンショットを撮影する

var url = '';

casper.start();
casper.test.begin('テストの雛形'  , function(test) {

  casper.open(url).viewport(1024, 690).then(function() {
    var expectStatus = 200;
    var expectTitle = '';
    test.assertHttpStatus(expectStatus , 'ステータスコードが200である');
    test.assertTitle(expectTitle , 'ページタイトルが正しく表示される');
  });

  casper.run(function() {
    test.done();
		this.capture('ss.png');
  });

});i

フォームへの入力とsubmit

registerFormに対して、useridにtestと入力してsubmitする。

casper.then(function() {
  this.fillSelectors("form[name='registerForm']", {
    "input[name=userid]": 'test'
  }, true);
});

なお、入力だけ行ってsubmitしない場合は、末尾のtrueをfalseに変えること

開いてるページへのJavaScript実行

該当ページに対してjQueryでDOMの値を入手し、それを検証する

casper.then(function() {
  var value = this.evaluate(function() {
    return $('h1').text();
  });
  test.assertEquals(value , '見出し');
});

evaluateメソッドで渡す関数の戻り値がevaluateメソッド自体の戻り値になる。
h1の要素ぐらいならCasperのメソッドで直接取得できます。実際はCasperのメソッドだけじゃ事足りない細かなスクリプトを実行したい場合に用いる

特定要素が表示されるまで待機する

セレクタでDOMを指定し、そのDOMが表示されるまで次のステップに行かずに待機する。
JavaScriptなどで動的にDOMを生成し、それに時間がかかるような場合に有用

casper.waitForSelector('#hoge', function() {
  // #hogeが表示されたら実行する
});

特定のテキストが表示されるまで待機する

ページ内に指定したテキストが表示されるまで、次のステップに行かずに待機する。
waitForSelectorより使い勝手良さそう

casper.waitForText("hoge", function() {
  // テキスト'hoge'が表示されるまで待機
});

CasperJSのtesterモジュールまとめ

前提

PhantomJS × CasperJSによるブラウザテスト の実質的な続き。

本記事について

CasperJSで使用できる、テストモジュールのリファレンスに日本語版が存在しないこと、軽く調べても日本語でまとめられた情報がなかったことから、公式リファレンスを元にリファレンスを整理した。

なお、全ての関数について試せたわけではないこと、誤訳から誤った内容が書いてある可能性もあるので、必要に応じて公式リファレンスを参照すること。

testerモジュール一覧

※ オプション引数についての説明は省略

関数名 引数 内容
assert (Boolean condition[, String message]) conditionが真であるかを検証
assertNot (mixed subject[, String message]) subjectが偽であるかを検証
assertTruthy (Mixed subject[, String message]) subjectが真であるかを検証
assertFalsy (Mixed subject[, String message]) subjectが偽であるかを検証
assertEquals (mixed testValue, mixed expected[, String message]) testValueとexpectedと同値であるかを検証
assertNotEquals (mixed testValue, mixed expected[, String message]) testValueとexpectedが異なる値であることを検証
assertMatch (mixed subject, RegExp pattern[, String message]) subjectが正規表現patternにマッチするかを検証
assertType (mixed input, String type[, String message]) inputの型がtypeであることを検証
assertTitle (String expected[, String message]) ページタイトルがexpectedであることを検証
assertTitleMatch (RegExp pattern[, String message]) ページタイトルが正規表現patternにマッチするかを検証
assertHttpStatus (Number status[, String message]) HTTPステータスコードがstatusであることを検証
assertUrlMatch (Regexp pattern[, String message]) URLが正規表現patternにマッチするかを検証
assertSelectorHasText (String selector, String text[, String message]) selectorで指定した要素が、プレーンテキストtextを保有しているかを検証
assertSelectorDoesntHaveText (String selector, String text[, String message]) selectorで指定した要素が、プテーんテキストtextを保有していないことを検証
assertTextExists (String expected[, String message]) ページ内にプレーンテキストtextが存在するかを検証
assertTextDoesntExist (String unexpected[, String message]) ページ内にプレーンテキストtextが存在しないことを検証
assertField (String or Object input, String expected[, String message, Object options]) name属性がinputの要素の入力値がexpectedであることを検証(下記との差異が不明)
assertFieldName (String inputName, String expected[, String message, Object options]) name属性がinputNameの要素の入力値がexpectedであることを検証(上記との差異が不明)
assertFieldCSS (String cssSelector, String expected, String message) CSSセレクタcssSelectorに合致するDOMの入力値がexpectedであることを検証
assertFieldXPath (String xpathSelector, String expected, String message) xpathSelectorで指定したフォームの入力内容がexpectedであることを検証(?)
assertExists (String selector[, String message]) selectorに該当するDOMが存在するかを検証
assertDoesntExist (String selector[, String message]) selectorに該当するDOMが存在しないことを検証
assertElementCount (String selector, Number count[, String message]) selectorに該当するDOMの個数がcountと一致しているかを検証
assertVisible (String selector[, String message]) selectorの対象DOMのうち1つ以上が表示されているかを検証
assertAllVisible (String selector[, String message]) selectorの対象DOM全てが表示されているかを検証
assertNotVisible (String selector[, String message]) selectorの対象DOMのうち1つ以上が非表示であることを検証
assertEval (Function fn[, String message, Mixed arguments]) 対象ページに対して関数fnを実行し、戻り値が真であることを検証する
assertEvalEquals (Function fn, mixed expected[, String message, Mixed arguments]) 対象ページに対して関数fnを実行し、戻り値がexpectedであることを検証する
assertRaises (Function fn, Array args[, String message]) 関数fn実行時に、例外が発生した化を検証
assertResourceExists (Function testFx[, String message]) 対象ページにて、指定したリソースが読み込まれているかを検証する(?)
assertInstanceOf (mixed input, Function constructor[, String message]) inputがconstructorのインスタンスであるかを検証する

参考

The tester module

Babel+Gulpで始めるES6

前提

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

  • Debian 8.7
  • nodejs 7.4.0
  • npm 4.1.2

概要

本記事では以下について紹介する

  • JavaScriptの次世代仕様であるES6について
  • Babelを用いたES6コードの変換
  • Gulpを用いたBabelの自動実行

ES6(ECMAScript6またはECMAScript2015)

2015年に策定されたJavaScriptの新仕様。JavaScriptに主に以下のような言語仕様を追加した、次世代のJavaScriptと呼べるもの。

  • classベースによるオブジェクト指向
  • アロー関数
  • ブロックスコープを持つ変数/定数の定義
  • 文字列内での変数展開
  • 分割代入
  • 汎用的な非同期処理

など、他言語の良いところをドンドン取り入れて、現代的になったもの。
なお、ES6についてはご存知の方も多いと思うため、詳細は割愛するので必要に応じてググってください。

ES6の問題点

ES6はあくまでW3Cが策定した標準仕様に過ぎず、それを実装するのは各ブラウザベンダである。
そのため、ブラウザによって実装されている機能、されていない機能がバラバラで、特にモバイル系のブラウザと例によってIEは全体的にES6の対応ができていない。

よって、2017年現在でもES6のコードをそのまま使うのはあまり現実出来ではない問題がある。

Babelとは

そこで登場するのが、Babelというツールである。BabelはES6を含んだJavaScriptコードを、ES5以前(基本的に全てのブラウザで動くコード)に変換してくれるツールだ。

Babelを導入することで、フロントエンジニアは最先端のJavaScriptコードを記述しながら、ビルド時には旧コードに変換してブラウザで実行することで、ES6の問題点を解決することができる。

Babelのインストール

node環境であればBabelのインストールは以下のコマンドでインストールすることができる。

$ sudo npm install babel-cli -g
$ sudo npm install --save-dev babel-preset-es2015 -g

Babelの実行

下記のコードは、ES6の機能のうち幾つかを使用した、特に意味のないコードである。ちなみにこのコードでは、ES6のクラス構文、文字列内での変数展開、アロー関数、ブロックスコープを使っている。

class Human {
  constructor(name) {
    this.name = name;
  }
  greeting() {
    console.log(`Hello, My name is ${this.name}`);
  }
}

let createHuman = (name) => new Human(name);

createHuman('sasaki').greeting();

上記のコード(hoge.js)をbabelを用いて変換する。babelでは、特に指定が無い場合変換結果を標準出力するので、fuga.jsに出力する。

$ babel hoge.js --presets es2015 > fuga.js

変換されたコード(fuga.js)が以下である。

'use strict';

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Human = function () {
  function Human(name) {
    _classCallCheck(this, Human);

    this.name = name;
  }

  _createClass(Human, [{
    key: 'greeting',
    value: function greeting() {
      console.log('Hello, My name is ' + this.name);
    }
  }]);

  return Human;
}();

var createHuman = function createHuman(name) {
  return new Human(name);
};

createHuman('sasaki').greeting();

Class構文がprototype構文に変換されている所が非常に複雑でわかりづらいが、アロー関数を使用していた部分がfunctionに書き換えられていることは一目瞭然だろう。
全体的にコードの内容及びレイアウトが複雑になってしまうが、変換後のコードはプログラマが読み書きすることは無いので、可読性などは不要である。

変換の前後で実行結果が変わらないことを以下に示す。

$ node hoge.js
Hello, My name is sasaki
$ node fuga.js
Hello, My name is sasaki

Gulpとは

前項で、Babelコマンドを用いてES6のコードを変換できることはわかった。しかし、フロント開発をする際に、コードを修正する度にBabelコマンドを実行するというのは現実的ではない。

そこで、タスクランナーツールのGulpを用いて、Babelの実行を自動化する。Gulpなどのタスクランナーとは、特定のタイミングで特定のタスクを処理してくれるツールのことで、Gruntなどが有名である。

今回は、比較的新しく、タスクの記述が簡単なGulpを導入し、JavaScriptコードが更新される度にBabelを自動実行するタスクをGulpで記述する。

Gulpのインストール

Gulp本体と、GulpからBabelを実行するモジュールをインストールする。

$ npm install --global gulp
$ sudo npm install --save-dev gulp-babel

タスクの記述

Gulpでは、gulpfile.jsというファイルに、JavaScriptの形式でタスクを記述する。Gulp自体については、本記事ではあまり紹介しないので、気になる方は参考ページを参照してください。

今回作成したgulpfileを以下に示す。

var gulp = require('gulp');
var babel = require('gulp-babel');

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

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

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

ザックリ説明すると、このgulpfileでは、次のようなタスクを実行する。

「app/scripts/内のjsファイルが更新されたら、Babelで変換してapp/scripts/build/にファイルを作成する」

gulpの実行

今回作成したgulpfileには、ファイル監視のタスクを含めているため、バックグラウンドで動作させたままにするのが望ましい。

$ gulp &

gulpがバックグラウンドで動いている間に、該当のJavaScriptファイルを編集し、保存をした瞬間に、以下のようにgulpがタスクを実行する。

[06:45:19] Starting 'babel'...
[06:45:19] Finished 'babel' after 2.86 ms
[06:45:19] Starting 'babel'...
[06:45:19] Finished 'babel' after 2.3 ms

タスクがきちんと実行され、app/scripts/buildディレクトリの方に変換後のファイルが自動で作成される。ご覧のとおり、1ファイルの変換で系5ミリ秒ほどしかかからないため、プログラマはそれを意識すること無く開発を行うことができる。

なお、本記事とは直接関連は無いが、Gruntとそのプラグインを導入することで、以下のようなことも可能になる。

  • 複数のJavaScriptファイルを1ファイルに結合
  • 結合したファイルをミニファイ
  • ブラウザを再読込

モダンなフロント開発ではJavaScriptのコード量、ファイル数が多くなってくるため、1ファイルにまとめてミニファイすることは必須とも言える。

所感

今回、BabelやGulpを導入した根本的な目的は、ES6でJavaScriptを記述することである。そのため、本項ではES6に関する所感をまとめる。

  • prototypeベースとかいう個人的にまったく受け付けないオブジェクト指向を書かずに済むのは本当に助かる
  • JavaScriptは関数が第一級オブジェクトであるため、頻繁に関数定義を行うので、アロー関数はコードの記述量を大幅に減らせて助かる
  • ブロックスコープに寄って、例えばfor文内だけで有効な変数を宣言できるのは大きい。
  • 文字列内で変数を使いたい時に、チマチマ + を使っていたのが辛かったので今更ながら実装されてよかった。

以上より、今後もES6を使える場面では率先して使いたいし、周りにも布教してBabel,Gulpを用いたモダンな開発を取り入れていきたい。

参考

もうはじめよう、ES6~ECMAScript6の基本構文まとめ(JavaScript)
Babelで始める!モダンJavaScript開発
ドットインストール gulp入門 (全12回)

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とくらべて比較した場合の感想

AngularJSの専門用語整理

※AngularJSはとても奥が深く、まだ調べ始めたばかりなので誤りがあるかもしれません。
※AngularJSの専門用語と称していますが、基本的な概念はAngular2でも通用すると思います。
続きました

双方向データバインディング


AngularJS最大の機能の一つ。UIが表示しているデータと、JavaScriptが内部で保持しているデータを自動で同期させる機能。

ただし、同期が行われるのはUIに対するイベントによって値が変更された場合のみなので、jQueryなどで直接値を書き換えた場合には手動で同期する必要がある。

そもそもAngularを用いてフロントエンドの開発をする場合、闇雲にjQueryを使うべきではないと思われる。

モジュール


AngularJSアプリの最大単位。小規模であれば1アプリ1モジュールでも良い。規模が大きいなら、機能ごと、コンポーネントごとにモジュールを作るなどができる。

モジュール内で他モジュールを依存先として指定することができ、これはサードパーティー製のモジュールの機能を引き継ぐ時に使える。

例えば以下の場合は、ngRouteモジュール、ngAnimateモジュールに依存するmyappモジュールを作成できる。

myapp = angular.module('degulog', ['ngRoute' , 'ngAnimate']);

生成したモジュールに対して、各種コントローラ、サービス(後述)、ディレクティブ(後述)などを定義する。

サービス(service)


特定の目的を持った関数(オブジェクト)

AngularJSにおけるサービスは、シングルトンオブジェクトで、状態や振る舞いを保持することができる。そのため、一般的なMVCにおけるモデル層をサービスとして定義する。

サービスは使い方に応じて、Sercice,factory,provider,valueなどに分類されるが、モデルを記述する場合は基本的にfactoryを用いる。

以下はカウントアップ機能を備えたサービスの定義である。

myModule = angular.module(‘mymodule’ , []);
myModule.factory(‘countup’  , function() {
  let count = 0;
  return {
    add: () => ++count,
    get: () => count,
  };
});

上記のように、クロージャになるように記述することで、状態の保持ができるようになるのでモデルとして利用することができる。

ディレクティブ


HTML上で利用できる拡張タグ、クラス、属性のこと。<ng-***>はAngularJSに標準で搭載されたディレクティブ。

js

var app = angular.module("myApp", []);
app.directive("test", function () {
    return {
        restrict: "E",
        template: "<div>テスト</div>"
    }
})

html

<test></test>

これをブラウザで開くと、以下のように変換される

<test><div>テスト</div></test>

例はシンプルすぎるもので、ただの置換にも見えるが、ディレクティブでは要素に属性を付与したり、オブジェクトを操作したりと様々なことができる。

<

h2>

スコープ($scope)


スコープはServiceの1種で、ビューとコントローラの懸け橋となるオブジェクト。

コントローラでscope内に定義したデータ(メソッド)は、対応するビューから参照(実行)することができる。

逆に、ビュー側で名前付けしたDOMを、コントローラでscopeを通して読み書きしたり、イベントを監視することができる。

AngularJSではDOMを直接操作することは推奨されていないので、原則scopeを通してビューを操作する。

フィルター


データを出力する際に特定の加工を施す機能。例えば、テンプレートにて以下のような記述をすると、objectをJSONに変換して出力する

{{obj | JSON}}

フィルターはモジュールで自作することができ、入力に対する出力を返す関数であれば好きなように作成できる。