月別アーカイブ: 2017年1月

LESSを用いたCSSの効率化とGulpによる自動コンパイル

前提

前提となる環境は以下の通り。

ツール バージョン
node 7.4.0
npm 4.1.2
gulp 3.9.1

上記環境のインストールについては下記参照
Debian8.7にMEAN開発環境を構築する
Babel+Gulpで始めるES6

LESSとは

CSSをロジカルに記述できるメタ言語。通常のCSSを拡張する形で、主に以下の機能が利用できるようになる。作成したLESSファイルは、LESSコンパイラを通してCSSに変換して利用する。

  • 変数代入
  • 階層構造化
  • 条件分岐
  • ミックスイン
  • 継承
  • 豊富な標準関数

LESS.jsによる動的なコンパイル

LESSは、基本的にコンパイラを用いてデプロイ前に静的に変換するが、コンパイル環境を持たずとも、LESS.jsを用いることでWebブラウザ上で動的にCSSに変換することができる。

LESS.jsはCDNから利用できるので、該当のHTMLページでそれをロードし、それより上でLESSのファイルをCSSと同じようにロードする。下記にその例を示す。

<html>
<head>
  <link rel="stylesheet/less" type="text/css" href="hoge.less">  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/less.js/2.2.0/less.min.js"></script>  
</head>
<body>
</body>
</html>

これで、LESS.jsは読み込まれた時点でリソースに存在するlessファイルをcssに変換する。非常にお手軽ではあるが、ブラウザに変換を任せてしまうこと、コード量が多くなってしまうことから、デプロイ時には静的変換をかけるようにし、LESS.jsの使用は開発中に限ったほうが良いと思われる。

lessコンパイラのインストール

本記事の前提の項で触れているとおり、gulp(3.9.1)が使える状態から始まっているので、ここではgulpでlessを利用するためのパッケージをインストールする

$ sudo npm install -D gulp-less

インストールされたバージョンを確認

$ npm ls gulp-less
degulog@1.0.0 /home/vagrant/less-test
└── gulp-less@3.3.0

gulpにタスクを定義

ここでは、app/styles/style.lessに変更を検知したら、それをcssに変換し、app/styles/build/style.cssに保存するタスクを記述する。

gulpfile.js

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

gulp.task('less' , function() {
  gulp.src('app/styles/style.less')
      .pipe(less())
      .pipe(gulp.dest('app/styles/build/'));
});

gulp.task('watch' , function() {
  gulp.watch('app/styles/style.less' , ['less']);
});

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

これで、gulpコマンド実行時と、実行中にファイルが書き換わった際に自動でコンパイルしてくれるのでコーダーは一切意識すること無くLESSだけを記述することができる。

LESS構文例

ここからは、LESSの主な機能について特に意味のない極シンプルなコード例を示す。コードは、変換前のlessファイル、変換後のcssファイルの順で掲載する。

変数の利用

@変数名: 値
の形式で宣言できる。単純な計算や文字列展開も可能。

変換前

@header1: 18px;
@header2: @header1 - 4;
@imgdir: "/img";
h1 {
  font-size: @header1;
}
h2 {
  font-size: @header2;
}
.content {
  background-image: url("@{imgdir}/bg1");
}

変換後

h1 {
  font-size: 18px;
}
h2 {
  font-size: 14px;
}
.content {
  background-image: url("/img/bg1");
}

階層構造化

ある要素の子要素にのみスタイルを振ると行った場合に有効。もう冗長にしか見えないコードを書く必要がない。

変換前

h1 {
  font-size: 18px;
  a {
    text-decoration: none;
  }
}

変換後

h1 {
  font-size: 18px;
}
h1 a {
  text-decoration: none;
}

条件分岐

変数の値を元に、スタイルの設定を分岐させる

変換前

@width: 400px;

.content when (@width <= 400px) {
  font-size: 12px;
}
.content when (@width > 400px) {
  font-size: 14px;
}

変換後

.content {
  font-size: 12px;
}

ミックスイン(関数)

イメージは関数呼び出し?宣言済みのスタイルを取り込むことができる。

変換前

.my-header(@font-size: 16px) {
  text-align: center;
  font-size: @font-size;
}
h1 {
  .my-header();
}
h2 {
  .my-header(14px);
}

