はじめに
アウトプットの一環としてObserver系のAPIを使って何か拡張機能を作りたいと思っていたので、YouTubeの広告をスキップするChrome拡張機能を作り、Chromeウェブストアに公開しました。
作ったもの:YT Ad Skip
作業の流れ
拡張機能の公開までにやったことは大まかに3つです。
- 処理をJavaScriptで書く
- manifest.jsonを用意
- Chromeウェブストアに公開申請
1.処理をJavaScriptで書く
完成したコードがこちらです。
{
const hostName = location.hostname;
const subDomain = hostName.split('.')[0];
const newStyle = document.createElement('style');
newStyle.innerText = '.ytp-ad-overlay-container{display:none!important;}';
document.getElementsByTagName('head').item(0).appendChild(newStyle);
const returnPlayerId = (v) => {
switch (v) {
case 'music':
return 'player';
break;
case 'www':
default:
return 'ytd-player';
break;
}
};
const clickSkipButton = () => {
const $skipButton = document.getElementsByClassName("ytp-ad-skip-button-container")[0];
if( $skipButton ) $skipButton.click();
};
const obConfig = {
childList: true,
subtree: true
};
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if( mutation.addedNodes.length && mutation.addedNodes[0].className === 'ytp-ad-player-overlay' ) clickSkipButton();
});
});
const playerId = returnPlayerId(subDomain);
const initInterval = setInterval(() => {
if( document.getElementById(playerId) != null ) {
const obTarget = document.getElementById(playerId);
observer.observe(obTarget, obConfig);
clearInterval(initInterval);
}
clickSkipButton();
}, 1000);
};
コードの解説
サブドメインを取得
const hostName = location.hostname;
const subDomain = hostName.split('.')[0];
「YouTube」と「YouTube Music」で処理を分岐させるため、サブドメインを取得しています。
オーバーレイ広告を非表示
const newStyle = document.createElement('style');
newStyle.innerText = '.ytp-ad-overlay-container{display:none!important;}';
document.getElementsByTagName('head').item(0).appendChild(newStyle);
オーバーレイ広告を非表示にするCSSをhead要素に追加しています。
Observerを使うという趣旨から外れるのですが、とりあえずはCSSで対応しました。
監視対象のid名を返す処理を定義
const returnPlayerId = (v) => {
switch (v) {
case 'music':
return 'player';
break;
case 'www':
default:
return 'ytd-player';
break;
}
};
サブドメインで条件分岐させ、監視対象であるプレイヤーのid名を返します。
スキップボタンをクリックする処理を定義
const clickSkipButton = () => {
const $skipButton = document.getElementsByClassName("ytp-ad-skip-button-container")[0];
if( $skipButton ) $skipButton.click();
};
広告の再生時、「広告をスキップ」ボタン(class名:ytp-ad-skip-button-container
)が動的に生成されます。
スキップボタンが存在するときに要素をクリックするclickSkipButton()
を定義します。
MutationObserverを設定
const obConfig = {
childList: true, // 監視対象ノードの子ノードの追加・削除を監視します。
subtree: true // 監視対象ノードの子孫ノードも監視対象とします。
};
インスタンスメソッドobserve()
に渡すオプションを設定します。
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if( mutation.addedNodes.length && mutation.addedNodes[0].className === 'ytp-ad-player-overlay' ) clickSkipButton();
});
});
MutationObserverをインスタンス化。
Callbackとして、スキップボタンのラッパー(class名:ytp-ad-player-overlay
)が追加されたときclickSkipButton()
を実行します。
ポーリングを用意
今回の場合、MutationObserverでの監視対象はYouTube側が動的に生成している要素です。
場合によっては要素が生成される前にMutationObserverが実行される(拡張機能が実行される)ことがあるため、その対応としてポーリングを用意しています。
※拡張機能が実行されるタイミングは自分で指定できたりするんですが、その辺は後述します。
const playerId = returnPlayerId(subDomain);
const initInterval = setInterval(() => {
if( document.getElementById(playerId) != null ) {
const obTarget = document.getElementById(playerId);
observer.observe(obTarget, obConfig);
clearInterval(initInterval);
}
clickSkipButton();
}, 1000);
監視対象のid名を変数に入れ、setIntervalで1秒毎にclickSkipButton()
を繰り返し実行すると同時に、監視対象の存在をチェックしています。
監視対象が存在した場合Observerを実行し監視開始、ポーリングをストップします。
2.manifest.jsonを用意する
これまでに記述した処理をブラウザに拡張機能として認識させるためにmanifest.json
を用意します。
処理を記述したJavaScriptと同じディレクトリに作成します。
{
"manifest_version": 2, // マニフェストファイルのバージョン(現在有効なバージョンは2のみ)
"name": "YT Ad Skip",
"version": "1.3.0",
"description": "This plugin will skip YouTube ads.",
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
},
"content_scripts": [{
"matches": ["https://*.youtube.com/*"], // 拡張機能を実行するサイトを設定
"js": ["yt-ad-skip.js"] // JavaScriptファイルのパスを指定
}]
}
必須項目はmanifest_version
、name
、version
のみです。
その他は適宜記述します。
拡張機能が実行されるタイミングについて
拡張機能が実行されるタイミングを説明する前提として、YouTubeでの読み込みフローは以下のようになっています。
- HTMLのロード
- HTMLをパース
- JavaScriptをロード
- JavaScriptをパース・実行
- CSSをロード
- CSSをパース
- CSSOMを構築
- 2-2 で実行されたJavaScriptが要素を追加~~、DOMを再構築~~
- DOMの構築(DOMContentLoaded)
- 画像やフレームなどを読み込み
- ページの読み込み完了(load)
※便宜上上記の順番付けをしていますが、実際には各ステップが同時に実行されたり、前ステップの途中で次のステップが実行されたりします。
実行するタイミングを指定する
拡張機能が実行されるタイミングを指定するには2つの方法があります。
- JavaScriptで指定
-
manifest.json
で指定
1.JavaScriptで指定
DOMContentLoadedやload、onreadystatechange等のイベントによってタイミングを指定します。
2.manifest.json
で指定
manifest.json
のrun_at
プロパティでタイミングを指定します。
{
"content_scripts": [{
"run_at": "document_idle",
}]
}
run_at
プロパティに設定できる値は以下のとおりです。
設定値 | 説明 | 実行タイミング | 備考 |
---|---|---|---|
document_idle | ブラウザが、document_end からwindow.onload イベントが発生した直後までの間に拡張機能を実行するタイミングを選択します。拡張機能を実行する正確なタイミングは、ドキュメントの複雑さと読み込みにかかる時間に依存し、ページの読み込み速度に合わせて最適化されます。 document_idle で実行されるコンテンツ拡張機能は、window.onload イベントを待機する必要はなく、DOMが完了した後に実行されることが保証されています。window.onload の後に拡張機能を実行する必要がある場合、拡張機能はdocument.readyState プロパティを使用して、onload が既に発生しているかどうかをチェックすることができます。 |
4 〜6 の間で実行されます。 |
初期値/推奨 |
document_start | 拡張機能はCSSファイルの後に実行されますが、他のDOMが構築されたり他のスクリプトが実行される前に実行されます。 |
2-5 の直後に実行されます。 |
- |
document_end | 拡張機能は、DOMが完成した直後に実行されますが、画像やフレームなどのサブリソースがロードされる前に実行されます。 |
4 〜5 の間で実行されます。 |
- |
実際に実行されるタイミング
前述のように拡張機能が実行されるタイミングを指定しても、動的な要素が生成されてから実行されるという保証はありません。
YouTubeの読み込みフローでいう6
の後に要素が生成されることもあり、それはloadイベントやmanifest.json
だけでは対応できません。
そのため、ポーリングを用意して監視対象の存在をチェックする必要があります。
3.Chromeウェブストアに公開申請
JavaScriptとmanifest.json
の他にアイコン画像を用意したら、あとは公開申請を出すだけです。
細かい手順はこちらを参考にしました。
- 開発者アカウントを作成($5支払う必要があります)
- 必要ファイルをzipにして開発者アカウントのダッシュボードからアップロード
- 拡張機能の名前や説明を入力して申請
審査が通ればChromeウェブストアに自作の拡張機能が追加されます。
作ってみて所感
巷の拡張機能を見て「作った人すげぇ」とか思ってましたが、公開するだけなら意外と簡単でした。
技術的に悩んだところもあり、拡張機能が実行されるタイミングの部分はレンダリングについても理解する必要があって、色々と調べてみて知見が広がりました。
Observer系のアウトプットとしてはじめましたが、拡張機能の作り方についても学ぶことができプラスになったと思います。