Takazudo hamalog

programming notes. mainly about JavaScript / jQuery. [@Takazudo] [takazudo@gmail.com] Hint: alt + /

cool guy

Davis.jsでHistory APIを比較的お手軽に使う

2011/07/28 permalink

ポチってもうたー開発メモ。
今日、HTML5 History APIの、URLは静的に見えるけどダイナミックにUI変えちゃうよーっていうのを実装してみた。ポチってもうたーをChromeとかFirefoxで見ると、色々遷移するとURLは変わるけど全面リロードは起こらなくなってます。

これを実装するには、HTML5のhistory.pushStateとかhistory.popStateとかいう、historyをいじる仕組みを使わないといけないんですが、その辺をうまいことラップしてくれる Davis.js というライブラリがあったので、これを使ってみました。

Davis.jsの使い方は、基本的にはこんな感じ。

var app = Davis(function () {
    this.get('/', function () { // ...1
        // / に遷移した場合に発火。
        // トップページにするがごとくここでUIをゴネゴネいじったりする
    })
    this.get('/user/:name', function (req) {
        var name = req.params['name'];
        // /user/hoge とかに遷移した場合に発火。
        // hogeという値がとれるので、ユーザーがhogeである場合のページ
        // にするがごとくここでajaxするなりしてUIごゴネゴネいじったりする
    })
    this.get('/help', function (req) {
        // /help に遷移した場合に発火
        // ヘルプページの内容にするがごとくここでUIごゴネゴネいじったりする
    })
    this.post('/update', function (req) {
        var postedVal = req.params['name'];
        // /updateにpostされた場合に発火。
        // postされた値を取れる。ajaxしてどうのこうのする
        req.redirect('/' + req.params['name'])
        // それで / にリダイレクトさせたりする
        // →上記1が発火する
    })
})
app.bind('unsupported', function () {
    alert('あんたのブラウザはHistory APIに対応していないでーす');
})
$(document).ready(function () {
    app.start(); // これで始める
})

Davis.jsは、全てのa要素click、formのsubmitを監視して、その遷移先が指定したものにマッチした場合、指定したハンドラを実行してくれる。全てのa要素、formで実行されてしまうのが嫌な場合は、設定を変えることもできる。Davis.jsのサンプルを見ると、そんなかんじなのかと分かるかと思います。

遷移を監視して別のことをさせる。だけどURLもちゃんと変わっているというのは、おお素敵だねと思うんですが、面倒なのは、History APIに対応していない場合でも、その状態のページを見せないといけないこと。Davis.jsのサンプルだと、初め、URLは
http://davisjs.com/examples/greeter
なんですが、フォームに bob と入力すると、以下のようにURLが変わる。
http://davisjs.com/greet/bob
で、ブラウザでそのタブを閉じてまた
http://davisjs.com/greet/bob
を開いても、サーバー側でうまいことしていないと、当然ながら、NOT FOUNDと言われてしまいます。なので、JSがその辺の状態遷移を上手いこと管理できて、サーバー側もそれに合わせて上手いことhtmlを書きだしてやらないといけない。

noscript時のことも考えると、サーバー側でもテンプレにデータをちゃんと突っ込んで吐き出してクライアント側でもダイナミックにhtmlを変化させて…とやらないといけないわけなんですけど、今回、ポチってもうたーではnoscript時のことは考慮していないので、

History APIを使える時: Davis.jsを使う
History APIを使えないとき: DOMContentLoaded時、URLにマッチしたハンドラを実行
としました。

簡単に言うと、/ に遷移したときは、Davis.jsでは、トップページをガリガリ作る処理を走らせるわけですが、History APIを使えないときは、URLが / だったら、初めにその処理を走らせるだけという感じです。history APIが使えないので、全ての遷移は普通にページ移動になります。このへんは、IE9以下でポチってもうたーを見てもらうとどういうことなのか分かります。

で、これを実装するため、Davis.jsと、このオンロード発火みたいなものをブランチさせるラッパーを書きました。それが、以下のようなもの。

/**
 * UrlDispatcher
 */
