19日目: AJAX

昨日は、Jobeet用の検索エンジンを実装しました。

今日は、検索エンジンのレスポンスを強化するために、検索エンジンをライブ検索エンジンに変換するAJAXを利用します。

JavaScriptの有無に関わらずフォームは動作するので、ライブ検索機能は控えめな JavaScriptを利用して実装します。控えめなJavaScriptを利用することでHTML、CSSとJavaScriptのふるまいの間のコードの関心の分離も可能になります。

jQueryをインストールする

車輪の再発明とブラウザーの間の多くの違いを管理するのを避けて、JavaScriptライブラリのjQueryを使います。 Ark フレームワーク自身は任意のJavaScriptライブラリで動作します。

jQueryの公式サイトに移動し、最新バージョンをダウンロードし、.jsファイルをroot/js/に設置します。

jQueryをインクルードする

すべてのページでjQueryが必要なので、の前でこれをインクルードするためにレイアウトを更新します。

? block javascripts => sub {
    <script type="text/javascript" src="<?= $c->uri_for('/js/jquery-1.3.2.min.js') ?>"></script>
? } # endblock javascripts

ふるまい(JavaScript)を追加する

ライブ検索を実装することは、検索ボックスでユーザーが文字を入力するたびに、サーバーの呼び出しが必要であるを意味します; サーバーはページ全体をリフレッシュせずページの一部を更新するために必要な情報を返します。

jQueryの背景にある主要な原則はon*()HTML属性でふるまいを追加する代わりに、ページが完全にロードされた後でDOMにふるまいを追加することです。この方法によって、ブラウザーでJavaScriptのサポートを無効にする場合、ふるまいは何も登録されず、フォームは以前のとおりに動作します。

最初のステップは検索ボックスでユーザーがキーを入力するときにこれを傍受することです:

$('#search_keywords').keyup(function(key)
{
  if (this.value.length >= 3 || this.value == '')
  {
    // 何かを行う
  }
});

ユーザーがキーを入力するたびに、jQueryは上記のコードで定義される匿名関数を定義しますが、ユーザーが3文字以上を入力した場合、もしくは inputタグからすべてを削除した場合のみです。

サーバーでAJAX呼び出しを行うにはDOM要素でload()メソッドを使うだけなのでシンプルです:

$('#search_keywords').keyup(function(key)
{
  if (this.value.length >= 3 || this.value == '')
  {
    $('#jobs').load(
      $(this).parents('form').attr('action'), { query: this.value }
    );
  }
});

AJAX呼び出しを管理するために、"普通"のものとして同じアクションが呼び出されます。アクションの必要な変更は次のセクションで行われます。

最後に大事なことですが、JavaScriptが有効な場合、検索ボタンを削除したい場合は次のとおりです:

$('.search input[type="submit"]').hide();

ユーザーのフィードバック

AJAX呼び出しを行うとき、ページはすぐに更新されません。ブラウザーはページを更新する前に戻ってくるサーバーのHTTPレスポンスを待ちます。一方で、何が起きているのか知らせるためにユーザーに視覚的なフィードバックを提供する必要があります。

慣習としてAJAX呼び出しの間にローダーのアイコンが表示されます。ローダーの画像を追加してデフォルトでこれを隠すためにレイアウトを更新します:

        <div class="search">
          <h2>Ask for a job</h2>
          <form action="<?= $c->uri_for('/search') ?>" method="get">
            <input type="text" name="q"
              id="search_keywords" />
            <input type="submit" value="search" />
            <img id="loader" src="<?= $c->uri_for('/images/loader.gif') ?>" style="vertical-align: middle; display: none" />
            <div class="help">
              Enter some keywords (city, country, position, ...)
            </div>
          </form>
        </div>

これでHTMLを動作させるために必要なすべてのピースが用意されたので、これまで書いてきたJavaScriptを含むsearch.jsファイルを作ります:

// root/js/search.js
$(document).ready(function()
{
  $('.search input[type="submit"]').hide();

  $('#search_keywords').keyup(function(key)
  {
    if (this.value.length >= 3 || this.value == '')
    {
      $('#loader').show();
      $('#jobs').load(
        $(this).parents('form').attr('action'),
        { q: this.value },
        function() { $('#loader').hide(); }
      );
    }
  });
});

この新しいファイルをインクルードするためにレイアウトも更新する必要があります:

? block javascripts => sub {
    <script type="text/javascript" src="<?= $c->uri_for('/js/jquery-1.3.2.min.js') ?>"></script>
    <script type="text/javascript" src="<?= $c->uri_for('/js/search.js') ?>"></script>
? } # endblock javascripts

アクションにおけるAJAX

JavaScriptが有効な場合、jQueryは検索ボックスに入力されたすべてのキーを傍受し、searchコントローラのindexアクションを呼び出します。そうではない場合、ユーザーがフォームを投稿するときに"enter"キーを押すもしくは"search"ボタンをクリックすることで同じsearchアクションも呼び出されます。ですので、searchアクションは呼び出しがAJAX経由か否かを決定する必要があります。 AJAX呼び出しによってHTTPリクエスト(AJAX)が行われるときは、リクエストヘッダー X-Requested-WithXMLHttpRequest が含まれるのでそれを用いて判別します:

sub index :Path {
    my ($self, $c) = @_;

    my $query = $c->req->param('q')
        or $c->detach('/default');

    $c->stash->{jobs} = models('Schema::Job')->search_fulltext($query);

    if ($c->req->header('X-Requested-With') =~ /XMLHttpRequest/i) {
        $c->view('MT')->template('search/ajax');
    }
}

jQueryはページをリロードしませんが、DOM要素の#jobsをレスポンスの内容に置き換えることだけを行うので、ページはレイアウトによってデコレートされません。これは共通のニーズなので、AJAXリクエストがやってくるときレイアウトはデフォルトで無効です。

さらに、完全なテンプレートを返す代わりに、job/partialjobs パーシャルの内容を返すことだけが必要です。そこでこのアクションでは AJAX 経由でのアクセスの場合のみ別のテンプレート search/ajax.mt を読み込むようにします。

search/ajax.mt は以下のようになります:

?= include('job/_partial_jobs', $c->stash->{jobs}->all );

ユーザーが検索ボックスのすべての文字を削除する場合、もしくは検索が結果を返さない場合、空白ページの代わりにメッセージを表示する必要があります。テンプレートを以下のようにします:

? my @jobs = $c->stash->{jobs}->all;

? if (@jobs) {
?= include('job/_partial_jobs', @jobs );
? } else {
No results.
? }

また明日

今日は、よりレスポンスを強化するためにjQueryを使いました。 Arkフレームワークは簡単にMVCアプリケーションを開発して他のコンポーネントと連携するためのすべての基本的なツールを提供します。いつものように、求人用のベストなツールを使うことを心がけてください。

明日は、JobeetのWebサイトを国際化する方法を見ます。