nsIWebProgressListener.onStateChange で DOMContentLoaded のタイミングを受信する話
Secure Login Rebooted の、ログイン情報探索を呼び出す回数を減らそうって話です。
結論から言うと、「イベントハンドラ使わずに state flags だけで DOMContentLoad
取れた!」みたいな都合のいい話はなかった。
nsIWebProgressListener
nsIWebProgressListener とは、リソースの読み込みに応じて登録したリスナを呼び出し、任意の処理を実行可能にする仕組み。これを使うことで、「ページのロード時に○○を実行する」みたいなことが可能になる。詳細は MDC とか参照のこと。
このリスナの登録先は複数あって、nsIWebProgress
に登録するとか、gBrowser.addProgressListener()
を使って、現在のタブのイベントだけを受け取るとか、gBrowser.addTabsProgressListener()
を使って、開いたタブ( xul:browser
)のイベントをすべて受け取るとか、色々バリエーションがある。
nsIWebProgressListener の活用例
この仕組みは Secure Login(本家、fork 版)や Secure Login Rebooted でも使っている。本体でも色々な個所で使っている。nsILoginManager あたりでも使ってますね。
本題
nsIWebProgressListener でページの遷移状態を取得する場合、onStateChange()
を使うのが一般的だと思うのだけれども、onStateChange()
を使用した場合、状態フラグを適切に判別しないと無駄な処理が増えることになる。
Secure Login Rebooted の先日までの実装では以下のようなコードを用いて、ページの読み込みが終了次第、ページの読み込みが完了してから、ページ中のログイン情報を探索するようにしていた。
onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) { // ページの読み込みが止まっているかどうか if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { //ページ中のログイン情報の探索 } },
しかし、この実装には以下のような問題もあった。
- ページの読み込みが完了するまで(
STATE_STOP
になるまで)探索ができないぽいload
イベントが発火するまで探索できないのとほぼ同義
- ページ中の iframe の読み込みが完了する度に探索を呼び出す(
STATE_STOP
になる度に呼び出す) - ページ中で通信が発生する度に探索を呼び出す(
onStateChange()
の罠ですね)
Facebook のトップページや Google のログインページなどであればそこまで問題は無いのだけれども、ログインフォームのあるトップページに iframe 埋め込んでて非同期で通信までしてる pixiv とかだと、ページを開いて放置しているだけで探索が5回も10回も実行されるみたいなひどいことになっているわけ。
で、しょうがないのでどうにかこの探索回数を減らすことにチャレンジしてみた
結局 DOMContentLoad
を使う
最初は onStateChange()
フラグだけで頑張ってみようとしたのだけれども、どうしても通信が発生する度に onStateChange()
が呼び出されて上手くいかない。
しかたがないので DOMContentLoad
のタイミングでイベントリスナ呼び出すように登録させることにした。
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { // STATE_STOP だと読み込み完了してるのでDOMContentLoaded使う意味ない if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_STOP)) { // タブの復元時はDOMContentLoaded発火しない if (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) { // 探索処理に直行 } else { // DOMContentLoaded のタイミングで探索処理を行わせる aBrowser.addEventListener("DOMContentLoaded", this, true, true); } } },
ページが DOMContentLoad
を発火させたタイミングにのみ探索処理を行えることに成功。これで探索回数をガクッと減らせるようになった。
この実装の問題点
上手くいくかなと思ったのも束の間、別の問題発生。https://twitter.com/login を、初めて訪問した場合に、ログイン情報を探索しても見つからなくなる事態が発生する。
リダイレクト先の https://twitter.com/#!/login を直接訪れた場合は、問題なく動いているので、
みたいな条件が揃った場合だと、
DOMContentLoad
発火後にページが書き換わる- 通信の度にチェックしていないので、非同期でリソースを読み込んでページを更新する種類のテクニックに対応できない
- アドレスバー更新してから少し経った後の変更は
onLocationChange()
で捕まえられない
ので、ダメなんじゃないかなーと推察。振り出しに戻りかける。
(※挙動から推察してるんで、厳密な発生条件は不明)
こうした問題があったけれども、結局は DOMContentLoad
を採用することにした。理由は以下の二点。
- 問題が発生するページはレアケースである
- 上記の Twitter の例も、十分に回避可能な状況である
- 正規のフォームであれ、動的に構築されたログインフォームは行儀が悪いので、無視すべきであるように感じる
とくに下の点が主な理由。今後、History API を使うなどした所謂pjaxが普及するとしても、ログインフォームの動的構築はあまり行儀が良くないように感じる。History API での動的ページ遷移を行っている Github ですら、ログインページは専用の固定ページを使用している。
仮に今後、そうしたログインフォームが一般的になり、無視できなくなった場合は、今回の実装をバックアウトするなりなんなりで改めて問題に対処することにすればよい。
onLocationChange()
の新しい引数
Gecko 10、すなわち Firefox 10 からは、nsIWebProgressListner.onLocationChange()
に新しくロケーションの状態を示すフラグが加わった。
このフラグを用いることで、onLocationChange()
が呼び出された時が、History API やアンカーによるロケーションの変更なのか、一般的なページ遷移によるものなのかがわかる。便利ですね。
とはいえ、上述の理由により、onLocationChange()
のタイミングで表示されたログインフォームって信頼して良いのか結構疑問なところはある。
その一方で、アンカーなどを利用してページ中の「既に構築されたログインフォーム」の位置に移動するような場合を考慮すると onLocationChange()
は必要にも思える。https://twitter.com/#!/login のようなページを訪れた場合、上のディレクトリに移動した場合は、onStateChange()
ではなくonLocationChange()
が呼ばれる。利用するケースの頻度はともかく、あるに越したことはない。
だが、ページの読み込み時に構築されて、そのまま非表示か何かになっているフォームは DOMContentLoad
のタイミングで発見できるだろうし、そうでないものは結局動的生成しているものだし、「 history.pushState()
→ フォームを構築して DOM ツリーに追加」みたいな場合では、onLocationChange()
による探索はまず失敗するので、onLocationChange()
は Secure Login Rebooted からはいったん外しても問題ないと考えている。いざとなったら MutationEvent でイベントリスナ追加してどうにかすればいいだろうとは考えてる。
ちなみに nsILoginManager の、フォームへのログイン情報の自動補完は、onStateChange()
と DOMContentLoad
イベントを利用したもの( onLocationChange()
は使用していない )なので、そっちの実装に合わせる方針もありかもしれない。
onLocationChange()
をいったん削除してみた。今後の状況を見て、問題があるようならバックアウトかな。