カテゴリ: Javascript - ページ 2

HTML5 History API を徹底的に試してみる

History APIとは

History APIはHTML5の機能のひとつで、ブラウザの戻る進むボタンのイベントを取得してページの内容を動的に変えることができるものです。なかなか便利な機能ではあったのですが、ちょっとつまずきポイントが多く、癖もかなりあるっぽいので徹底的に試してみようと思います。

スタック

ブラウザの履歴の一つ一つの記録をスタックといい、一つ履歴が増える度にスタックが増える。そしてHistoryAPIを使うとこのスタックをページ推移を行わなくても増やすことができます。 HistoryAPIによってスタックが増えた場合は、ブラウザの戻るボタンを押してもページ遷移が発生せず、何も起きない。それがHistory API。

スタックの追加

スタックを追加するには、このようにやります。
history.pushState("hoge", null, "/hoge");Code language: JavaScript (javascript)
これで、履歴が一つ分増えます。これを実行すると、ブラウザの戻るボタンを一度押しても何も起きません。(ページ推移が発生しません) しかし、二回ボタンを押すといつもどおりページ推移が発生します。 しかし、このhistory.pushStateを使う前に必ず
history.replaceState("index");Code language: JavaScript (javascript)
とします。 これをすることでGoogle Developer Tools もしくは Firebug上で
history
と入力し、エンターを押すと、historyオブジェクトが見えるので、historyオブジェクトを展開し、stateプロパティを確認すると、そこがindexになっていることが確認できます。 必ずこれをやっておかないとめんどくさいことになります。

ブラウザの戻る・進むイベントを監視する

jQueryを使っていますが、jQueryの場合はこのように書くことでイベントが実装できます。
$(window).on("popstate", function(_event){
    var state = _event.originalEvent.state;
    console.log("_event", _event);
    console.log("state", state);
});Code language: JavaScript (javascript)
これをソースコード内に記述して、
history.pushState("hoge", null, "/hoge");Code language: JavaScript (javascript)
を実行して、ブラウザの戻るボタンをクリックします。 Google Developper Tools または Firebugのコンソールで確認すると、 state がindexになっていることがわかります。 これがハマりポイントでした。最初に history.replaceState(“index”); をしたことで、index と表示されたわけですが、これをやらないとここが、 null になってしまいます。 最初このstateの値は hoge になるものだと思っていたのですが、実はそうではなく、戻るページに設定されているstateらしいです。 構造的には「index -> hoge」 という構造になっています。 このあたりがややこしいので注意が必要です。

Tips

History APIは基本的に以上の仕組みさえ理解すればそれだけですんなりと使えますが、他にも覚えておくと便利な点をあげておきます。

history.state

現在のstateを確認できます。indexなのかhogeなのか。

history.back()

これを実行するとブラウザを戻ります。ページ内に戻るボタンを設置する場合はこのメソッドを使用します。ただ、対応していないブラウザもあるので機能を切り分ける必要があります。

history.forward()

これを実行するとブラウザを進みます

history.go(num)

指定した履歴へジャンプします。

history.length

現在の履歴の数を表示します

location.pathname

現在のドメインを覗いたディレクトリパスを取得します

location.search

現在のURLの?の後ろのパラメータを取得します

Javascriptテストフレームワーク Jasmineを試す

日頃からJavascriptで開発をしているのにも関わらずあまりテストを書かないので、ここは本格的にテストを書こうと調べてみました。JavascriptのテストフレームワークといったらJsUnitなのかなーと思っていたが、調べてみると結構いろんな種類のテストフレームワークがあったりして、その中で得に人気なのかどうやらJasmineらしい。
Jasmine ~ JavaScript Test フレームワーク より引用: 今回は, JavaScript のテストを行うためのフレームワークJasmine の紹介です。 JavaScript のテストといえば, JSUnit が有名です。 JSUnit は, JUnit とに似たような, Matcher が利用できたりしてわかりやすいのですが, 開発やメンテナンスがストップしており, またWebプロジェクトに組み込まないと利用できないことが ちょっと残念です。 JUnit のページでも紹介があるように, 今後は Jasmine というフレームワークを開発していくようです。
なるほど、JsUnitはかつて人気だったテストフレームワークだったけども開発ストップしてて古くなっており、今はJasmineということなのか。

