The Document of Aska

3-3. Offline

One Page Applicationはもちろんスマホでも動きます、しかし地下鉄を始め電波がつながらないこともしばしば...

  1. オフライン時に登録処理をキューイング
  2. オフライン時に開けるようにする

キューイング

不通になってしまった場合も登録処理が出来る仕組み

  1. Ajaxで更新失敗、ステータスコード0ならキューイング
  2. Ajax通信成功、キューがあったらサルベージ

Code

// タスクの登録に失敗し、オフラインと判断された場合、キューイングする。
app.ajax({
    type: 'POST',
    url: url,
    data: form.serialize(),
    dataType: 'json',
    salvage: true
})
.done(function(data){
    // ...
})
.fail(function(jqXHR, textStatus, errorThrown){
    if (!jqXHR.status) {
        app.queue.push({
            api: api,
            req: form.serializeArray()
        });
        app.dom.reset(form);
        var time = (new Date()).getTime();
        var task = {
            id: (task_id || (list.id + ':' + time)),
            requester: requester,
            registrant: registrant,
            assign: assign,
            name: name,
            due: due,
            status: 0,
            closed: 0,
            comments: [],
            history: [],
            created_on: time,
            updated_on: time,
            salvage: true
        };
        app.fireEvent('registerTask', task, list);
        app.fireEvent('openTask', task);
        app.dom.reset(form);
        if (task_id) {
            app.dom.hide(form);
        } else {
            app.dom.show($('#create-task-twipsy'));
        }
    }
});

サルベージ

複数のキューをまとめて投げます、サーバーサイドでキューの内容によるエラーがあっても無視します。

  • キューに問題がある場合何度実行しても無駄なのでスルー
  • 貧弱な回線だとリクエストだけ送信してレスポンスを受け取れずに失敗し、サーバー上には反映されている等のケースがよく発生するので、重複無視
  • 401等salvage失敗時はキューを消さない

Code

// app.ajax
return $.ajax(option)
.done(function(){
    if (option.url !== '/api/1/account/salvage'
        && option.url !== '/token') {
        app.util.salvage();
    }
});

// キューがあったら投げる、成功したらメッセージ出す
app.util.salvage = function(){
    var queues = app.queue.load();
    if (!queues) {
        return;
    }

    return app.ajax({
        url: '/api/1/account/salvage',
        type: 'post',
        data: { queues: JSON.stringify(queues) },
        dataType: 'json'
    })
    .done(function(data){
        if (data.success === 1) {
            app.queue.clear();
            app.dom.show($('#notice-succeeded-salvage'));
            app.fireEvent('reload');
            app.state.offline = false;
            app.state.offline_queue = false;
        }
    });
}

オフライン起動

オフライン状態からでもWebアプリケーションを起動させる仕組み

  1. manifestを用意し、applicationCacheに対応
  2. boot用apiの結果はlocalStorageに保存しておく
  3. 起動時オフラインの場合localStorageから画面構築

※Google ChromeでのapplicationCacheの挙動に不具合があるので、スマホ限定でもいいかも。

HTML

<html manifest="app-mobile.manifest">

Manifest(app-mobile.manifest)

マニフェストにはCSS/画像/Jsといった依存するリソースすべてを列挙します。

列挙されていないリソースがHTMLから参照されているとうまく動作しません。

完成したアプリでうまくapplicationCacheが効かない場合、最小のサンプルコードから原因を探っていきましょう。

CACHE MANIFEST
# version 1.52
CACHE:
/static/css/bootstrap-1.3.0.min.css
/static/css/app-base.css
/static/css/app-mobile.css
/static/js/jquery-1.6.4.min.js
/static/js/jquery-ui-1.8.13.js
/static/js/iscroll.js
/static/js/spin.min.js
/static/js/app-core.js
/static/js/app-base.js
/static/js/app-base-signin.js
/static/js/app-base-viewer.js
/static/js/app-mobile-viewer.js
/static/img/address.png
/static/img/create-task.png
/static/img/cross24.png
/static/img/down.png
/static/img/email24.png
/static/img/email_off24.png
/static/img/left24.png
/static/img/minus24.png
/static/img/plus24.png
/static/img/right24.png
/static/img/star24.png
/static/img/star_off24.png
/static/img/tasks24.png
/static/img/tasks_half24.png
/static/img/tasks_off24.png
/static/img/up.png
NETWORK:
/token
/api/1/account/me
/api/1/account/update
/api/1/account/delete
/api/1/account/salvage
/api/1/list/create
/api/1/list/update
/api/1/list/delete
/api/1/list/clear
/api/1/task/create
/api/1/task/update
/api/1/task/move
/api/1/comment/create
/api/1/comment/delete
/api/1/twitter/update_friends
/api/1/profile_image/
https://tasks.7kai.org/api/1/profile_image/
https://graph.facebook.com/
https://si0.twimg.com/
https://si1.twimg.com/
https://si2.twimg.com/
https://si3.twimg.com/
FALLBACK:
https://tasks.7kai.org/api/1/profile_image/ /static/img/address.png
https://graph.facebook.com/ /static/img/address.png
https://si0.twimg.com/ /static/img/address.png
https://si1.twimg.com/ /static/img/address.png
https://si2.twimg.com/ /static/img/address.png
https://si3.twimg.com/ /static/img/address.png

Nginx

location ~ .*.manifest {
    add_header Content-Type text/cache-manifest;
    root /home/aska/Dropbox/product/7kai-Tasks/htdocs/static;
}

Code

// オンライン時にboot用APIの結果はlocalStorageにキャッシュしておく
app.api.account.me = function(option){
    if (!navigator.onLine) {
        console.log('offline');
        return;
    }
    return app.ajax({
        url: '/api/1/account/me',
        data: option.data,
        dataType: 'json',
        loading: false,
        setup: option.setup
    })
    .done(function(data){
        if (data) {
            localStorage.setItem("me", JSON.stringify(data));
            app.util.buildMe(option, data);
        }
    });
}

// オフライン時はlocalStorageから画面構築
app.addListener('setup', function(option){
    if (navigator.onLine){
        app.api.account.me({ setup: true });
    } else {
        var data = localStorage.getItem("me");
        if (data) {
            app.util.buildMe({ setup: true }, JSON.parse(data));
        }
    }
});