Archive for the ‘JavaScript’ Category

[オフラインで使いたい]スマホで Indexed Database API まだですか [iOSに実装はいつ?]


2013
05.09

皆様、おはようございます。今回は Indexed Database API についてです。

そう言えば今年は蛇年ですね、蛇。

最近すっかり見なくなってしまいましたが、昔はシマヘビなんかを見かける度に追いかけ回したものです。

 

Indexed Database API ってどうなの?

さて、久々にガッチリと プログラム関係の技術情報 で行きたいと思います。初心忘れるべからずです。

当社は今年も引き続きスマホやタブレットの web アプリを推して行きたいと思っているのですが、必ずネックになる部分があります。

それはやはり「 ローカルでデータベースを操作する 」、これでしょう。

当サイトでも色々検証しました マニフェスト と組み合わせれば、オーディオ / ビデオ系の再生を除けばかなり ネイティブアプリ のようにゴキゲンな動作をするアプリが作成可能になります。

 

黎明期は JS から「 SQLite 」を扱う事が出来る Web SQL Database API 一択だったので、何も悩むこと無く使っていたのですが、 2010 年だか 2011 年だかにこの API が HTML5 の仕様策定範囲から外されてしまいました。(出典 : Livecast 様)

よって去年( 2012 年)は今後メンテナンスが無く、いつ消えても文句は言えない状態の Web SQL Database API なんて使ってらんないぜ!推奨されている Indexed Database API ってのに乗り換えよう!

と色んな解説サイトを巡りながらサンプルを作ってみたのは良かったのですが…

iOS で動かないのよ、コレが。

http://labs.vividworks.jp/indexedDB/

Indexed Database API

プログラムの中身はこんな感じ

※jQueryを読み込んでます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
var IDB = window.indexedDB || window.webkitIndexedDB;
var cursorIDB = window.IDBCursor || window.webkitIDBCursor;
var transactionIDB = window.IDBTransaction || window.webkitIDBTransaction;
var keyrangeIDB = window.IDBKeyRange|| window.webkitIDBKeyRange;
var DB_NAME = 'TEST';
var DB_VERSION = '1.0';
var DB_TODO = 'todo';
var DB_SORT = false;
var odb;

alert(IDB);

//=================================================
// DBのバージョン別 Object Store 初期化オブジェクト
//=================================================
var DB_OBJECT_STORES = {
    '1.0': function(odb) {
        var objectStore = initObjectStore(odb, DB_TODO, { keyPath: "id", autoIncrement: true });
        objectStore.createIndex('flg', 'flg');
    }
};

//=================================================
// Object Storeのイニシャライズ
//-------------------------------------------------
// DBスキーマが変更されていたら既存の Object Storeを破棄して再生成する。
// 存在し無い場合は新規で生成する。
//-------------------------------------------------
// @param:  object
// @param:  string
// @param:  object
//-------------------------------------------------
// @return: object
//=================================================
var initObjectStore = function(odb, name, option) {
    var check = odb.objectStoreNames;
   
    for(var key in check) {
        if(check[key] == name) odb.deleteObjectStore(name);
    }
   
    var objectStore = odb.createObjectStore(name, option);
    return objectStore;
};


//=================================================
// DBのバージョンを比較
//-------------------------------------------------
// @param:  string
// @param:  string
//-------------------------------------------------
// @return: bool
//=================================================
var checkVersion = function(current, latest) {
    if(current != latest) return false;
    else return true;
};


//=================================================
// DBオープン
//-------------------------------------------------
// @param:  object
//=================================================
var openDB = function(callback) {
    var req = IDB.open(DB_NAME);
   
    // オープンに成功
    req.onsuccess = function(event) {
        alert("success");
        odb = req.result;
        var current = odb.version || 0;
        var latest = DB_VERSION;
       
        if(!checkVersion(current, latest)) {
            odb.setVersion(latest).onsuccess = function(event) {
                for(var key in DB_OBJECT_STORES) DB_OBJECT_STORES[key](odb);
                typeof callback == "function" && callback();
            };
        } else {
            typeof callback == "function" && callback();
        }
       
        getTodo(function(todo) {
            showTodo(todo);
        }, DB_SORT);
    };
   
    // オープンに失敗
    req.onerror = function(event) {
        alert("error");
    };
};


//=================================================
// ObjectStore の取得
//-------------------------------------------------
// @param:  string
// @param:  string
//-------------------------------------------------
// @return: object
//=================================================
var getObjectStore = function(name, mode) {
    var transaction;
   
    switch(mode) {
        case 'READ_WRITE': transaction = odb.transaction(name, transactionIDB.READ_WRITE);  break;
    }
   
    return transaction.objectStore(name);
};