とりあえずやってみた

しかし、初めて触るライブラリというのはどうにもこうにも敷居が高い。少しずつ調べてみる。なんにせよまずはインストールだ。 公式ページ: http://pivotal.github.com/jasmine/ 公式ページ(の翻訳): http://mitsuruog.github.com/jasmine/ ダウンロード: https://github.com/pivotal/jasmine/downloads ダウンロードしたファイルは、jasmine-standalone-1.3.1.zip です。

二種類の使い方

Jasmineは使い方が二種類あるそうです。
  • standalone
  • rubygems + rake
standaloneはRubyを使わずにJavascriptだけで使える環境、 rubygems + rake はテストを自動化させたい場合はこちらを選ぶ。ただ、standaloneでもPhantomJSを使えば自動化できるようです。

ここではstandaloneについて実験

rubygemsを使った方法だと、テストを実行させるためのHTMLを自動生成してくれて大変便利らしいのですが、ここでは導入としてstandaloneを使ったやりかたについて試してみたいとおもいます。 ディレクトリの構造はこのようになっています。
.
├── SpecRunner.html
├── lib
│   └── jasmine-1.3.1
│       ├── MIT.LICENSE
│       ├── jasmine-html.js
│       ├── jasmine.css
│       └── jasmine.js
├── spec
│   ├── PlayerSpec.js
│   └── SpecHelper.js
└── src
    ├── Player.js
    └── Song.jsCode language: CSS (css)
これをWebサーバーにあげて、SpecRunner.html を開いてみます。 スクリーンショット 2013-01-03 3.24.32 なるほど、わからん。 見た感じ、上の5つの丸がテストの成功か失敗かを表し、下段の黒字がテストスイート(カテゴリ分け)、緑の文字がテストケースという具合のようです。

Specの意味

「仕様」 = テストケース

Suiteの意味

「一式(分類)」 = テストスイート

SpecRunner.htmlの中身を見る

SpecRunner.htmlの中身を見てみます。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
  "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <title>Jasmine Spec Runner</title>

  <link rel="shortcut icon" type="image/png" href="lib/jasmine-1.3.1/jasmine_favicon.png">
  <link rel="stylesheet" type="text/css" href="lib/jasmine-1.3.1/jasmine.css">
  <script type="text/javascript" src="lib/jasmine-1.3.1/jasmine.js"></script>
  <script type="text/javascript" src="lib/jasmine-1.3.1/jasmine-html.js"></script>

  <!-- include source files here... -->
  <script type="text/javascript" src="src/Player.js"></script>
  <script type="text/javascript" src="src/Song.js"></script>

  <!-- include spec files here... -->
  <script type="text/javascript" src="spec/SpecHelper.js"></script>
  <script type="text/javascript" src="spec/PlayerSpec.js"></script>

  <script type="text/javascript">
    (function() {
      var jasmineEnv = jasmine.getEnv();
      jasmineEnv.updateInterval = 1000;

      var htmlReporter = new jasmine.HtmlReporter();

      jasmineEnv.addReporter(htmlReporter);

      jasmineEnv.specFilter = function(spec) {
        return htmlReporter.specFilter(spec);
      };

      var currentWindowOnload = window.onload;

      window.onload = function() {
        if (currentWindowOnload) {
          currentWindowOnload();
        }
        execJasmine();
      };

      function execJasmine() {
        jasmineEnv.execute();
      }

    })();
  </script>

</head>

<body>
</body>
</html>Code language: HTML, XML (xml)

よくわからなかったのでコメントを振ってみる

