# はじめに
アウトプットの一環として、Observer系のAPIを使って何か拡張機能を作りたいと思っていたときに[こちらの記事](https://qiita.com/HirosuguTakeshita/items/fd1da5f6b727fbe611e4) を見つけ、題材としては適当なのでYouTubeの広告をスキップ(非表示に)するChrome拡張機能を作ってみました。
# 作業の流れ
拡張機能を作る場合、やることは大まかに下記の2つです。
1. 実際の処理をJavaScriptで書く
2. manifest.jsonを用意
# 1.実際の処理をJavaScriptで書く
とりあえず完成したコードがこちら。
```ad-skip.js
{
const hostName = location.hostname;
const pattern = /.*\.youtube\.com/;
if( hostName.match(pattern) ) {
const newStyle = document.createElement('style');
newStyle.innerText = '.ytp-ad-overlay-container{display:none!important;}';
document.getElementsByTagName('head').item(0).appendChild(newStyle);
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 initInterval = setInterval(() => {
if( document.getElementById('ytd-player') != null ) {
const obTarget = document.getElementById('ytd-player');
observer.observe(obTarget, obConfig);
clearInterval(initInterval);
} else {
clickSkipButton();
}
}, 1000);
}
};
```
## コードの解説
### 拡張機能が実行される範囲を指定
```ad-skip.js
const hostName = location.hostname;
const pattern = /.*\.youtube\.com/;
if( hostName.match(pattern) ) {
...
}
```
拡張機能が実行される範囲を「*.youtube.com」ドメイン内に指定しています。
「YouTube Music」にも対応させるための記述です。
### オーバーレイ広告を非表示
```ad-skip.js
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での対応としました。
### スキップボタンをクリックする処理を定義
```ad-skip.js
const clickSkipButton = () => {
const $skipButton = document.getElementsByClassName("ytp-ad-skip-button-container")[0];
if( $skipButton ) $skipButton.click();
};
```
Youtubeでは広告の再生時、動的に「広告をスキップ」ボタン(class名:`ytp-ad-skip-button-container`)が生成されます。
この要素が存在するときにスキップボタンをクリックする`clickSkipButton()`を定義します。
### MutationObserverを設定
```ad-skip.js
const obConfig = {
childList: true, // 監視対象ノードの子ノードの追加・削除を監視します。
subtree: true // 監視対象ノードの子孫ノードも監視対象とします。
};
```
インスタンスメソッド`observe()`に渡すオプションを設定します。
```ad-skip.js
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()`を実行します。
### ポーリングを用意
```ad-skip.js
const initInterval = setInterval(() => {
if( document.getElementById('ytd-player') != null ) {
const obTarget = document.getElementById('ytd-player');
observer.observe(obTarget, obConfig);
clearInterval(initInterval);
} else {
clickSkipButton();
}
}, 1000);
```
setIntervalで1秒毎に`clickSkipButton()`を繰り返し実行すると同時に、監視対象の存在をチェックしています。
監視対象が存在した場合Observerを実行し監視開始、1秒毎の繰り返し処理をキャンセルさせます。
本来このような処理は不必要なのですが、拡張機能が実行されるタイミングに起因する問題への対応としてこのような処理が必要になります。(詳細は後述)
# 2.manifest.jsonを用意する
これまでに記述した処理をブラウザに拡張機能として認識させるために`manifest.json`を用意します。
処理を記述したJavaScriptと同じディレクトリに作成します。
```manifest.json
{
"manifest_version": 2, // マニフェストファイルのバージョン(現在有効なバージョンは2のみ)
"name": "yt-ad-skip",
"version": "1.1.0",
"description": "Sample Extension",
"content_scripts": [{
"matches": ["https://*.youtube.com/*"], // 拡張機能を実行するサイトを設定
"run_at": "document_idle", // 拡張機能の実行タイミングを設定
"js": [
"yt-ad-skip.js" // JavaScriptファイルのパスを指定
]
}]
}
```
必須項目は`manifest_version`、`name`、`version`のみです。
その他は適宜、記述します。
# ブラウザに拡張機能としてインストール
`ad-skip.js`と`manifest.json`を適当なフォルダに入れ、Chrome拡張設定`chrome://extensions/`の「パッケージされていない化されていない拡張機能を読み込む」から読み込めばインストール完了です。
# 拡張機能が実行されるタイミングの問題
ポーリングが必要な理由がこの「拡張機能実行タイミングの問題」です。
この問題のポイントは「MutationObserverの監視対象が、YouTube内で動的に生成される要素である」にもかかわらず、「動的な要素が生成されてから拡張機能(MutationObserver)を実行する」ことが出来ないことです。
前述の`manifest.json`には拡張機能の実行タイミングを設定するプロパティがありますが、このプロパティを設定しても問題解決には至りません。
説明の前に、YouTubeのページ読み込みの流れを記載します。
## YouTubeでの読み込みの流れ
0. HTMLのロード
1. HTMLをパース
1 - 1. JavaScriptをロード
1 - 2. JavaScriptをパース・実行
1 - 3. CSSをロード
1 - 4. CSSをパース
1 - 5. CSSOMを構築
2. DOMの構築(DOMContentLoaded)
3. 1-2 で実行されたJavaScriptが要素を追加、DOMを再構築
4. 画像やフレームなどを読み込み
5. ページの読み込み完了(window.onload)
注)便宜上、上記の順番付けをしていますが、実際には各ステップが同時に実行されたり、前ステップの途中で次のステップが実行されたりします。
## manifest.jsonで拡張機能の実行タイミングを設定した場合
`manifest.json`の`run_at`プロパティで設定できる値は下記のとおりです。
| 設定値 | 説明 | 実行タイミング | 備考 |
| :--------------: | ----- | -------------- | ----- |
| document_idle | ブラウザが、`document_end`から`window.onload`イベントが発生した直後までの間に拡張機能を実行するタイミングを選択します。<br>拡張機能を実行する正確なタイミングは、ドキュメントの複雑さと読み込みにかかる時間に依存し、ページの読み込み速度に合わせて最適化されます。<br><br>`document_idle`で実行されるコンテンツ拡張機能は、`window.onload`イベントを待機する必要はなく、DOMが完了した後に実行されることが保証されています。`window.onload`の後に拡張機能を実行する必要がある場合、拡張機能は`document.readyState`プロパティを使用して、`onload`が既に発生しているかどうかをチェックすることができます。 | `2`〜`5`の間で実行されます。 | 初期値/推奨 |
| document_start | 拡張機能はcssファイルの後に実行されますが、他のDOMが構築されたり他のスクリプトが実行される前に実行されます。 | `1-5`の直後に実行されます。<br>検証ではDOMが構築される前に拡張機能が実行されていました。 | - |
| document_end | 拡張機能は、DOMが完成した直後に実行されますが、画像やフレームなどのサブリソースがロードされる前に実行されます。 | `2`〜`4`の間で実行されます。 | - |
参考URL:[Content Scripts - Google Chrome](https://developer.chrome.com/extensions/content_scripts#run_time)
初期値/推奨値である`document_idle`を指定した場合「`document_idle`で実行されるコンテンツ拡張機能は、`window.onload`イベントを待機する必要はなく」とあるように、`window.onload`や`addEventListener load`の記述があっても、最終的なDOMが構築される前に実行される可能性があります。
結論:ポーリング用意する
# 参考URL
- [課金なんて必要ない?YouTubeの広告自動スキップ&バナー自動削除のChrome拡張作ってみた](https://qiita.com/HirosuguTakeshita/items/fd1da5f6b727fbe611e4)
- [動的なページの読み込みが完了してからChrome拡張機能を実行する方法](https://qiita.com/3mc/items/c3c580ca5de4a2d3990d)