【JavaScript】URLの変更を検知するイベントを作る

# はじめに

私はこの記事のコードの実行環境として、

* Chrome バージョン: 119.0.6045.105

を使っています。

上の実行環境以外でこの記事に書いたコードが動くかどうかはわかりません。
個人用として考えたものなので...

それでもよければどうぞ。


# カスタムイベントについて

JavaScriptには`CustomEvent`というものがあります。
そして、これを使うと自分でイベントを作ることができます。

基本的な構文は、

* `Element.dispatchEvent(new CustomEvent('イベント名'));`でイベントを発火
* `Element.addEventListener(処理)`でイベントを受け取る(?)

です。

今回は`urlChange`というイベントを作ります。


# DOMの変更を検知する

[JavascriptでURLの変更を検知したいなら、DOMの変化を検知しろ。](https://qiita.com/aki-743/items/f2ede921ae7fdeeed7c0)
という記事があるのですが、ここでもこの方法を使います。

基本的にURLが変わる時って、何かしら要素に変化があったりします。

一生`while(true)`や`setInterval`で変更を検知し続けるのはまずそうだし、
何よりスマートじゃないです。


## やり方

上の記事でも使っていますが、ここでも`MutationObserver`を使います。

`MutationObserver`を使うと、指定した要素に変更が加えられたときに
何かしらの処理をすることができます。

変更とみなされるのは下の通りです。

* 子要素の追加や削除(`childList`)
* 属性の変化(`attributes`)
* テキストノードの変化(`characterData`)

ただ、このうちのどれを検知するかは自分で指定することができます。

使い方は下の通りです。
```js
const observer = new MutationObserver(mutations => {
    // ここにDOM変更時の処理を書く
});

// 検知を開始
observer.observe(検知対象の要素, options);

// 検知を終了
observer.disconnect();
```

また、`observe`のoptionsに使えるプロパティは以下の通りです。

```js
observer.observe(target, {

    // デフォルトだと全部false
    // なんちゃらOldValueがtrueのとき、そのなんちゃらもtrueになる
    
    subtree: true, // 子孫ノードの変更を検知するか
    
    childList: true, // 子ノードの追加や削除を検知するか

    attributes: true, // 属性の変化を検知するか
    attributeFilter: ['class'], // 指定した場合、ここに入れた属性のみ検知対象になる
    attributeOldValue: true, // 変化前の値を　mutations.oldValue　に格納するか

    characterData: true, // テキストノードの変更を検知するか
    characterDataOldValue: true // 変化前の値を　mutations.oldValue　に格納するか
    // 手元で試した所、subtreeがtrueになってないと動かなかった
    
});
```

結構細かく設定できます。


## 実際に検知してみる

ということで、実際にDOMの変更を検知してみます。

```js
const observer = new MutationObserver(() => {
    // ここにDOM変更時の処理を書く
    console.log('変更を検知');
});

observer.observe(target, options); // ここをどうするか
```

まず、URLが変わったときにどこのDOMが変わるかはわからないため、
targetは`document.body`に、そしてoptionsは`{subtree: true}`にします。

```diff_javascript
const observer = new MutationObserver(() => {
    // ここにDOM変更時の処理を書く
    console.log('変更を検知');
});

- observer.observe(target, options); // ここをどうするか
+ observer.observe(document.body, {subtree: true}); // 変更
```

そして、検知する対象ですが、個人的には全部検知でいいと思います。
まあ検知対象は多い方が精度高そうだし...多分...

```diff_javascript
const observer = new MutationObserver(() => {
    // ここにDOM変更時の処理を書く
    console.log('変更を検知');
});

observer.observe(document.body, {
    subtree: true,
+   childList: true, 
+   attributes: true,
+   characterData: true
});
```

ただ、コードの実行タイミングによってはこれだと`document.body`が読み取れずにエラーが出るため、`window.addEventListener`を使います。

```diff_javascript
const observer = new MutationObserver(() => {
    // ここにDOM変更時の処理を書く
    console.log('変更を検知');
});

+ window.addEventListener('DOMContentLoaded', () => {
    observer.observe(document.body, {
        subtree: true,
        childList: true, 
        attributes: true,
        characterData: true
    });
+ });
```

この状態でもう検知できるはずです。
何かしらDOMを変更したときにログに`変更を検知`と出ていれば成功です。

ちなみに、読み込み時にもこのログは出るかもしれません。


# urlChangeイベントを作る

`new MutationObserver`に入れた関数を変更して、
`window`にイベントを発火するようにします。

```diff_javascript
const observer = new MutationObserver(() => {
-   console.log('変更を検知');
+   window.dispatchEvent(new CustomEvent('urlChange'));　// イベントを発火
});

window.addEventListener('DOMContentLoaded', () => {
    observer.observe(document.body, {
        subtree: true,
        childList: true, 
        attributes: true,
        characterData: true
    });
});

+ // 検知のテスト用
+ window.addEventListener('urlChange', () => {
+   console.log('イベントの発火を検知');
+ });
```

DOMを変更したときに、無事にログが出ていれば成功です。

ただ、今のままだとURLが変わっていなくてもイベントが発火してしまうので、
URLが変わった時だけ発火するよう少し変更します。

```diff_javascript
+ let oldUrl = ''; // URLの記憶用

const observer = new MutationObserver(() => {
+   if(oldUrl !== location.href) {
        window.dispatchEvent(new CustomEvent('urlChange'));　// イベントを発火
+       oldUrl = location.href; // oldUrlを更新
+   }
});

window.addEventListener('DOMContentLoaded', () => {
    // ~略~
});

// 検知のテスト用
window.addEventListener('urlChange', () => {
    console.log('イベントの発火を検知');
});
```

DOMとURLを変更したときに、無事にログが出ていれば成功です。
同じページ内に飛ぶaタグ(クリックしたらDOMも変わる)なんかを使うと、
ちゃんと動くかテストすることができます。

無事に動いていることが確認できたら、`// 検知のテスト用`以降のコードは消してもらってかまいません。

```diff_javascript
    // 略
});

- // 検知のテスト用
- window.addEventListener('urlChange', () => {
-   console.log('イベントの発火を検知');
- });
```

また、たまに`oldUrl !== location.href`を判定するタイミングが早すぎてうまく動かないことがあるかもしれません。
その場合は`setTimeout`なんかを使って判定のタイミングを遅らせてください。

```js
// 以下は試してません
const observer = new MutationObserver(() => {
    setTimeout(() => { // 遅延
        if(oldUrl !== location.href) {
            window.dispatchEvent(new CustomEvent('urlChange'));　// イベントを発火
            oldUrl = location.href; // oldUrlを更新
        }
    }, 50);
});

// 略
```


# 参考

https://qiita.com/aki-743/items/f2ede921ae7fdeeed7c0

https://eclair.blog/mutation-observer-api/

https://developer.mozilla.org/ja/docs/Web/API/MutationObserver

https://misc.laboradian.com/js-test/001/