よくわからなかったのでコメントを振ってみます。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
  "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <title>Jasmine Spec Runner</title>

  <!-- ショートカットアイコン -->
  <link rel="shortcut icon" type="image/png" href="lib/jasmine-1.3.1/jasmine_favicon.png">

  <!-- スタイルシート -->
  <link rel="stylesheet" type="text/css" href="lib/jasmine-1.3.1/jasmine.css">

  <!-- Jasmineの読み込み -->
  <script type="text/javascript" src="lib/jasmine-1.3.1/jasmine.js"></script>

  <!-- Jasmineの読み込み -->
  <script type="text/javascript" src="lib/jasmine-1.3.1/jasmine-html.js"></script>

  <!-- テストを行う対象ファイルを読み込み -->
  <script type="text/javascript" src="src/Player.js"></script>
  <script type="text/javascript" src="src/Song.js"></script>

  <!-- 独自定義のマッチャを読み込み -->
  <script type="text/javascript" src="spec/SpecHelper.js"></script>

  <!-- テストケースファイルを読み込み -->
  <script type="text/javascript" src="spec/PlayerSpec.js"></script>

  <script type="text/javascript">
    (function() {

      // Jasmineの環境設定オブジェクトを読み込み
      var jasmineEnv = jasmine.getEnv();

      // アップデートの間隔を1秒に設定?
      jasmineEnv.updateInterval = 1000;

      // テスト結果を取得するためのオブジェクトを取得
      var htmlReporter = new jasmine.HtmlReporter();

      // 不明・・・
      jasmineEnv.addReporter(htmlReporter);

      // 個別のテストスイートやテストケースをクリックすることで、テストスイートの一部分のみを実行して結果を取得する
      jasmineEnv.specFilter = function(spec) {
        return htmlReporter.specFilter(spec);
      };

      // ウィンドウをリロードするオブジェクト生成
      var currentWindowOnload = window.onload;

      window.onload = function() {
        if (currentWindowOnload) {
          currentWindowOnload();
        }

        // テスト実行
        execJasmine();
      };

      // テスト実行関数の定義
      function execJasmine() {
        jasmineEnv.execute();
      }

    })();
  </script>

</head>

<body>
</body>
</html>Code language: HTML, XML (xml)
このファイルは、テストを行いたいJavascriptを読み込み、それに対してテストを定義し実行するというもののようです。しかし、「jasmineEnv.updateInterval = 1000;」という行が気になった。これはもしかして1秒ごとにテストを実行するということなのだろうか。もしそうなら、このHTMLファイルを開きっぱなしで常にテスト結果を確認できるということなのでとても嬉しいのだけれど・・。

バグらせてみる

まだなんだか感覚がつかめないので、テスト対象のファイルをバグらせてみることにします。 Player.jsをバグらせる。
function Player() {
}
Player.prototype.play = function(song) {
  this.currentlyPlayingSong = song;
  this.isPlaying = true;
};

Player.prototype.pause = function() {
  this.isPlaying = false;
};

Player.prototype.resume = function() {
  if (this.isPlaying) {
    throw new Error("song is already playing");
  }

  this.isPlaying = true;
};

Player.prototype.makeFavorite = function() {
  this.currentlyPlayingSong.persistFavoriteStatus(true);
};Code language: JavaScript (javascript)
中身はとてもシンプルで、本当にプレイヤー機能が入っているのかと思えばそんなことはなくて、模倣したオブジェクトのようです。 コメントを振ってみます。
function Player() {
}

// 再生する
Player.prototype.play = function(song) {
  this.currentlyPlayingSong = song;
  this.isPlaying = true;
};

// ポーズする
Player.prototype.pause = function() {
  this.isPlaying = false;
};

// 途中から再生する
Player.prototype.resume = function() {
  if (this.isPlaying) {
    throw new Error("song is already playing");
  }

  this.isPlaying = true;
};

// お気に入りに登録
Player.prototype.makeFavorite = function() {
  this.currentlyPlayingSong.persistFavoriteStatus(true);
};Code language: JavaScript (javascript)
お気に入りに登録のコメントが合っているのか不安ですが、これの途中から再生するをバグらせてみます。
...

// 途中から再生する
Player.prototype.resume = function() {
  if (this.isPlaying) {
    throw new Error("song is already ");
  }

  this.isPlaying = true;
};

