2015年6月5日金曜日

[pushState] jquery.pjaxなクライアントとExpress4+ECTなサーバサイドのWebアプリサンプル

jquery.pjaxとは、ブラウザ履歴をプログラムから操作できるHTML5のAPI「pushState」を便利に実装したjqueryプラグインです。ajaxを使ったWebアプリでは、画面遷移が発生しないのでサクサク動くというメリットがある反面、画面が変化してもURLが変わらないためにSEOで不利というのがデメリットでした。しかしHTML5のpushStateのお陰でそのデメリットもやっと解消されることになりました。jquery.pjaxを使えば、pushStateを簡単に使うことができます。
今回ご紹介するのは、jquery.pjax.js (クライアントサイド) と express4 (サーバサイド) を使ったpjaxなWebアプリの超簡単なサンプルです。

プロジェクト作成


まずはプロジェクトを雛形から作成しましょう。プロジェクト名は pjax-example としました。
$ express pjax-example && cd pjax-example

次に jquery と jquery.pjax.js (v1.9.6)をダウンロードして public/javascripts/ にコピーします。

今回はテンプレートエンジンに ECT を使用します。
$ sudo npm install --save ect

この時点で package.json は以下のようになります。pjax向けのモジュールは特に何も入れていません。

package.json
{
  "name": "pjax-example",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "body-parser": "~1.12.0",
    "cookie-parser": "~1.3.4",
    "debug": "~2.1.1",
    "ect": "^0.5.9",
    "express": "~4.12.2",
    "jade": "~1.9.2",
    "morgan": "~1.5.1",
    "serve-favicon": "~2.2.0"
  }
}

npm install しておきます。
$ sudo npm install

それではプロジェクトの準備が出来たのでコードを触って行きましょう。


views


画面を作成します。
まずはレイアウトから。

views/layout.ect
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title><%= @title %></title>
    <script type="text/javascript" src="/javascripts/jquery-2.1.0.min.js"></script>
    <script type="text/javascript" src="/javascripts/jquery.pjax.js"></script>
  </head>
  <body>
    Created at: <%= new Date().getTime(); %>
    <div>
      <h1 id="title"><%= @title %></h1>
    </div>
    <div id="content-wrapper"><% content %></div>
    <div id="menu">
      <h2>Menu</h2>
      <nav>
        <ul>
          <li><a href="/page1">page1</a></li>
          <li><a href="/page2">page2</a></li>
        </ul>
      </nav>
    </div>
    <script>
    $(function(){

      // calls after completion of a partial replacement
      $(document).on('pjax:end', function(e, xhr, options) {
        // rewrites this page's <title> & #title's text
        var title = $(e.target).find("div").data("title");
        $("title").text(title);
        $("#title").text(title);
      });
      
      $("a").on("click", function(e){
        e.preventDefault(); // stops default click stream
        var url = $(this).attr("href");
        $.pjax({
          url: url,
          container: "#content-wrapper",
          timeout: 5000
        });
      });

    });
    </script>
  </body>
</html>

NOTE:

$.pjax({...
ここはpjax リクエストしている部分です。ここは特に説明は必要ないでしょう。

$(document).on('pjax:end', ...
ここは pjax での画面書き換えが終了したタイミングで実行されます。何もしなければタイトル <title> が変更されませんので、pjaxでの画面変更後に改めてタイトル関係要素の文字列を書き換えています。こうしないとブラウザ履歴に表示されるタイトルが変わりません。

views/page1.ect (GET /page1 されたときに呼び出される画面)
<% extend 'layout' %>
<% include 'page1-partial' %>

views/page1-partial.ect (page1からincludeされるパーツ)
<div data-title="<%= @title %>">
  partial-page1 Created at: <%= new Date().getTime(); %>
</div>

views/page2.ect (GET /page2 されたときに呼び出される画面)
<% extend 'layout' %>
<% include 'page2-partial' %>

views/page2-partial.ect (page2からincludeされるパーツ)
<div data-title="<%= @title %>">
  partial-page2 Created at: <%= new Date().getTime(); %>
</div>


app.js

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var app = express();

// ECT view engine setup
var ECT = require('ect');
var ectRenderer = ECT({
  watch: true,
  root: __dirname + '/views',
  ext: '.ect'
});
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ect');
app.engine('ect', ectRenderer.render);

// routes setup
var routes = require('./routes/index');
app.use('/', routes);

// uncomment after placing your favicon in /public
//app.use(favicon(__dirname + '/public/favicon.ico'));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
  extended: false
}));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

