15th
Tumblr Life 1.0 Pre 8
Tumblr Dashboardを拡張するユーザースクリプト、Tumblr Lifeの1.0 Pre 8を公開しました。
Dashboardのフィルターリストを復活したのと、デザインの調整がメインになります。また、Tumblrが標準でLikeのキーボードショートカットを実装したので、Tumblr Lifeではサポートを止めました。ショートカットが以前のAからLに変更されています。
詳しい変更点。
- Tumblrが標準でLikeのキーボードショートカットを実装したので、Tumblr Lifeではサポートを止めた
- /tumblelog が /blog に改名されたので、それに合わせた修正
- GreaseKitをサポートから外した
- 新しい(と言っても去年の中頃にアップデートされた)Dashboardに合わせて表示を調整
- Dashboardのフィルターリストが表示されなかった問題修正
最近意外と使ってもらえていることを知る機会がいくつかあってうれしく思っています。要望や問題を発見した際はGitHubのIssuesや@ykskまでお願いしますー。
GreaseKitのサポート廃止に当たって、作者のkzysさんに今後更新の予定がないことを確認しました。今まで素晴らしいツールをありがとうございました!
18th
pixivポップボードのキャッシュの仕組みとFacebookのUIの話
こんにちは。JavaScript Advent Calendar 2011 オレ標準コース18日目の@ykskです。
先日pixivにポップボードという通知機能がリリースされました。自分がお気に入りユーザーに追加されたり、投稿したイラストがブックマークされたりした時にヘッダーに未読件数などのお知らせを表示します。僕は直接機能を実装していたわけではないのですが、リリース直後に起こった負荷の問題でJSを書きました。今日はその話をします。主にUIの話です! え!
リリース直後、定期的に未読数の更新をAjaxで行っていた部分の負荷が急激に上がりました。ページロード時にHTMLに未読数を埋め込んだあと、2分ごとに未読数取得APIへリクエストするという処理です。
ポップボードはヘッダーに出るため、ほぼ全てのページでこの処理が入ります。複数のタブでpixivを開いて見ていた場合、その分だけリクエストも増える……そのことが原因じゃないかという話になった時、まじかよと思って自分のブラウザを確認すると複数タブで開いて見ていました。ははーん……。
具体的な対応は後述しますが、結果的には負荷の問題は解決出来ました。それよりも気付かされたのはサイトの設計の問題です。僕はこの現象をUI上のバグだと思いました。同じお知らせ機能のあるFacebookとpixivの違いを考えた時に浮かぶFacebookを複数タブで開いて見ているか?という疑問。Facebookはサイト設計にあたって複数タブを開かせないことに最適化しているのではないか。そして現状pixivは複数タブを開いた方が便利な設計になっていること、負荷になるほど一般的な方法だと思われること……。説が正しいかは別にして、思い付いてからFacebookへの見方が変わりました。チャットもアクティビティも1つのタブで全て見れるし、遷移も速い(history.pushState)。フィードに全てが流れる。不便さを感じさせないことはユーザー体験だけでなく、同じ機能を実装しようと思った時に負荷の面でもメリットになっているいい例なのではないかと思いました。逆に局所的に先進性を取り入れた結果、歪みが生じるのは避けられないことなのかもしれません。
以上UIの話でした。取り上げたUIの問題については現在製作中の国際版で解決する予定です。Railsで作っています。興味のある方一緒に作りましょう!
明日は@aerithさんがinstagramのお話をしてくれるようです。楽しみですね! 以下は実装の話になります。
さて具体的な解決方法です。結論から言うと、JS側ではタブ間でキャッシュを共有して、いくつタブを開いていてもリクエスト数は一定になるようにして負荷を減らしました。
# ざっくりとしか説明していません。実際のコードはpixiv.jsで見ることが出来ます。今のところ難読化していません:)
キャッシュにはHTML5のlocalStorageを使います。localStorageがすばらしいのはブラウザ全体で値を共有出来ることです。ページをリロードする必要なく、タブ1で値を書き換えたらその瞬間にタブ2で更新された値にアクセス出来ます。この仕組みがあればキャッシュを全体で共有出来ます。
- localStorageの値を監視
- 値の期限が過ぎていたらAPIから値を再取得して、結果をlocalStorageに突っ込む
- 値の期限が残っていたらlocalStorageの値をそのまま使う
これにより最初に期限が切れたタブだけがリクエストして、他のタブはその結果を使い回します。
対策した後の処理の流れです。まず、ページロード時にHTMLに入っている未読数をlocalStorageに突っ込みます。このキャッシュは2分間有効。これによって2分以内に新しいタブが開かれ続ければ常にキャッシュが生きるのでAPIリクエストは全く発生しなくなります。キャッシュの期限はsetIntervalで10秒ごとに確認します。前述の通り、古くなっていればそのタブが値の更新を担当します。まだ期限が残っていれば、値が新しくなっていないかの確認を行います。
タブを開きっぱなしにして何も操作をしていない場合を想定して、リクエストの度に間隔を4分→8分と延ばすようにしました。新しいタブが開かれると間隔はリセットされます。
タブを開いたタイミングによっては1つのタブでリクエストしている間に別のタブからさらにリクエストが飛ぶ可能性があるので、その対策も必要です。今回はjQueryのDeferredオブジェクトを使って解決しました。localStorageにロード中のフラグを立てて、フラグが立っている間に発生したリクエストにはDeferredオブジェクトを返しておきます。最初のリクエストの結果が返ればlocalStorageの期限が更新されるはずです。待っているタブではキャッシュを1秒おきに確認して、更新されたらDeferred.resolveで更新されたキャッシュの値を流してあげるようにしました。
localStorageでいくつか注意する点として、1つはlocalStorageにはCookieのように有効期限が設定出来ません。今回はこんなこともあろうかと整備してあった、localStorageの値自体に期限を設定出来るライブラリを使ってさくっと実装出来ました。抜き出すと、
function storageParse(d) {
var data, expires;
try {
if (!isNaN(d)) {
return d;
}
d = JSON.parse(d);
return $.type(d) == 'object' ?
pixiv.storage.parseExpire(d) :
d;
}
catch (e) {
return d;
}
}
function storageAddExpire(data, expires) {
expires = +new Date + (expires || 0);
return {data: data, expires: expires};
}
function storageParseExpire(data) {
var expires = data.expires;
return expires || expires === 0 ?
(+new Date > expires ? undefined : data.data) :
data;
}
function storageLocalStorage(name, value, expires) {
expires && (value = pixiv.storage.addExpire(value, expires));
return storageStorage('local', name, value);
}
function storageStorage(type, name, value) {
var storage = w[type + 'Storage'], ret;
if (!storage || !name || !$.support.json) return;
switch (value) {
case null:
storage.removeItem(name);
break;
case undefined:
return pixiv.storage.parse(storage[name]);
default:
try {
$.browser.touchDevice && storage.removeItem(name);
storage.setItem(name, $.type(value) == 'object' ? JSON.stringify(value) : value);
}
catch (e) {
pixiv.log(e);
}
}
}
// 使う時はこれだけでOK
pixiv.storage.localStorage('name', value, 1000 * 60 * 60);
このような感じ。オプションを与えるだけでいいようにしておくと便利だと思います。もう1つはlocalStorageの値はサービスのログイン状態とは無関係ということです。ユーザーがログアウトした後も固有の情報がlocalStorageに残り続けてアクセス出来てしまうのは問題です。今回は未読数しかキャッシュしない上にキャッシュする時間も短時間なので利用しましたが、この制限でだいぶ利用範囲が狭まります。。実装可能か検討する際には考慮しておきましょう。最後にlocalStorageはIE 8未満では使えません。pixivではIE 8未満はサポートしない方針なので、APIリクエストによる未読数更新はlocalStorageをサポートしないブラウザでは動かないようにしました(ページをリロードすれば更新される)。
最終的にどうにかなったものの、ダメだった場合は機能自体が成り立たない可能性がありました。非同期な機能が増えると事前の予測が難しくなっていくので、問題が起こった例は共有していくといいと思います。これらの処理が必要になるサービスは滅多にないとは思いますが、何かの折りに参考になればうれしいです。
4th
jqyery.dataset.js
前回の$.fn.dataの問題を回避するためにしばらく $.fn.attrを使っていたのですが、面倒になってきた(特に$.fn.attrではオブジェクトで値を取得出来ない)ので$.fn.datasetを作りました。
使いながらもう少しチューニングするかも。
29th
$.dataはHTML5 datasetのラッパーではない
HTML5では要素に任意の属性を追加出来るdata-*属性がサポートされました。仕様では
<div data-foo="bar">...</div>
data-fooの値にはdiv.dataset.fooからアクセスすることが出来ます。ただしdatasetは現在一部ブラウザでしかサポートされていない。。そこでjQueryに存在する$.dataメソッドを使いましょう、という流れなのですが、$.dataをラッパーとして見た場合結構罠がありおすすめ出来ません。中途半端に対応してしまったために誤解されがちですが、$.dataはdatasetのラッパーではありません。
属性値がJSONとして解釈出来る場合、パースして返す
<div id="test1" data-json="{"foo":"bar"}">...</div>
$('#foo').data('json').foo // = "bar"
<div id="test2" data-number="42">...</div>
$('#test2').data('number') // = 42
<div id="test3" data-null="null">...</div>
$('#test3').data('null') // = null
HTML5 datasetは全ての値を文字列として扱いますが、$.dataはJSONとして扱える場合JSONとして処理します。上の例のようにJSONテキストはオブジェクトに、数値文字列は数値型に、「null」はnullに変換されます。また、「true」と「false」は特別に真偽値に変換されます。
このためユーザーの入力値をdata-*属性に入れている場合、処理内容によってはセキュリティ的に問題になったり、文字列のみが返る想定で作っているとJSのエラーを引き起こす可能性があります。TwitterのポストIDのように極端に桁が大きい数値文字列は、この型変換で丸められて正常に取得出来なくなってしまうかもしれません。
元々jQueryは独自に要素にデータを持たせる仕組みを持っており、HTML5でdata-*属性が仕様化された後に「data-*属性にデフォルト値を指定出来る」扱いでdata-*属性の値取得をサポートしました。データそのものの扱いはjQueryの以前からの仕組みのままです。そのため、1.6.2現在でも$.dataで書き込んだ値にDOMからアクセス出来ないなど、単純に属性値を読み書きするメソッドとして考えるのは無理があります。
このような状況なので、使うならば仕様を良く理解した上で、data-*属性の内容が任意の場合やパース処理が余計な場合には$.dataを使わずに単純に文字列で入出力するメソッドを作るのがいいと思います($.attrも使える)。先に触れたように誤解されやすいので、jQueryに$.datasetメソッドが新設されてもいいかもしれないですね。プラグインはあるようです(ただしこのプラグインは$(element).dataset('fooBar', 123)をdata-foobar属性に書き込む。仕様に合わせる意味でdata-foo-barに書き込まれるのがベターだと思うし、最新のjQueryではそうなっている)。
24th
IEメモ
localStorageで容量限界近くになると書き込み処理でout of memoryが起こる
IE 8で確認。9ではチェックしてないです。全く書き込みをしない
localStorage.foo = '';
でも起こる。が、必ず起こるわけではない。別の書き方で
localStorage.removeItem('foo');
とすれば再現しなくなった。直接代入するのは止めた方が良さそうな気配。ついでに気付いたのが、代入して容量オーバーになった時のエラーはメモリが不足しています。
(とout of memoryのアラート)、setItemを使った時のエラーはこの操作を完了するのに十分な記憶域がありません。
と内容が変わること。やはりメソッドを経由した方が良さそう。
確認してないけどsessionStorageも同様な気がする。
固定サイトの挙動
IE 9で追加された固定サイト機能。
meta要素に設定を書くのだけど、その設定はmsapplication-starturlのURLに見に行っているらしい(今開いているページに同様の記述があっても)。指定したURLがリダイレクトしていると設定がいつまでも反映されずに、例えばタスクの定義が反映されなかったりしてハマる。
あとタスクバーにショートカットを置いた際に大きなアイコンが必要になったので、faviconをX-Icon Editor(紹介記事は複数サイズに対応したアイコンエディターが公開されました (HTML5 アプリ) - Shigeya Tanabe’s blog - Site Home - TechNet Blogs)で作ったらIE 8で壊れて見えるアイコンが出来て泣いた。Safariで作ったからかな…。
24th
IEのアップデートリンクに使いたいURL
IEにおけるgetfirefox.comのようなURLには
を利用するのがいいみたい。
このURLだと言語ごとのIEのページにリダイレクトしてくれます。さらにリダイレクト先のページではOSにインストール出来る最も新しいIEが紹介されるので(例えばWindows XPではIE 8)、アップデートを促す際には最適です。
getinternetexplorer.comやgetie.comもあるようですが、いずれも言語を判別せずに英語サイトに飛ぶようです。
@mayukiさんに教えてもらいました:)
13th
jQuery Templatesのテンプレート内にscript要素を書く
jQuery Templatesはscript要素の中にテンプレートを書く都合上、普通にテンプレート内にscript要素を登場させることが出来ません。
<script id="template-foo" type="text/x-jquery-tmpl">
<script src="foo.js"></script><!-- ここでテンプレートの終わりと見なされる -->
<p>この要素はHTMLに溢れる</p>
</script>
みんなどうしてるんだろうと思ってぐぐったら微妙な解決方法しか出てこなかったので、{{script}}テンプレートタグを作りました。usage.htmlみたいにして使います。標準で入れて欲しい!
12th
TurntableでファンのDJが回し始めたらGrowlに通知する

