pjaxが便利過ぎて鼻血出そうになった(railsのサンプル付き)

最近pjaxという単語をよく見るから調べてみたら結構よさげだったのでrailsを使ったサンプルを記事にしてみました。

pjaxとAjaxの違い:Ajax=技術、pjax=jQueryプラグイン

Ajax
JavaScriptを用いてサーバから非同期に取得したデータを用いてコンテンツを改変する技術。
pushState
HTML5で登場した、ブラウザの履歴を変更する仕様兼その実装。history.pushStateで使える
pushState+Ajax
Ajaxでコンテンツを変えると同時にpushStateでブラウザの履歴を更新して、さらに更新後のURLに直接アクセスしても同じページが表示されるようにする。つまり、Ajaxで変更した後のページにもパーマリンクを作る。その結果ブラウザの戻るボタンで、同一ページ内のAjaxの効果を消したりもできるようになる。
pjax
pushState+Ajaxを実現するjQueryプラグイン。https://github.com/defunkt/jquery-pjax

なので、pjaxとAjaxはレイヤーが異なる単語のようですね。

pjaxを使ってみる

pjaxを使うと、本当に簡単にpushState+AjaxなWebサービスを作れます

クライアント側の実装
  1. jqueryjquery.pjaxをロードする
  2. $("a.js-pjax").pjax("#main");を実行する
    • class="js-pjax"なaタグをクリックすると、href属性の先を読み込んでid="main"の中身を置き換えるようにしてくれる
    • この時、hrefの値をpushStateして履歴をpjaxが進めてくれる
サーバ側の実装
  1. リクエストヘッダにX-PJAX: trueがある、もしくはGETで_pjax=trueというのが送られているか調べる
    • なかったら、html全体を返す
    • あったら、コンテンツだけを返す

追記:下記は間違ってました
POSTで_pjax=true

railsで使ってみたサンプル

上記の仕様にそってrailsアプリケーションを作ってみた。
railsではレイアウトビューの中にyieldメソッドを組み込んでコンテンツを埋め込むので、pjaxの時だけレイアウトをオフにしてやることで簡単に実装できる。
追記:8月末日現在のpjaxの最新版にはバグがあって動作しません。http://pjax.heroku.com/jquery.pjax.jsをダウンロードしてくると、バージョンは古いですが動作します。下記のサンプルではこのjsを使っています。

  • app/views/layouts/application.rhtml
<!DOCTYPE html>
<html lang="ja">
<head>
  <%= javascript_include_tag "jquery" %>
  <%= javascript_include_tag "jquery.pjax" %>
  <script>
    $(function() {
      $("a.js-pjax").pjax("#main");   // #main の中身をAjaxで切り替える
      alert("hoge");  // 説明のためにつけただけ
    });
  </script>
</head>
<body>
  <ul>
    <li><%= link_to("page A", { :action => "page_a" }, {:class => "js-pjax" }) %></li>
    <li><%= link_to("page B", { :action => "page_b" }, {:class => "js-pjax" }) %></li>
  </ul>
  <div id="main"><%= yield %></div>  <!-- #main の中に yield を入れるのがみそ -->
</body>
</html>
  • app/views/core/page_a.rhtml
<title>page_a</title>
page_a
  • app/views/core/page_b.rhtml
<title>page_b</title>
page_b
  • app/controllers/core_controller.rb
class CoreController < ApplicationController
  def page_a
    # pjaxだったらレイアウトを使わない。たったこれだけ
    render(:layout => false) if request.headers["X-PJAX"]
  end

  def page_b
    render(:layout => false) if request.headers["X-PJAX"]
  end
end

動かしてみる

f:id:yuku_t:20110823232830p:image

  • 最初に/core/page_aにアクセスした
  • レイアウトが有効なので読み込みが終わった時点でalert("hoge")が実行された

f:id:yuku_t:20110823232831p:image

  • 次にpage Bをクリックしてみた
  • タイトルとコンテンツとURLが変更されている
  • コンテンツだけを読み込んでいるので、alert("hoge")は実行されない

f:id:yuku_t:20110823232832p:image

  • 最後にpage Aをクリックした
  • 最初のページに戻る
  • コンテンツだけを読み込んでいるので、alert("hoge")は実行されない

この間のコンテンツの変更はAjaxで行われていますが、同時にpushStateしているので、ブラウザの戻るボタンを押せばpage_bのコンテンツが表示されます。

重要な点は/core/page_aに直接アクセスしても、page Aをクリックしても、同じURL同じ内容が表示されている、つまりAjaxで動的に生成したページにパーマネントリンクが作られているということ。
なので、twitterfacebookはてブやメールで共有できるということですね。

しかも、Ajaxを使って必要最低限のものしかやり取りしないので、高速に動作する

実装するのも非常に簡単なので、ユーザビリティー的にも、使わない手はないんじゃないでしょうか?