変換後

h1 {
  text-align: center;
  font-size: 16px;
}
h2 {
  text-align: center;
  font-size: 14px;
}

継承

ミックスインと似ているが、生成されるCSSを見ると分かる通り、こちらのほうがコード量が少なく、親子関係もわかりやすくなる。

変換前

.my-header {
  text-align: center;
}
h1 {
  &:extend(.my-header);
  color: red;
}
h2 {
  &:extend(.my-header);
  color: blue;
}

変換後

.my-header,
h1,
h2 {
  text-align: center;
}
h1 {
  color: red;
}
h2 {
  color: blue;
}

豊富な関数

LESSには様々な関数が用意されている。以下はその1例で、@base-colorを元に、それより10%明るい色を計算して設定してくれる

変換前

@base-color: #aa1111;
h1 {
  color: lighten(@base-color , 10%);
}

変換後

h1 {
  color: #d81616;
}

所感

  • 個人的には、階層構造化できる点が一番大きい。CSSでいつも冗長な記述をしている気がしてならなかったので
  • Node環境を用意できなくてもJavaScriptファイルをロードするだけでLESSが使えるというのは大きい。これならデザイナーさんなども手軽に利用できる
  • 最近学習した技術と比較しても、覚えることが非常に少なくそれでいてパワフルな生産性を生み出せるコスパの良い技術だと思った。従来のCSSに対して上位互換を持っている分、既存のCSSをそのまま使いつつ一部分だけLESSで拡張すると行ったこともできるので、導入コストは非常に小さいと思われる

参考

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

Markdown プラグイン追加してみました

見出し(H1〜H6)

シャープ(#)を先頭に記述すると見出しになります。

見出し H1

見出し H2

見出し H3

見出し H4

見出し H5
見出し H6

テキストの装飾

アスタリスク()、アンダーバー(_)でテキストを囲むと文字を装飾できます。
*テキストを強調する(EM)

テキストを強調する(EM)
テキストを太字にする(STRONG)
テキストを太字にする(STRONG)


箇条書きリスト(UL)

アスタリスク(*)、ハイフン(-)、プラス(+)を先頭に記述すると箇条書きになります。
* リスト
* リスト
* リスト
* リスト
* リスト
* リスト
* リスト
* リスト


数字の箇条書きリスト(OL)

数字、ドット(.)を先頭に記述すると数字の箇条書きになります。
1. リスト
1. リスト
1. リスト
1. リスト
1. リスト
1. リスト
1. リスト
1. リスト


引用(BLOCKQUOTE)

文章の先頭に大なり記号(>)を記述すると引用になります。

Markdown(マークダウン)は、文書を記述するための軽量マークアップ言語のひとつである。もとはプレーンテキスト形式で手軽に書いた文書からHTMLを生成するために開発された。


リンク(A)

テキストにリンクを設定できます。


複数のリンクをまとめて記述する

検索エンジンといえば、GoogleYahoo!JAPAN が定番ですが、Bing のことも時々思い出してやってください。


脚注

記事下に補足を挿入できます。
Markdown(マークダウン)1記法を利用すると、記事作成を効率化2できます。


  1. Markdown(マークダウン)は、文書を記述するための軽量マークアップ言語のひとつである。もとはプレーンテキスト形式で手軽に書いた文書からHTMLを生成するために開発された。現在ではHTMLのほかパワーポイント形式やLATEX形式のファイルへ変換するソフトウェア(コンバータ)も開発されている。各コンバータの開発者によって多様な拡張が施されるため、各種の方言が存在する。(Wikipediaより) 
  2. 全ての場合において効率化になるわけではありません。自分で試して、役立つようであれば活用してください。


    略語(ABBR)

    略語を利用する場合に正式名称を表示できます。
    このプラグインはテキストをHTMLに自動変換します。
    *[HTML]: HyperText Markup Language


    水平罫線(HR)

    ハイフン(*)、アンダーバー(_)、ハイフン(-)を3連続で記述すると水平の罫線になります。


    ___

    参考

    【追加プラグイン】JP Markdown
    【参考URL】JP Markdown – マークダウン記法で記事が書けるWordPressプラグイン 

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}}

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

Debian8.7にMEAN開発環境を構築する

概要