//=================================================
// データ書き込み
//=================================================
var putTodo = function() {
    if($('#title').val().length > 0) {
        var data = {
            flg: 0,
            title: $('#title').val()
        };
        var todoObjectStore = getObjectStore(DB_TODO, 'READ_WRITE');
        var put = todoObjectStore.put(data);
       
        put.onsuccess = function() {
            $('#title').val('');
            getTodo(function(todo) {
                showTodo(todo);
            }, DB_SORT);
            alert('登録しました。');
        };
       
        put.onerror = function() {
            alert('登録に失敗しました。');
        };
    }
};


//=================================================
// データ読み込み
//=================================================
var getTodo = function(callback) {
    var todoObjectStore = getObjectStore(DB_TODO, 'READ_WRITE');
    var res = new Array();
    var get;
   
    switch(DB_SORT) {
        case 0:
            get = todoObjectStore.index('flg').openCursor(0);
            break;
       
        case 1:
            get = todoObjectStore.index('flg').openCursor(1, cursorIDB.PREV);
            break;
       
        default:
            get = todoObjectStore.openCursor();
            break;
    }
   
    get.onsuccess = function() {
        var cursor = get.result;
       
        if(!cursor) {
            typeof callback == "function" && callback(res);
            return;
        }
       
        res.push(cursor.value);
        cursor.continue();
    };
   
    get.onerror = function() {
        alert('読み込みに失敗しました。');
    };
};


//=================================================
// データ更新
//=================================================
var updateTodo = function(id, mode) {
    var todoObjectStore = getObjectStore(DB_TODO, 'READ_WRITE');
    var update = todoObjectStore.openCursor(keyrangeIDB.lowerBound(id, false));
   
    update.onsuccess = function() {
        var cursor = update.result;
       
        if(cursor) {
            var data = cursor.value;
            data.flg = mode;
            cursor.update(data);
            cursor.continue();
        }
       
        getTodo(function(todo) {
            showTodo(todo);
        }, DB_SORT);
    };
   
    update.onerror = function() {
        alert('更新に失敗しました。');
    };
};


//=================================================
// データ削除
//=================================================
var deleteTodo = function(id) {
    var todoObjectStore = getObjectStore(DB_TODO, 'READ_WRITE');
    var del = todoObjectStore.delete(id);
   
    del.onsuccess = function() {
        getTodo(function(todo) {
            showTodo(todo);
        }, DB_SORT);
        alert('削除しました。');
    };
   
    del.onerror = function() {
        alert('削除に失敗しました。');
    };
};


//=================================================
// データ表示
//=================================================
var showTodo = function(todo) {
    var container = document.getElementById('todoList');
   
    for(var i = container.childNodes.length - 1; i >= 0; i--) {
        container.removeChild(container.childNodes[i]);
    }
   
    for(var key in todo) {
        var li = document.createElement('li');
        var update = document.createElement('input');
        var del = document.createElement('input');
        var html = '';
       
        update.type = 'button';
        update.value = '変更';
        update.id = 'todoUpdate_' + todo[key].id;
        del.type = 'button';
        del.value = '削除';
        del.id = 'todoDel_' + todo[key].id;
        del.onclick = function() {
            if(confirm("削除しますか?")) deleteTodo(parseInt(this.id.replace('todoDel_', '')));
        };
       
        if(todo[key].flg == 0) {
            html += '[未処理]';
            update.onclick = function() {
                updateTodo(parseInt(this.id.replace('todoUpdate_', '')), 1);
            };
        } else {
            html += '[処理済]';
            update.onclick = function() {
                updateTodo(parseInt(this.id.replace('todoUpdate_', '')), 0);
            };
        }
       
        html += todo[key].title;
       
        li.appendChild(document.createTextNode(html));
        li.appendChild(update);
        li.appendChild(del);
        container.appendChild(li)
    }
};


//=================================================
// ソート切り替え
//=================================================
var sortTodo = function(id) {
    switch(id) {
        case 'sort1':   DB_SORT = false;    break;
        case 'sort2':   DB_SORT = 0;        break;
        case 'sort3':   DB_SORT = 1;        break;
    }
   
    getTodo(function(todo) {
        showTodo(todo);
    }, DB_SORT);
};