module.exports = app;

app.jsではpjaxのために特に何もしていません。


routes


reoutes/index.js
var express = require('express');
var router = express.Router();

// middleware for pjax
router.use(function(req, res, next) {
  var properTemplate;
  var path;
  if (!req || !req.path || req.properTemplate) return next();
  path = req.path;
  if (path) {
    properTemplate = path.substr(1); // eg. "/page1" -> "page1"
  } 
  if (!properTemplate) {
    properTemplate = "index"; // default template
  }
  // for pjax
  if (req.header('X-PJAX') == "true") {
    properTemplate = properTemplate + "-partial";
  }
  req.properTemplate = properTemplate;
  next();
});


/* GET homepage. */
router.get('/', function(req, res, next) {
  console.log("GET /");
  // forwarding to "/page1"
  req.url = "/page1";
  req.properTemplate = "page1";
  next();
});

/* GET /page1 */
router.get('/page1', function(req, res, next) {
  console.log("GET /page1");
  // req.properTemplate is "page1" or "page1-partial"
  res.render(req.properTemplate, {
    title: 'This is Page1'
  });
});

/* GET /page2 */
router.get('/page2', function(req, res, next) {
  console.log("GET /page2");
  // req.properTemplate is "page2" or "page2-partial"
  res.render(req.properTemplate, {
    title: 'This is Page2'
  });
});

module.exports = router;

NOTE:

// middleware for pjax
router.use(function(req, res, next) {...
ここではクライアントからの X-PJAX なリクエストヘッダを裁くために自作ミドルウェアを作成しています。このミドルウェアは、リクエストpathを見てそれに見合ったテンプレートファイル名を req.properTemplate に保存します。またクライアントサイドからのリクエストが pjax だった場合(つまり req.header('X-PJAX') == "true" だった場合)は、properTemplate の末尾に "-partial" が付与されます。
req.properTemplateは res.render() の第一引数(テンプレートファイル名)に使うことを想定しています。

これによって、例えばクライアントサイドから GET /page1 された時に普通のリクエスト(非pjax)だった場合には page1.ect を含むlayout画面が返されることで画面全体が表示されますが、もしpjaxなリクエストだった場合には page1-partial.ect な画面パーツだけが返されることになります。

// forwarding to "/page1"
req.url = "/page1";
req.properTemplate = "page1";
ここでは、 GET / された時に /page1 へ forward させています。このfoward方法では次の処理で自作pjaxミドルウェアが呼ばれないので、ちょっとカッコ悪いですが req.properTemplate = "page1" しています。


アプリはこれで完成です。
この時点でプロジェクト構造はこのようになっています。(node_modulesフォルダは省略)
.
├── app.js
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   │   ├── jquery-2.1.0.min.js
│   │   └── jquery.pjax.js
│   └── stylesheets
│       └── style.css
├── routes
│   └── index.js
└── views
    ├── layout.ect
    ├── page1-partial.ect
    ├── page1.ect
    ├── page2-partial.ect
    └── page2.ect


実行


それではいよいよサンプルアプリを実行です。

$ npm start

サーバを立ち上げたら http://localhost:3000 にアクセスしてみましょう。
/page1 な画面に forward されていると思います。

page1 や page2 のリンクをクリックすると画面遷移をすることなく #content-wrapper 部分(とタイトル関係)が書き換わると思います。もちろんブラウザ履歴からも画面を移動できるはずです。

また、ブラウザのアドレスバーに http://localhost:3000/page2 などと直接打ってもpage2な画面はちゃんと表示されるはずです。

以上となります。

0 件のコメント:

コメントを投稿