MEANは近代的でフルスタックな開発フレームワーク。以下の構成要素の頭文字をつなげたもの

M MongoDB NoSQLなデータベース
E Express Node上で動くWebアプリケーションフレームワーク
A AnglarJS フロントエンドのMVCフレームワーク
N NodeJS サーバサイドのJavaSciprt実行環境(Webサーバの役割)

今回は、vagrantを用いて以下のように仮想環境を立ち上げて、そこにMEAN環境を構築した

$ vagrant box add debian https://github.com/holms/vagrant-jessie-box/releases/download/Jessie-v0.1/Debian-jessie-amd64-netboot.box

導入したOSは以下のとおり、Debian 8.7である

$ cat /etc/debian_version
8.7
$ uname -a
Linux Debian-jessie-amd64-netboot 3.16.0-4-amd64 #1 SMP Debian 3.16.39-1 (2016-12-30) x86_64 GNU/Linux

MongoDB


MongoDBはNoSQL(表形式じゃないDBMS)のデファクトスタンダード。

MongoDBのインストール

$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
$ echo "deb http://repo.mongodb.org/apt/ubuntu "$(lsb_release -sc)"/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.0.list
$ sudo apt-get update
$ sudo apt-get install mongodb-org

適当にデータを入れておく

ここでは、mydbというデータベースを作成し、さらにUsersというコレクション(テーブルのようなもの)を作成し、そこに適当なオブジェクトを挿入する。

$ mongo
(省略)
> use mydb;
> db.users.insert({name: 'sasaki' , age: 24});
WriteResult({ "nInserted" : 1 })
> db.users.find({});
{ "_id" : ObjectId("5881d7ed4393e91ab4ebd18d"), "name" : "sasaki", "age" : 24 }

バイナリをダウンロード

何故か上記のやり方でバイナリが落ちなかったので別途入れる