//=================================================
// メイン処理
//=================================================
$(function() {
    $('#register').click(function() { putTodo(); });
    $('#sort1').click(function() { sortTodo($(this).attr('id')); });
    $('#sort2').click(function() { sortTodo($(this).attr('id')); });
    $('#sort3').click(function() { sortTodo($(this).attr('id')); });
    openDB(function() {
        //
    });
});

いやマジで、上記サンプルを iOS 系で開いてもらえば分かりますが、11 行目の DB Object 取得部分で既に undefined が返ってくる。( =未実装 )

こっち使えと言いつつ実際は使えないってどうなん?

ねぇねぇ?どうして欲しいの??

と、割と本気で思います。

 

2013 年には…と淡い期待も有ったのですがやはりダメなようです。

iPhone4 、 iPad Retina のそれぞれ最新の iOS ファームで確認( 2013/01/08 )。ちなみに Chrome for  iOS でも当然ダメだった、ベースの webkit が同じなんだから当たり前か。

勿論 PC や Mac の webkit 系なら動くよ!多分 IE 10 とかも大丈夫。

 

結局「 無いものは仕方がない 」と言う事で、引き続き SQLite でゴリゴリ作るしか無いのかなぁ。

私は別に代替えで推奨の物があるなら、別に SQL だろうが key / value 型であろうが何でも構わないと思っています。

だって、欲しいものは「 要件の動きが実現可能 」「 動作の安定感 」「 アップデートによる保守性 」じゃないですか?

 

今市場を見れば「 全力で HTML5 を使えるのは スマホ / タブレット だけ! 」と言うのは火を見るより明らかです、ブラウザが webkit 系しか無い訳だし。

じゃあ、さっさと何とかしてちょうだい!と思うのはワガママなのでしょうか。

 

どう考えても PC or  Mac より、スマホ or タブレットの方が ローカル DB の恩恵が大きい と言う事実が追い打ちをかけています。

据え置き機がオフライン状態でしか使えないなんて事、回線トラブルとか引越し直後以外に有るの?って感じですし。

個人が買うマシンは明らかに 過剰なオーバースペック になって行ってるし、回線だって ISDN が珍しくなってきているこのご時世に「 トラフィックを減らす 」「 高速ブラウジング 」なんてのは、本当に大きな大きなサイトを運営していて、かなりサーバがしんどいよ、きっついよって思ってる極々一握りの世界なんじゃ無いかって思うんです。

 

実際は色々無茶が出来る据え置き機で開発を重ね、仕様が安定してから iOS 等のコンパクト OS に移植するって事なんでしょうけどね。

 

今年も悶々としながら続報を待っています、もう少し勉強を進めるべきか。

なんとなく下書きが残っていたので投稿しました。

それではまた。

Indexed Database API について(へっぽこプログラマーの日記様)
Indexed Database API(HTML クイックリファレンス様)
Indexed Database APIによるデータベース(Libro.様)

などなど、参考にさせて頂きました。

[jQuery]Uncaught TypeError: Property ‘submit’ of object が出た時のTips


2012
02.08

ちょっとハマったのでメモ。

 

静的な項目をチェックするなら jquery.validate.js で一発OKなのですが、動的に項目が増減するフォームで入力チェックをしたい時は自作しなきゃならない事がまま有ります。

今回複数ページに渡りそう言うフォームになっていて、一つ手前のページでは行けるのに次のページでは Uncaught TypeError: Property ‘submit’ of object と言うエラーが出てしまう。

 

どうやらその関数は既に有るから重複したものは作らないでくださいよー

 

と言うコンフリクト系のエラーらしい。

いや、でも一つ前のページでは機嫌良く動いてるじゃないですかやだー。と言う状態で少しハマったが、解決出来ました。

 

再現方法

javascript

1
2
3
4
5
6
$(function() {
    $('#submit').click(function() {
        /*  小難しい処理とか色々 */
        $('#form').submit();
    });
});

html

1
2
3
4
<form name="form" id="form" action="hoge.php" method="post">
    <input type="text" name="hoge" id="hoge" value="" />
    <img src="path/button.gif" id="submit" />
</form>

これで少なくとも webkit 系のブラウザなら再現します。

 

ちなみに「onsubmit 使えよ!」と言われるかも知れませんが、私が今作成しているフォームはサブミットしなければならないボタンが同一フォーム上に3つ以上あります。

なので type に submit や image は使えなくて、img や button に関数を割り当てて各々パラメータ変更してから submit() してやると言う仕組みなんですね。

 

さてさて、google 先生に聞いても有効な回答が得られなかったので自分で考えてみます。

まずこのエラーはコンフリクト系のエラーと言う事は解ったので、ソースを見直して見る。