...Code language: JavaScript (javascript)
「song is already playing」からplaying を消してみました。これでいったいなにが起きるか・・・。 先ほどの SpecRunner.html を開いて確認してみます。

バグがある場合のテスト結果

スクリーンショット 2013-01-03 4.10.31 おお! なんか出ました!エラーがでました!これはエラー詳細ページで、全体のテストケース一覧を見るには「5 specs」をクリックします。 スクリーンショット 2013-01-03 4.11.31 エラーがあるテストケースは赤字になっています。 また、自動的にリロードしてくれるのか期待していたのですが、自動リロード機能はないようなので、ブラウザの自動リロード機能を使って常に監視しておくといいです。

テストのほうを弄る

テストケースが定義されているファイルをいじってみます。弄るファイルはspecフォルダにある「PlayerSpec.js」です。 中身はこんなかんじ
describe("Player", function() {
  var player;
  var song;

  beforeEach(function() {
    player = new Player();
    song = new Song();
  });

  it("should be able to play a Song", function() {
    player.play(song);
    expect(player.currentlyPlayingSong).toEqual(song);

    //demonstrates use of custom matcher
    expect(player).toBePlaying(song);
  });

  describe("when song has been paused", function() {
    beforeEach(function() {
      player.play(song);
      player.pause();
    });

    it("should indicate that the song is currently paused", function() {
      expect(player.isPlaying).toBeFalsy();

      // demonstrates use of 'not' with a custom matcher
      expect(player).not.toBePlaying(song);
    });

    it("should be possible to resume", function() {
      player.resume();
      expect(player.isPlaying).toBeTruthy();
      expect(player.currentlyPlayingSong).toEqual(song);
    });
  });

  // demonstrates use of spies to intercept and test method calls
  it("tells the current song if the user has made it a favorite", function() {
    spyOn(song, 'persistFavoriteStatus');

    player.play(song);
    player.makeFavorite();

    expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
  });

  //demonstrates use of expected exceptions
  describe("#resume", function() {
    it("should throw an exception if song is already playing", function() {
      player.play(song);

      expect(function() {
        player.resume();
      }).toThrow("song is already playing");
    });
  });
});Code language: JavaScript (javascript)
うおお..よくわからない… RSpecというruby界隈の方にはお馴染みの書き方のようです。そしてこの書き方がテストをやる上ですごいやりやすいんだとか。 また同じようにコメントを振ってみます。
// テストスイート定義
describe("プレイヤー", function() {

  // テストを行うJavascriptのオブジェクトを読みこませる変数の宣言
  var player;
  var song;

  // テストを開始する準備を行う 
  beforeEach(function() {
    // テスト対象となるオブジェクトの読み込み
    player = new Player();
    song = new Song();
  });

  // テストケースを定義 it("テストケース名", 無名関数)
  it("曲を再生することができる", function() {

    // 音楽を再生
    player.play(song);

    // player.currentlyPlayingSong が song と同値であることを期待する
    expect(player.currentlyPlayingSong).toEqual(song);

    // プレイヤーが再生中であることを期待する -> SpecHelper.js に定義されています
    expect(player).toBePlaying(song);
  });

  // テストスイート定義(階層になっていて見やすい)
  describe("曲が一時停止されたときの挙動", function() {

    // テストを開始する準備を行う
    beforeEach(function() {

      // プレイヤーを再生
      player.play(song);

      // プレイヤーをポーズ
      player.pause();
    });

    // テストケース定義
    it("曲が一時停止しているか確認する", function() {

      // player.isPlaying が false になっている
      expect(player.isPlaying).toBeFalsy();

      // プレイヤーが再生されているマッチャに対しnotメソッドによって評価が逆になり、プレイヤーは音楽を再生していない となる
      expect(player).not.toBePlaying(song);
    });

    it("途中から再生可能かどうか", function() {

      // プレイヤーをレジュームする
      player.resume();

      // player.isPlaying は true になっている
      expect(player.isPlaying).toBeTruthy();

      // player.currentlyPlayingSong は song と 同じ値であることを期待する
      expect(player.currentlyPlayingSong).toEqual(song);
    });
  });

  // テストケース定義
  it("ユーザーがお気に入りにしていた場合、その曲を教える", function() {

    // オブジェクトのメソッドの呼び出しをスパイを使って監視
    spyOn(song, 'persistFavoriteStatus');

    // プレイヤーを再生
    player.play(song);

    // プレイヤーがお気に入りに設定(内部で this.currentlyPlayingSong.persistFavoriteStatus(true); が実行されます )
    player.makeFavorite();

    // song.persistFavoriteStatusメソッド は 引数に true を伴って実行されたか
    expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
  });

  // テストスイート定義
  describe("#レジューム", function() {
    it("曲が既に再生されている場合に例外をスルーする", function() {

      // プレイヤーを再生
      player.play(song);

      // プレイヤーをレジュームしてその例外が同じものであることを期待する -> player.resume(); では throw new Error("song is already"); として例外が送出されている。
      expect(function() {

        // レジューム
        player.resume();

      }).toThrow("song is already playing");
    });
  });
});Code language: JavaScript (javascript)
頭を使いすぎた・・・。だいたいこんなかんじで合っているとおもいます。 流れはコメントの通りで、expect の期待が外れると、そのテストケースは「失敗」となるようです。describeによるテストスイートのカテゴリ分けや階層化もなかなかおもしろいと思いました。 そして、とくに気になった点を2つあげると、独自定義マッチャと、スパイ機能です。これについては次に書きます。そしてその前に、マッチャってなんだ・・・・・。