$ curl -O https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.2.9.tgz
$ tar -zxvf mongodb-linux-x86_64-3.2.9.tgz
$ mv mongodb-linux-x86_64-3.2.9/bin/* /usr/local/bin

確認

$ mongo --version
MongoDB shell version: 3.2.9

NodeJS

NodeJSはサーバサイドでのJavaScriptの実行環境。ノンブロッキングIOを用いて大量アクセスを華麗に裁く

nodejsのインストール

aptを用いてインストールできるnodeは非常に古い。
とりあえず古いバージョンをインストールし、バージョンアップする手段を用いる。

$ sudo apt install nodejs
$ nodejs -v
v0.10.25

npmのインストール

npmはnodeで動くパッケージの管理ツール。必須。

$ sudo apt install npm
$ npm -v
4.1.2

nのインストール

nはnode本体のバージョンを管理するツール。npmを用いてインストールする

$ sudo npm -g install n
$ n --version
2.1.3

nodeを最新版に引き上げる

$ sudo n stable
$ node -v
v7.4.0

Express


expressはnode上で動作するWebアプリケーションフレームワーク。これもnpmを用いてインストールする

expressのインストール

$ npm install express -g
$ express --version
4.14.0

AngularJS


AngularJSはフロントエンドのMVCフレームワーク。こちらはCDNを用いるので、別途インストールの作業はない。

Hello,World


必要なnodeモジュールを追加でインストールする

ejsはテンプレートエンジン。mongodbはnodeからmongodbにアクセスするモジュール。

$ npm install ejs
$ npm install mongodb

nodeでmongodbに接続するためのスクリプトを作成

ここでは27017番ポートでmongodbが立ち上がっているものとし、データベース名がmydbであるとする。

var db;
var MongoClient = require('mongodb').MongoClient;
var assert = require('assert');
var url = 'mongodb://0.0.0.0:27017/mydb';

MongoClient.connect(url, function(err, mongodb) {
  assert.equal(null, err);
  console.log("Connected correctly to server");
  db = mongodb;
});

var collection = function(name) {
  return db.collection(name);
}

module.exports = collection;

メインスクリプトを作成

作業ディレクトリにapp.jsを作成する。この例では、8086番ポートでサーバを立ち上げている。

// express準備
var express = require('express');
var ObjectID = require('mongodb').ObjectID;
var collection = require('./mongo');
var COL = 'users';
var app = express();

// テンプレートディレクトリのパスを指定
app.set('views', __dirname + '/views');

// テンプレートエンジンをejsに指定
app.set('view engine', 'ejs');

// ルーティング設定
app.get('/', function (req, res) {
  collection(COL).find({}).toArray(function(err , docs) {
    res.render('index' , {name: docs[0].name});
  });
  //res.render('index');
});

// サーバー起動
var server = app.listen(8086, function () {
  var host = server.address().address;
  var port = server.address().port;
  console.log('Example app listening at http://%s:%s', host, port);
});

トップページを作成

views/index.ejsを作成する。この例では、AngularJS1.6.1をCDNから引っ張ってきている。
ややこしいが、<%= hoge %>が、ejsによるサーバサイドでの値出力。{{hoge}}が、AngularJSによるクライアントサイドでの値出力となる。

<!doctype html>
<html ng-app>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>
  </head>
  <body>
    <div>
      <label>Name:</label>
      <input type="text" ng-model="yourName" placeholder="Enter a name here">
      <hr>
      <h1>Hello {{yourName}}! My name is <%= name %>!</h1>
    </div>
  </body>
</html>

サーバを起動

$ node app.js
Example app listening at http://:::8086

ブラウザから8086番ポートにアクセスし、以下の画面が表示されれば成功

クライアントからの要求は、NodeJSがリクエストを初めに受け取る。NodeJS上で動作するexpressは、URLを元にルーティングし、MongoDBからデータを取得してejsテンプレートに引き渡している。ejsがテンプレートに値を埋め込むと、それをHTMLとしてクライアントに返却。クライアントはHTMLに含まれているAngularJSをダウンロードし、Angularアプリを描画する。そんな流れ。

AngularJS使ってみた

AngularJSとは


  • フロントエンドのMVC/MVVMフレームワーク
  • モデルとビューのデータ双方向バインディングが特徴
  • SPA開発に強い
  • カスタムタグ属性を用いることで、通常のHTMLに直接記述できる

AngularJSとAngular2の違い


Angular2はAngularJSの後継バージョンだが、AngularJSとは互換性を一切持たない。アーキテクチャが異なるため、今後も互換性は導入されないと言われている。

今回は、主に以下の理由からAngular2ではなくAngularJSを用いることにした。

  • Angular2は2016年9月にリリースされたばかりで、情報が比較的少ない
  • Angular2はTypeScriptでの利用を推奨している(JavaScriptでも書けるには書ける)

AngularJSによるサンプルコード


以降のサンプルコードは、コード全体のうち重要な部分のみ抜粋する。そのため、サンプルコードのみでは動作しないのがほとんどなので注意。AngularJSでこんなことができるよ、程度の参考にすること。

<!doctype html>
<html ng-app="myapp">
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>
  </head>
  <body>
    <div>
      <input type="text" ng-model="inputText">
      <h1>Hello {{inputText}}!</h1>
    </div>
  </body>
</html>

AngularJSでは、特別なテンプレートファイルなどを用いること無く、通常のHTMLファイルを拡張する形で利用することができる。

HTMLタグに対して、AngularJS特有の情報を持たせるには、”ng-model”など、ng-XXXで表記される拡張タグを用いる。

また、モデルのデータは {{param}} の形式で、HTMLに直接埋め込むことができる。

モデルとビューの双方向データバインディング機能


AngularJSの大きな機能である「双方向データバインディング」とは、名前の通り、モデルが保持しているデータと、それを表示しているビューを全自動で同期してくれる機能だ。

前項のサンプルコードを例にすると、inputタグに、ng-model=”inputText”と指定することで、このテキストボックスの内容は”inputText”というモデルにバインディングさせる。

それによって、テキストボックスを用いて入力内容を変更すると、自動でinputTextという名前のモデルも更新される。そのため、{{inputText}}で出力している内容もリアルタイムに更新される。

従来はjQueryなどを用いて、テキストボックスの変更を検知したらイベントで出力内容を変更すると言った処理を行っていたが、それが不要になり、かつ同期漏れなどのプログラマのミスを無くすことができる。

モデルの変更監視


モデルの変更は当然モデルでも検知することができる。
以下のコードは、taskList.tasks(array)の内容が変更された場合に、内容を保存している

$scope.$watch('taskList.tasks' , function() {
  taskList.saveTasks();
} , true);

ルーティング

ここでのルーティングは、SPAを前提とした、非同期通信をベースとした画面遷移のことを指すが、
もちろんWAF別途導入した非SPAのアプリケーションを開発することも可能。

AngularJSのルーティングは、以下の特徴を持つ

  • URLによるルーティング
  • SPAだが、戻る・進む・更新に対応
  • 自動的に非同期でページを返却する

ルーティングのサンプルコード(の一部)

JS

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

app.config(function($routeProvider) {
  $routeProvider
  .when('/' , {
    templateUrl: 'views/top.html'
  })
  .when('/taskList' , {
    templateUrl: 'views/taskList.html',
    controller: 'taskListController as taskList',
  })
  .otherwise({
    redirectTo: '/'
  });

HTML

<nav>
<div><a href="#/">トップ</a></div>
<div><a href="#/taskList">タスク一覧</a></div>
<div><a href="#/bookmark">ブックマーク一覧</a></div>
</nav>

リンククリックから、画面遷移までの流れはおそらく以下の通り。
以下は全てajaxを用いて行われるが、プログラマはそれを意識する必要がない。

  • $routeProviderを参照し、該当のルーティングを取得
  • 該当するコントローラオブジェクトを生成
  • 該当するテンプレートとバインディング
  • バインディングされたコントローラとテンプレートを返却
  • ブラウザが直前のテンプレート・コントローラを破棄し、受け取ったテンプレートを描画

また、コードを見ての通り、AngularJSでは、テンプレートとコントローラをそれぞれ指定することができる。それによって、同じテンプレートに対して異なるコントローラや、異なるテンプレートに同じコントローラを割り当てるなど、柔軟に実装することができる。

AngularJS所感


1日ちょっとだが、AngularJSを使って簡易アプリを作ってみての感想は以下の通り

  • 固定の設計パターンが多く、学習コストが高い
  • Angularの設計思想に基いて正しく扱えれば、jQueryは不要になると想う
  • ビューとモデルの双方向バインディングが強力。同期漏れによるバグが起こらないのは強い
  • しかし、理解して使わないと同期が高頻度で行われすぎてパフォーマンスが低下するかも?
  • バージョンによる記述方法の差異が大きい
  • 非同期通信はjQueryより遥かに直感的
  • AngularJS用の単体テストモジュールがあるらしく、テストが容易

総評すると、学習コストが高いが、チーム全体で設計パターンを理解していれば、非常に保守性の優れたコードが書けると思った。コードのバグも生まれにくくなると思うが、慣れるまではAngularJSの使い方の問題で実装に苦労するかも。

MEANスタック開発(MongoDB,Express,Angular,NodeJS)が流行ってきており、勢いだけならLAMPよりありそうなので、本格的に勉強してみる価値はある。

おまけ


今回のAngularJSお試しで開発した簡易アプリはこちらからダウンロードできる。
Webサーバを使わずにブラウザのみで動作するので、ダウンロードすることでAngularJSを試すことができる。

また、本記事ではかなり駆け足な紹介だったので、ソースコードを参照すればより具体的な使い方がわかるはず。

WordPressでFormを用いてPOSTすると404になる問題

1時間ほど悩まされたので備忘録を残します。

基本的にプラグインを導入すれば目的を果たすことができるWordPressだが、やむを得ずFormを自前で用意してデータをPOSTしたい場合がある。

その際に、以下のようなよくあるFormを用いたところ、Submit後に404が返却される問題があった。




<form action="<?php the_permalink();?>" method="post">
  <input id="userid" type="text" name="userid" />
  <input id="name" type="text" name="name" />
  <input id="address" type="text" name="address"/>
  <button type="submit">お申込み</button>
</form>



原因を調べてみると、上記コードの以下の部分に問題があったようだ

<input id="name" type="text" name="name" />

どうやら、WordPressでは画面の遷移先の決定に$_POST[‘name’]を用いているらしく、自前のFormから送信されたnameと衝突してしまい、正しい画面遷移が行えなくなってしまったらしい。

そのため、今回の場合は以下のように修正することで正常にPOSTできることを確認した

<input id="name" type="text" name="username" />

参考