JS 側は動いているページが有るので問題無いはずなので、html に注目。

ああ、ひょっとして form 内から実行してるからか…?怪しいので html を変更。

 

html(訂正版)

1
2
3
4
<form name="form" id="form" action="hoge.php" method="post">
    <input type="text" name="hoge" id="hoge" value="" />
</form>
<img src="path/button.gif" id="submit" />

 

直りました。

 

要するに #submit が form 内に入っていると form 自体が持っている function submit を継承か何かするようで、そこで submit() を実行するとエラーになる。と言う事らしい。

外に放りだした事により、親要素(書いてないけど)の div は funciton submit を持っていないからエラーが無くなった。やったね!

 

変な所でハマると時間気になって精神衛生上宜しく無いですよね。

 

[CSS3|jQuery|Webkit]translate3d等のtransformで設定した現在値を取得したい


2011
09.26

皆さんご無沙汰しております、Web屋の中の人です。

手術やらリハビリやら、少々複雑なシステム開発案件等でお勉強の方を長期に渡り休止しておりましたが久々にやってみようと言う事でインターフェイスの調整やバグ取り、むしろソースの整理なんかをしているとタイトルの状況になりました。

iPhoneiPad 上でアニメーションしたい時は、jQuery.animate は原則使い物になりませんので CSS3transform を利用してグリグリします。

 

ちなみに今回はタイトルに記載している通り「webkit」ブラウザのみ対象としていますのでご注意ください。
※Safari、Chromeなどなど。

 

ありがちな例を挙げると、js でスライダーなんかを作っている時に「この要素の x 座標は今ナンボ程動いてますか」と言う状況です。
jQuery を使っているなら簡単ですよね、

1
alert($('#hoge').css("left"));

これでパパッと解っちゃう訳で、非常に便利です。

所が CSS3transform で設定する時は、

1
2
3
#hoge {
    -webkit-transform: translate3d(237px, 5px, 0px);
}

こんな感じなので、x 座標ってどうやって書けば取得出来るの?となった訳です。

1
alert($('#hoge').css("-webkit-transform"));

取りあえずこう書いてみると、下記のように出力されました。

matrix(1, 0, 0, 1, 237, 5)

うーん、4番目が x で5番目が y なんだろうな…じゃあ z はどこに出るんだろう。

 

ブラウザ限定の強み、と言う事で素直に WebKitCSSMatrix で分解する事にした。仕様書通り使うと

1
2
3
var obj = document.getElementById('hoge');
var m = new WebKitCSSMatrix(obj.style.webkitTransform);
alert(m.e);

こんな感じになりますが、折角 jQuery を使っているので…

1
2
var m = new WebKitCSSMatrix($('#hoge').css('-webkit-transform'));
alert(m.e);

こんな感じでも取得可能です、結構乱暴に使えますね。
取得出来る値はコチラの仕様書に網羅されています、かなり色々設定出来るだけに複雑ですね…単純な物は a〜f って覚えておくのが良さそうです。

 

 

それではまた。

 

 

[IE6|IE7|IE8]cloneNodeで複製したエレメントのidをjQueryから参照出来ない時のTips


2011
05.23

皆さんこんにちわ、なんだか冷えますね。

 

さて、皆大好き IE ブラザーズのお茶目な仕様で少々ハマりましたので仕様解説と解決策の TIPS です。

実は基本的な事かもしれませんが、仕様を理解しておくのに越した事は無いでしょう。

リッチコンテンツ制作時にちょこちょこ問題になる可能性が高いので、便利な関数も作ってみました。改良して使ってもらえればよろしいかと思います。

 

出くわした状況

メニューをスライドしたい時に数がギリギリなので、一番端の要素を予め逆側に複製しておいてスライドした後に複製したエレメントを消去しようとした。

 

症状

IE だけスライドした後に複製元となったエレメントが動き出して、思い通りの動きをしなかった。

 

元ソース

■html

1
2
3
4
5
<ul id="listBox">
    <li id="list1">hoge</li>
    <li id="list2">moge</li>
    <li id="list3">foo</li>
</ul>

■javascript

1
2
3
4
5
6
7
var content = document.getElementById('listBox');
var clone = document.getElementById('list1').cloneNode(true);
clone.id = 'temp';
clone.style.position = 'absolute';
clone.style.left = '600px';
content.appendChild(clone);
$('#temp').animate({left: 400});

 

cloneNode して、id を書き換えて親要素に appendChild して動かそうとした訳です。

IE 以外のブラウザはこれで問題無く動きます、IE 以外は。