マッチャ

マッチャとは、「AはBであることを期待する」というように正しいかどうかを評価するためのものです。 マッチャはいくつかの種類があります。
expect(x).toEqual(y); xとyが等しいことを期待する
expect(x).toBe(y); xとyが同じオブジェクトであることを期待する
expect(x).toMatch(pattern); 文字列または正規表現パターンでxと比較し、一致することを期待する
expect(x).toBeDefined(); xがundefinedではない場合ことを期待する
expect(x).toBeUndefined(); xがundefinedであることを期待する
expect(x).toBeNull(); xがnullであることを期待する
expect(x).toBeTruthy(); xがtrueであることを期待する
expect(x).toBeFalsy(); xがfalseであることを期待する
expect(x).toContain(y); 配列化か文字列であるxに対して、yが含まれていることを期待する
expect(x).toBeLessThan(y); xがy未満であることを期待する
expect(x).toBeGreaterThan(y); xがyよりも大きいことを期待する
expect(function(){fn();}).toThrow(e); 無名関数が実行された時に関数fnが例外eを投げることを期待する
.not.(matcher)(matcher)に他のマッチャを指定することでそのマッチャを逆に評価します(trueをfalseに falseをtrueに)

独自マッチャの定義

マッチャは予め定められたものだけではなく、「SpecHelper.js」によって独自に定義することもできます。
// テストを実行する準備をする
beforeEach(function() {

  // マッチャの追加
  this.addMatchers({

    //toBePlayingを追加
    toBePlaying: function(expectedSong) {

      // this.actual は expect(player).toBePlaying(song); の player の部分
      var player = this.actual;

      // 評価
      return player.currentlyPlayingSong === expectedSong && 
             player.isPlaying;
    }
  });
});
Code language: JavaScript (javascript)

スパイ

次はスパイを見ていきます。
  // テストケース定義
  it("ユーザーがお気に入りにしていた場合、その曲を教える", function() {

    // オブジェクトのメソッドの呼び出しをスパイを使って監視
    spyOn(song, 'persistFavoriteStatus');

    // プレイヤーを再生
    player.play(song);

    // プレイヤーがお気に入りに設定(内部で this.currentlyPlayingSong.persistFavoriteStatus(true); が実行されます )
    player.makeFavorite();

    // song.persistFavoriteStatusメソッド は 引数に true を伴って実行されたか
    expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
  });Code language: JavaScript (javascript)
