Yammer の非同期に追加・更新されるエレメントに対してユーザースクリプトを適用するには

やりたいこと

Chrome Extensions の Content Scripts で、画面の改変を行いたい場合、静的なエレメントを対象にするのであれば run_atdocument_enddocument_idle を指定すればよさそうです。

Content Scripts - chrome

In the case of "document_end", the files are injected immediately after the DOM is complete, but before subresources like images and frames have loaded.

In the case of "document_idle", the browser chooses a time to inject scripts between "document_end" and immediately after the window.onload event fires. The exact moment of injection depends on how complex the document is and how long it is taking to load, and is optimized for page load speed.

Yammer は全ての遷移が非同期に行われる

Yammer は閲覧するグループの変更や inbox への移動等も全て非同期に行われています。さらに History API で URL も変更されていっています。

このような画面においては、最初の読み込み後の改変はうまく行っても、それ以降が追随できません。

ただ、Yammer に関しては初回の読み込みすらも非同期なので run_at の指定だけではうまくいきません。

試したこと

  1. popstate イベントを補足する
  2. hashchange イベントを補足する
  3. chrome.webNavigation イベントを補足する (chrome.webRequest イベントも考えてみた)
  4. MutationObserver を利用する

popstate イベント

Yammer が History API を使っているようだったので popstate で画面の変更を検知して、改変をかけ直す作戦を取ってみましたが、 Yammer が pushState をするタイミング的に画面の状態が次の画面に変わっていないので意味がありませんでした 😅

developer.mozilla.org

hashchange イベント

冷静に考えたら今回のケースだと popstate とタイミング同じなんですけど、試してやっぱりダメでした。。

developer.mozilla.org

chrome.webNavigation イベント

Chrome Extensions の方の API ならうまいことサポートしてくれないかなと考え試してみました。

onDOMContentLoadedonCompleted あたりが、画面の更新毎に拾えると更新後の DOM に対して改変できていいなと期待したのですが、初回読み込み時に非同期に追加されるエレメント追加は全て拾ってくれたのですが、閲覧グループ変更等のイベント時には onReferenceFragmentUpdated しか発生せずで、DOM 読み込み以降のイベントは拾えませんでした 😲

chrome.webNavigation - Google Chrome

chrome.webRequest という API も用意されており、こちらは非同期リクエストのレスポンスを受け取ったところまで補足できるのですが、いずれにしろ更新後の画面の DOM が構築された後は無理そうだったので今回は試さず。

chrome.webRequest - Google Chrome

ちなみに webNavigation は Content Scripts では実行できない

という制約があるので Event Page (または Background Page)で webNavigation の補足を実行して、Content Scripts にメッセージを飛ばす必要があります。

// background.js
chrome.webNavigation.onCompleted.addListener(function(data) {
  // Event Page から Content Scripts にメッセージを飛ばすには tabId を指定する必要があるので、アクティブなタブを取得してから
  chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
    chrome.tabs.sendMessage(tabs[0].id, {webNavigationEventType: 'onCompleted'}, function(response) {
      
    });
  });
});

// content_scripts.js
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
  if(request.webNavigationEventType == 'onCompleted') {
    // ここで DOM を改変できたらよかったな。
  }
});

MutationObserver

元々、スクロール自動読み込みに対応するにはこれを使うかなぁと思っていたのですが、結果として画面の更新に対しても MutationObserver を使うのが良さそうでした。

以下の例は(乱暴ですが) body 配下全部監視して、追加されたノードに querySelector をかけて改変したい Yammer のアクションボタンがあればログに出すというものです。

// NOTE body 配下の .yj-actions のエレメント(yammer のアクションボタン)の追加を監視するようにした
var selector = '.yj-message-list-item--action-list.yj-actions';

var observer = new MutationObserver(function (mutations) {
  mutations.forEach(function (mutation) {
    for (var i = 0; i < mutation.addedNodes.length; i++) {
      var addedNode = mutation.addedNodes[i];

      if (typeof addedNode.querySelector === "function") {
        if (addedNode.querySelector(selector)) {
          console.log('MutationObserver : addedNode matches');
        }
      }
    }
  });
});

observer.observe(document.body, {
  childList: true, subtree: true
});

これをやってみて気付きましたが、Yammer って、閲覧グループを移動して、画面からエレメントが消えてもキャッシュしてるんですね。直前で閲覧していたグループに再度移動しても、ノードが一切追加されませんでした。(どれぐらいキャッシュしてるのか分からないですが、改変したエレメントのライフサイクルは注意しないといけないっぽい)

参考

いろいろやってみて

Chrome Extensions には、まだまだ使って事ない API あるなぁっていうのを実感。(後、公式ドキュメントが充実していて助かる)

後、 MutationObserver 便利。

ただイベント拾ってログ吐いてるだけですが、試したコードは以下においています。Chrome Extensions 形式になっているので、実際に試すことができます。

github.com