IE だけは #temp を呼ぶと複製元である #list1 を参照して、そちらを動かそうとします。ちなみにリスナを仕掛けたり、setTimeout でぐるぐる回してフラグを監視して、存在確認をした後に命令しても一切を無視して複製元を動かします。
更に jQuery から attr を使って id を変えても駄目です、凄い頑固ちゃんです。

 

早速原因を追求していくと、どうにも困った事になっていました。

 

IE は同一プロセス内で cloneNode(複製)されたエレメントは、別プロセスから呼ばれない限り複製元の id を参照する。

 

え、なんで!?と言う仕様ですよね、cloneNode(複製)したプロセスとは別にボタンを用意して、適当に命令を書いた関数を呼び出すと思い通りの動きをします。

もしかしたらちょっと違うのかもしれませんが、大体こんな感じの挙動になる筈です。

 

 

非常に気持ち悪いです。

 

 

ともあれ fix していきましょう、要は cloneNode(複製)したエレメントの挙動がおかしいのならば新規でエレメントを生成して、複製元のエレメントもしくは複製したばかりのエレメントから欲しい情報を新規エレメントに移してから appnedChild すれば良いのです。

 

改良版ソース

■javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
var content = document.getElementById('listBox');
var clone = document.getElementById('list1').cloneNode(true);
var elm = document.createElement('li');
content.appendChild(getCloneNode(clone, elm, 'temp'));
$('#temp').animate({left: 400});

function getCloneNode(clone, elm, id) {
    elm.id = id;
    elm.style.position = 'absolute';
    elm.style.left = '600px';
    elm.innerHTML = colne.innerHTML;
    return elm;
}

 

 

こうやると意図した動きになるはずです、なんだか回りくどいけど。

これは複製したエレメントから新規エレメントに要素を移していますが、多分複製せずに直接突っ込んでも上手く行きそうですね。別処理との兼ね合いで content を外で作ってますが、全部関数内でやっちゃった方が綺麗にかけそうですね。

 

そこはお好みで。

 

なんか悔しかったから複製してやっただけですし。

 

それではまた。

 

[iPhone/iPad]WebApp制作における読み込み時間/オフライン問題


2011
05.18

皆さんこんにちわ、最近随分日が長くなったなぁとしみじみ思います。

そして随分書きかけのエントリーが溜まって来たなぁと思います。

消化しないとただの豪華なオンラインメモ帳になってしまいます、これはいけません。

 

そう言う事も踏まえてなお、単発ネタです。

iPhone / iPad をターゲットにした WebApp でネックになるのは読み込み時間とオフライン時の挙動です、普通にブラウザアプリを作る感覚で行くと下記のような問題にブチ当たります。

  • ページ遷移の度に読み込んでたらストレス溜まる
  • かと言って全部最初に読み込んでおいてメモリに展開すると色々と苦しい
  • そもそもオフラインだとページ遷移とか非同期通信自体不可能じゃない

ネイティブアプリは全てローカルにリソースを置く事で回避出来るのですが、Web ブラウザを使っている以上このような問題が起こります。

 

困りました、調べました、解決しました。

 

なんの事は無い、iOSAndroidWEB Database API を実装しているじゃないか。良く考えると双方 Webkit 系だもんね、今更気づいたのかよ!と言わざるを得ません。てへ。

この API はブラウザの JS から双方のネイティブアプリを触っている方はお馴染みの SQLite3 へとアクセスする事が出来ます、つまり始まる前に全部ローカルにデータを保存しておく事が出来ます。ちょっと試した事が無いのですが blob 型が有るのでバイナリも突っ込めるはず。

そしてもう1つの問題、オフライン時にページを開こうとしたりリロードしたりしようとすると「ページを開けません。インターネットに接続していません。」みたいなエラーが出るのですが、これも .manifest を使えば回避可能です。

オフラインだとこうなります

これは強力なキャッシュ機能で、指定したファイルを全てローカルに保存して次回からはとにかくここから読み出すようです。中身を更新したい時は .manifest の中身の文字を1文字でも変えれば再構成されるようなので、ページの要素を読み込む前に .manifest のチェックが先に実行されているみたいです。

要するにバージョン管理をきちんと行い、.manifest にはそのバージョン名を入れておけば常に最新の状態をユーザーに提供出来ると言う訳ですね。

 

さて、これで…かなりネイティブアプリに迫る事が出来るようになったハズです。

思っていた以上に私は WebApp について解っていなかったのがショックですが、これで同じ土俵には上がれたと思うので益々精進したいと思います。

 

それではまた。