スパイのテストケースはこうなっています。「spyOn(song, ‘persistFavoriteStatus’);」で song オブジェクトの persistFavoriteStatusメソッドを監視します。 songのpersistFavoriteStatusメソッドはこのようになっています。
function Song() {
}

Song.prototype.persistFavoriteStatus = function(value) {
  // something complicated
  throw new Error("not yet implemented");
};Code language: JavaScript (javascript)
「player.play(song);」でプレイヤーを再生するときには特に内部でスパイは活動を行いませんが、「player.makeFavorite();」で活動を行います。 makeFavoriteメソッドは内部ではこのようになっており、「this.currentlyPlayingSong」には 「player.play(song);」内部で定義された songオブジェクトが入っているため、「persistFavoriteStatus」が実行できます。
// お気に入りに登録
Player.prototype.makeFavorite = function() {
  this.currentlyPlayingSong.persistFavoriteStatus(true);
};Code language: JavaScript (javascript)
このときに、スパイが活動を始め、このメソッドが実行されたことをしっかりと記憶します。 そして、
// song.persistFavoriteStatusメソッド は 引数に true を伴って実行されたか
    expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);Code language: JavaScript (javascript)
スパイの活動を見るために、toHaveBeenCalledWithメソッドがスパイから songオブジェクトがpersistFavoriteStatusメソッドを実行したときに引数にtrueを使用したかを聞き出します。 true が引数に使われたことがわかったので、これで期待通りの結果になっているということがわかります。

スパイのためのマッチャ

このように toHaveBeenCalledWith のようなスパイのためのマッチャがいくつか用意されています。
expect(x).toHaveBeenCalled()xメソッドがスパイ中に呼び出されていたメソッドであることを期待
expect(x).toHaveBeenCalledWith(arguments)xメソッドがスパイ中に呼び出された時にそのメソッドに使用していた引数がargumentsであることを期待
expect(x).not.toHaveBeenCalled()xメソッドがスパイ中に呼び出されなかった
expect(x).not.toHaveBeenCalledWith(arguments)xメソッドがスパイ中に呼び出された時にそのメソッドに使用していた引数がargumentsでないことを期待

さまざまなスパイの呼び出し方法

スパイが監視しているメソッドの実行を検知したときに細かい挙動を指定することができます。
spyOn(x, ‘method’).andCallThrough();デフォルト機能。スパイ活動を開始します。
spyOn(x, ‘method’).andReturn(arguments);スパイが呼び出されたときに決められた引数であるargumentsを返します。
spyOn(x, ‘method’).andThrow(exception);スパイが呼び出された時に渡された例外をスルーします。
spyOn(x, ‘method’).andCallFake(function);スパイが呼び出された時に指定された関数へ実行を移譲します。

最後に

とても使いやすくて、ひとつのHTMLページに詰め込むこともできるし、デバッグモードをONにした場合にのみ表示なんてことも不可能ではないので、いろんな応用が効くテストフレームワークだと思いました。こんなに柔軟性のあるテストフレームワークだということに今更ながら驚いています。 スクリーンショット 2013-01-03 5.57.54

jQuery UI も Twitter Bootstrapもテーマカスタマイズができた

jQuery UI や Twitter Bootstrapのテーマを自由に変える

jQuery UI やTwitter Bootstrapのスタイルはcssによって描画されていますが、そのCSS(と画像ファイル)を自由にカスタマイズできるページがあったみたいです。

いやはやもう少し早く気づいておくべきでした・・。

jQuery UI

20120802040246

jQuery UI ThemeRoller
http://jqueryui.com/themeroller/

左ペインのメニューからいろいろ設定を変更してテーマを変えていきます。ギャラリーにはテーマ一覧もあったりして便利。設定完了したら「Download theme」ボタンをクリック。

Twitter Bootstrap

20120802040347

StyleBootstrap.info

http://stylebootstrap.info/

ここも同じようにカスタマイズした後最後にテーマをダウンロードできる。ダウンロードボタンがページ一番下にあるのでわかりにくいです。