Turntable面白い!
TurntableにはファンになったDJが演奏を始めるとメール通知してくれる機能がありますが、見逃してしまって気付いたころには終わっていることもよくあります。そこでMail.appのルール機能を使ってメールが来たらGrowlに通知するようにしてみました。
- mail-turntable-notifier.scptを適当な場所に保存
Mail.appに画像のようなルールを追加。「AppleScript を実行」には1で保存したファイルを選択

スティッキーなどの設定はGrowlの設定にある「Mail.app Rules」から変更してください。
19th
Tumblr Life 1.0 Pre 7
Tumblr Dashboardを拡張するユーザースクリプト、Tumblr Lifeの1.0 Pre 7を公開しました。
変更点。
- Firefox 3.6でポスト出来ない問題修正
- Endless scrolling無効時にページ高さが足りないと下部のエントリーにポジションを合わせられないTumblr側の問題を解決
- パフォーマンス改善
動作は落ち着いてきたように思います。設定画面がいるかどうかを考え中。
1.0までに実装、修正予定の項目はGitHubのIssuesで管理しています。
11th
Tumblr Life 1.0 Pre 6
Tumblr Dashboardを拡張するユーザースクリプト、Tumblr Lifeの1.0 Pre 6を公開しました。
ちまちま直してます。変更点はコミットログをどうぞ。
1.0までに実装、修正予定の項目はGitHubのIssuesで管理しています。