var UrlDispatcher = function(presets){
    var self = this;
    self._presets = presets;
    if(Modernizr.history){ // html5 History API使えたらDavis.js使う
        // davisをせっせと設定する
        self._davis = Davis(function(){
            var davis = this;
            $.each(presets, function(i, p){
                davis[p.davis_method](p.davis_path || p.path, p.handler);
            });
        });
        self._davis.configure(function(config){ // 発火させる要素にはjs-davisクラスつけるようにした
            config.linkSelector = 'a.js-davis',
            config.formSelector = 'form.js-davis'
        });
    }
};
UrlDispatcher.prototype = {
    fire: function(){
        if(Modernizr.history){
            this._davis.start();
            return;
        }
        // プリセットにマッチするURLがあったらハンドラを即発火
        $.each(this._presets, function(i, p){
            var handleThis = false;
            if(p.nodavis_path){
                handleThis = p.nodavis_path.test(location.pathname);
            }else{
                handleThis = p.path === location.pathname;
            }
            if(!handleThis){
                return;
            }else{
                p.handler();
                return false;
            }
        });
    },
    // instance.invoke('/') とかするとDavis.jsにリダイレクトさせるメソッド
    invoke: function(path, method){
        if(this._davis){
            Davis.location.assign(new Davis.Request({
                method: method || 'get',
                fullPath: path,
                title: ''
            }));
        }else{
            // 対応してなかったら普通にリダイレクト
            // ※History APIはlocationの変化を監視しているわけではないので
            // このようにlocation.hrefを変えてもイベントが発火するわけじゃない
            location.href = path;
        }
    }
};

/* so let's do it */

$(function(){
    var app = new UrlDispatcher([
        {
            path: '/',
            davis_method: 'get',
            handler: function(){
                // トップページを作るどうのこうの
            }
        },
        {
            path: '/me',
            davis_method: 'get',
            handler: function(){
                // 自分のページを作るどうのこうの
            }
        },
        {
            davis_path: '/u/:screen_name',
            nodavis_path: /^\/u\/.+/,
            davis_method: 'get',
            handler: function(req){
                var screen_name;
                if(Modernizr.history){
                    screen_name = req.params.screen_name;
                }else{
                    screen_name = location.pathname.match(/^\/u\/(.+)/)[1];
                }
                // screen_nameに合わせてページを作るどうのこうの
            }
        },
        ......
    ]);
    app.fire();
});

それで、サーバー側では、HTMLで参照される可能性のあるURLに対して、基本的には全部同じHTMLを返すようにしてます。なので、
http://pochitte.mouter.be/

http://pochitte.mouter.be/u/Takazudo
も、
HTMLソースは同じです。あとはURL見てJSで上手いことしてますというかんじです。

これを実装してみた感想としては、「おーURL変わってるのにJSが動いてるだけだ。すげーー」っていうのは開発者的にはあるんですが、hashchangeで同じようなことをする(Twitterとか)のと比べると、うーんやっぱりIEだとダメだよねーというのが結構気になりました。

History APIに対応していないブラウザの場合、ダイナミックにサクサク切り替わる部分が、全部普通のページ遷移になってしまうので、これはかなりの差です。というかもう、あなたのブラウザはヘボイんですよと言わんばかりの動作になってしまうので、まぁやっぱり普通にいろんなブラウザ環境のことを考えると、それはホントのURLじゃない問題はあるにせよ、後方互換性を考慮すると、hashchangeを選択するのもやむなしかなぁと思いました。現にTwitterはそうですし。

あとは、noscriptの考慮まですると、サーバー側でもテンプレを書きだしたりするロジックを実装しないといけないので、普通に考えて、かなりの開発工数がかかるんじゃないかと思います。同じテンプレートエンジンを使って巧いこと書きだしてどうのこうのと工夫すればできないことはないでしょうけど、まぁ一筋縄には行かない感じかと思います。そう考えると、いわゆる普通のプレーンな静的なHTML多数のサイトには向かない気がするので、今のところは、JSでダイナミックにでーんと全て管理するようなwebアプリが活躍の中心となる気がしました今のところ。

ただ、hashchangeはIE6以上でもpolyfillを使えば全く問題無く動作するんですが、さっき書いたホントのURLじゃない問題は延々と未来に引き継がれてしまうので、そこのところを、「先方互換性優先」としてHistory APIを採用するのはありなんじゃないかなーとは思います。

davisって誰なんだろう…

blog comments powered by Disqus

  1. fingaholichamalogからリブログしました
  2. hamalogの投稿です