WebExtensionsのwebNavigation.onCompletedで困ったこと

Firefoxのアドオンを作りたい

今までJVMという環境で作成してきたが、Firefoxのバージョン53以降ではどうやら完全にWebExtensionsで作成したものでなければ動かないらしい。
参考:Getting Started (jpm)

ということでちょこちょこ触ってみて備忘録を書いていこうと思う。

今回やりたいこと

ページを移動するたびにそのページの情報をconsole.logで表示したい。

手法

  • API
    ページを移動する時に情報を取りたいので、APIはWebNabigationを利用する。

  • Event
    情報を取得するタイミングはいくつか選べて、主に「navigationが開始された時」(onBeforeNavigate)、「navigationがコミットされた時」(onCommitted)、「DOMが読み込まれ始めた時」(onDOMContentLoaded)、「コンテンツの読み込みが終了した時」(onCompleted)の4つのよう。細かく言うとあと4つ程タイミングはあるようですが、詳しくはこちらを参照。webNavigation
    今回は読み込みが全て終了した時をタイミングとする。

  • 実装
    公式のドキュメント(webNavigation.onCompleted)を参考にしつつ、とりあえず以下のようなコードを作成して、backgroundのscriptとして読み込ませる。表示する情報はURLにしておく。

function logOnCompleted(details) {
  console.log("onCompleted: " + details.url);
}

browser.webNavigation.onCompleted.addListener(logOnCompleted);

そして、ブラウザーツールボックス(通常の開発ツールではないことに注意)を開き、適当なWebページに移動すると、下図のようにちゃんとログが出力されているのが確認できる。
追記:通常の開発ツールでも見れました。

"onCompleted: https://www.google.co.jp/"

しかし、ここで色々なサイトにアクセスしているうちに問題が発生した。

下のログがQiitaにアクセスした時の例である。

"onCompleted: https://staticxx.facebook.com/connect/xd_arbiter/r/lY4eZXm_YWu.js?version=42#channel=f39e3d532072bb8&origin=https%3A%2F%2Fqiita.com"  
"onCompleted: https://platform.twitter.com/widgets/tweet_button.5069e7f3e4e64c1f4fb5d33d0b653ff6.ja.html#dnt=false&id=twitter-widget-0&lang=ja&original_referer=https%3A%2F%2Fqiita.com%2F&size=m&text=Qiita%20-%20%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E%E3%81%AE%E6%8A%80%E8%A1%93%E6%83%85%E5%A0%B1%E5%85%B1%E6%9C%89%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9&time=1512561827145&type=share&url=http%3A%2F%2Fqiita.com&via=Qiita"  background02.js:2:3
"onCompleted: https://apis.google.com/se/0/_/+1/fastbutton?usegapi=1&size=medium&origin=https%3A%2F%2Fqiita.com&url=https%3A%2F%2Fqiita.com%2F&gsrc=3p&ic=1&jsh=m%3B%2F_%2Fscs%2Fapps-static%2F_%2Fjs%2Fk%3Doz.gapi.ja.msylnLBdkhc.O%2Fm%3D__features__%2Fam%3DAQ%2Frt%3Dj%2Fd%3D1%2Frs%3DAGLTcCMXw8PTgTYe2ntctyabGs_P9nnIKg#_methods=onPlusOne%2C_ready%2C_close%2C_open%2C_resizeMe%2C_renderstart%2Concircled%2Cdrefresh%2Cerefresh%2Conload&id=I0_1512561827099&_gfid=I0_1512561827099&parent=https%3A%2F%2Fqiita.com&pfname=&rpctoken=22374960" 
"onCompleted: https://cdn.api.b.hatena.ne.jp/entry/button/?url=http%3A%2F%2Fqiita.com" 
"onCompleted: https://www.google.com/recaptcha/api2/anchor?k=6LfNkiQTAAAAAM3UGnSquBy2akTITGNMO_QDxMw6&co=aHR0cHM6Ly9xaWl0YS5jb206NDQz&hl=ja&v=r20171129143447&size=normal&cb=h324yenu989z" 
"onCompleted: https://accounts.google.com/o/oauth2/postmessageRelay?parent=https%3A%2F%2Fqiita.com&jsh=m%3B%2F_%2Fscs%2Fapps-static%2F_%2Fjs%2Fk%3Doz.gapi.ja.msylnLBdkhc.O%2Fm%3D__features__%2Fam%3DAQ%2Frt%3Dj%2Fd%3D1%2Frs%3DAGLTcCMXw8PTgTYe2ntctyabGs_P9nnIKg#rpctoken=380524202&forcesecure=1"  background02.js:2:3
"onCompleted: https://www.google.com/recaptcha/api2/bframe?hl=ja&v=r20171129143447&k=6LfNkiQTAAAAAM3UGnSquBy2akTITGNMO_QDxMw6#k16ef81ev35u" 
"onCompleted: https://qiita.com/" 
"onCompleted: https://www.facebook.com/plugins/like.php?app_id=222621691108322&channel=https%3A%2F%2Fstaticxx.facebook.com%2Fconnect%2Fxd_arbiter%2Fr%2FlY4eZXm_YWu.js%3Fversion%3D42%23cb%3Df148e8534a9f534%26domain%3Dqiita.com%26origin%3Dhttps%253A%252F%252Fqiita.com%252Ff39e3d532072bb8%26relation%3Dparent.parent&container_width=397&href=https%3A%2F%2Ffacebook.com%2Flike.qiita&locale=en_US&sdk=joey&send=false&show_faces=true&width=380"

なんかいっぱい出て来た。

これはどうやら、https://qiita.com/にアクセスした際に、そのページにiframe要素として、Twitterfacebookのリンクが埋め込まれており、それを読み込んだ際にもonCompleteが反応していると考えられる。それによって、一つのWebページにしかアクセスしていないのに、複数回イベントが呼び出されてしまうということになっている。

どうにか解決できないかと色々探していたが、公式がwebNavigation.onCompletedのexample extensionとして公開しているコードに答えはあった。
webextensions-examples/navigation-stats/background.js

これを見てみると、コメントにこういうものがある。
「Filter out any sub-frame related navigation event」
見つけた時はだいぶテンションが上がったものである。

これを参考にしてコードを書き換えてみる。ついでに少しすっきりさせる。

browser.webNavigation.onCompleted.addListener(evt =>{
  if(evt.frameId !== 0){
    return;
  }
  console.log("onCompleted: " + evt.url);
});

これを再読み込みし、再度Qiitaにアクセスしてみた結果が以下である。

"onCompleted: https://qiita.com/"

成功!

要するに、アクセスしたページ本体のframeIdは0なので、それ以外を弾けば、複数回イベントが発生するという事態は回避できるということだった。

まとめ

公式のexampleはやっぱり役に立つのでちゃんと見よう。