【個人開発】何が何でも魚拓を見つけるChrome拡張を作った

##デモ
↓2019/03/02
![キャプチャ4.PNG](https://qiita-image-store.s3.amazonaws.com/0/190721/475d2745-ddfa-3a03-6e04-748d83161711.png)
↓1998/11/11
![キャプチャ5.PNG](https://qiita-image-store.s3.amazonaws.com/0/190721/f213c106-c7b7-7987-ad7a-a6173dc910bb.png)

##特徴
- archive.is, archive.org, gyo.tc, googleのキャッシュ、と計**四か所**から魚拓のURLを探します!
- vue.js製（やってみたかった）

##なぜ作ったか
ハテブで魚拓コメがトップだったりして、需要を感じた。
後普通に自分が使いたかった。
そして、Qiitaユーザーの皆さんも必要性を感じてくださっている事と思います^^。

##どう作ったか
Chorome拡張はHTML+CSS+javascriptの基本セットで作れます。
ファイル構成は以下のようになりました。

- manifest.json（どの拡張でも必須）
- popup.js
- popup.html
- popup.css
- jquery
- vue.js
- icons(フォルダ。配下にicon16.png, icon48.png, icon128.png)

詳しく見ていきましょう。
manifest.jsonは、Chrome拡張のまとめ役みたいな感じ。

```json:manifest.json
{
    "name": "FindGyotaku in Console",
    "version": "0.0.1",
    "manifest_version": 2,//2で固定する
    "permissions": ["tabs", "http://*/", "http://*/"],//tabsはアクティブなタブのURLを取得するため、http://*/,https://*/はChrome拡張からすべてのサイトへのアクセスを許可するために必要
    "description": "Show all Gyotaku",
    "icons": {
        "16": "icons/icon16.png",
        "48": "icons/icon48.png",
        "128": "icons/icon16.png"
    },
    "browser_action": {
        "default_popup": "popup.html"//ここでpopupのhtmlを指定
    }
}
```
そして、popup.jsが今回の大事ポイントです。

```javascript:popup.js
let vue;
//https://gyo.tc/から
function findGyotakuByUrl(url) {
    return new Promise(resolve => {
        console.log("findGyotakuByUrl Started");
        $.get("https://gyo.tc/" + url, null,
            function (data) {
                console.log(data);
                let gyotakuList_ = new DOMParser()
                    .parseFromString(data, "text/html")
                    .querySelectorAll("[id^=fish]");
                resolve(gyotakuList_)
            },
            "html"
        );
    })
}
//https://archive.is/から
function findInternetArchiveByUrl(url) {
    return new Promise(resolve => {
        $.get(`https://archive.is/${url}`, function (data) {
            let rowsList =
                new DOMParser()
                    .parseFromString(data, "text/html")
                    .querySelectorAll("[id^=row]");
            let archivesList = [];
            rowsList.forEach(row => {
                archivesList.push(
                    Array.from(
                        row.querySelectorAll("[style^=text\\-decoration\\:none]")
                    )
                )
            });
            resolve(archivesList.flat());
            console.log(`flatten archivesList is ${archivesList.flat()}`);
        })
    })
}
//googleのwebcacheから
function findGoogleCache(url) {
    return new Promise(resolve => {
        $.get(`http://webcache.googleusercontent.com/search?q=cache:${url}`, function () {
            resolve(`http://webcache.googleusercontent.com/search?q=cache:${url}`);
        })
    })
}
//http://web.archive.orgから
function findWaybackMachineLatest(url) {
    return new Promise(resolve => {
        //limit=-1で最新
        $.get(`http://web.archive.org/cdx/search/cdx?limit=-1&fl=timestamp&url=${url}`, function (timestamp) {
            resolve(`https://web.archive.org/web/${timestamp}/${url}`);
            console.log(`waybackMachine response(latest) is ${timestamp}`);
        })
    })
}
//http://web.archive.orgから
function findWaybackMachineOldest(url) {
    return new Promise(resolve => {
        //limit=1で最古
        $.get(`http://web.archive.org/cdx/search/cdx?limit=1&fl=timestamp&url=${url}`, function (timestamp) {
            resolve(`https://web.archive.org/web/${timestamp}/${url}`);
            console.log(`waybackMachine response(latest) is ${timestamp}`);
        })
    })
}

console.log("popup.jsLoaded");

chrome.tabs.query({
    active: true,
    currentWindow: true
}, function (tabsArray) {
    console.log(`currentUrl is ${tabsArray[0].url}`);

    findGyotakuByUrl(tabsArray[0].url).then((gyotakuList) => {
        console.log(gyotakuList);
        gyotakuList.forEach(element => {
            vue.items.push(element)
        });
        vue.isLoadingGyotaku = false
    }).catch((err) => {
        console.log(err);
        vue.isLoadingGyotaku = false
    });
    findInternetArchiveByUrl(tabsArray[0].url).then((archivesList) => {
        console.log(archivesList);
        archivesList.forEach(element => {
            vue.items.push(element)
        });
        vue.isLoadingArchives = false
    }).catch((err) => {
        console.log(err);
        vue.isLoadingArchives = false
    });
    findGoogleCache(tabsArray[0].url).then((googleCacheUrl) => {
        console.log({
            href: googleCacheUrl
        });
        vue.items.push({
            href: googleCacheUrl
        });
        vue.isLoadingGoogleCache = false
    }).catch((err) => {
        console.log(err)
    });
    findWaybackMachineLatest(tabsArray[0].url).then((waybackMachineUrl) => {
        console.log({
            href: waybackMachineUrl
        });
        vue.items.push({
            href: waybackMachineUrl
        });
        vue.isLoadingWaybackMachine = false
    }).catch((err) => {
        console.log(err)
    });
    findWaybackMachineOldest(tabsArray[0].url).then((waybackMachineUrl) => {
        console.log({
            href: waybackMachineUrl
        });
        vue.items.push({
            href: waybackMachineUrl
        });
        vue.isLoadingWaybackMachine = false

    }).catch((err) => {
        console.log(err)
    });

});

$(function () {
    vue = new Vue({
        el: '#example-1',
        data: {
            items: [],
            isLoadingGyotaku: true,
            isLoadingArchives: true,
            isLoadingGoogleCache: true,
            isLoadingWaybackMachine: true,
        }
    })

});
```
分割して解説します。

```javascript
function findGyotakuByUrl(url) {
    return new Promise(resolve => {
        $.get("https://gyo.tc/" + url, null,
            function (
                let gyotakuList_ = new DOMParser()
                    .parseFromString(data, "text/html")
                    .querySelectorAll("[id^=fish]");
                resolve(gyotakuList_)
            },
            "html"
        );
    })
}
```
↑要するにスクレイピングですね。javascriptではquerySelectorが便利です。
https://gyo.tc/ 以下にURLを投げると魚拓一覧が返るので、開発者ツールから頑張って解析しました。

```javascript
function findInternetArchiveByUrl(url) {
    return new Promise(resolve => {
        $.get(`https://archive.is/${url}`, function (data) {
            let rowsList =
                new DOMParser()
                    .parseFromString(data, "text/html")
                    .querySelectorAll("[id^=row]");
            let archivesList = [];
            rowsList.forEach(row => {
                archivesList.push(
                    Array.from(
                        row.querySelectorAll("[style^=text\\-decoration\\:none]")
                    )
                )
            });
            resolve(archivesList.flat());
        })
    })
}
```
↑こちらもhttps://archive.is/ 以下にURLを投げると魚拓一覧が返るので、こちらも開発者ツールから頑張って解析しました。
Array.flat()は[MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/flat)で分かるように実験的なヤツで、配列を好きな次元に均せる関数です。デフォルトで一次元なのでそのまま使用しました。

```javascript
function findGoogleCache(url) {
    return new Promise(resolve => {
        $.get(`http://webcache.googleusercontent.com/search?q=cache:${url}`, function () {
            resolve(`http://webcache.googleusercontent.com/search?q=cache:${url}`);
        })
    })
}
```
↑言うことなし。思ったより簡単。

```javascript
function findWaybackMachineLatest(url) {
    return new Promise(resolve => {
        //limit=-1で最新
        $.get(`http://web.archive.org/cdx/search/cdx?limit=-1&fl=timestamp&url=${url}`, function (timestamp) {
            resolve(`https://web.archive.org/web/${timestamp}/${url}`);
        })
    })
}

function findWaybackMachineOldest(url) {
    return new Promise(resolve => {
        //limit=1で最古
        $.get(`http://web.archive.org/cdx/search/cdx?limit=1&fl=timestamp&url=${url}`, function (timestamp) {
            resolve(`https://web.archive.org/web/${timestamp}/${url}`);
            console.log(`waybackMachine response(latest) is ${timestamp}`);
        })
    })
}
```
↑apiが有って助かりました。
WaybackMachineのアーカイブは膨大なので最古＆最新のみ取得しています。
limit=1で最古、limit=-1で最新を指定、fl=timestampでレスポンスをtimestampのみに限定します。
WaybackMachineはtimestampとURLを指定すると（珍しく）個別ページのURLが分かるので、timestampだけで十分なのです。

```javascript
$(function () {
    vue = new Vue({
        el: '#example-1',
        data: {
            items: [],//以下デフォルト値を指定しています
            isLoadingGyotaku: true,
            isLoadingArchives: true,
            isLoadingGoogleCache: true,
            isLoadingWaybackMachine: true,
        }
    })

});
```
```html:index.html
<p v-if="isLoadingGyotaku">
    魚拓から読み込んでいます
</p>
```
↑出ましたvue.js！読み込み中のメッセージを出しています。
vue.jsを使うと、HTMLとjs間でバインディングができます。
例えば、

HTMLのv-if属性にisLoadingGyotakuを指定
↓
js側でvueインスタンス作成(vueのdata属性に、isLoadinGyotakuをメンバにしたオブジェクトを付けたもの)
↓
vue.isLoadingGyotakuにアクセスできるように&
vue.isLoadingGyotakuがv-if="isLoadingGyotaku"とバインドされる

って感じ。
つまり、isLoadingGyotaku=trueならpタグが見えて、falseなら見えなくなる！
これは素晴らしいですね。本当に素晴らしい。
ほかにもいろいろな属性が有ります。だいたいv-が付く。

```html
<ul>
    <li v-for="item in items">
        <a href="{{ item.href }}" target='_newtab'>
            {{ item.href }}
        </a>
    </li>
</ul>
```
↑v-forとか。見つかったURLを表示するために使っています。
{{}}はテンプレートリテラルみたいに使えます。
↓ラスト

```javascript
chrome.tabs.query({
    active: true,
    currentWindow: true
}, function (tabsArray) {
    console.log(`currentUrl is ${tabsArray[0].url}`);

    findGyotakuByUrl(tabsArray[0].url).then((gyotakuList) => {
        console.log(gyotakuList);
        gyotakuList.forEach(element => {
            vue.items.push(element)

        });
        vue.isLoadingGyotaku = false
    }).catch((err) => {
        console.log(err);
        vue.isLoadingGyotaku = false
    });
    findInternetArchiveByUrl(tabsArray[0].url).then((archivesList) => {
        console.log(archivesList);
        archivesList.forEach(element => {
            vue.items.push(element)
        });
        vue.isLoadingArchives = false
    }).catch((err) => {
        console.log(err);
        vue.isLoadingArchives = false
    });
    findGoogleCache(tabsArray[0].url).then((googleCacheUrl) => {
        console.log({
            href: googleCacheUrl
        });
        vue.items.push({
            href: googleCacheUrl
        });
        vue.isLoadingGoogleCache = false
    }).catch((err) => {
        console.log(err);
        vue.isLoadingGoogleCache = false
    });
    findWaybackMachineLatest(tabsArray[0].url).then((waybackMachineUrl) => {
        console.log({
            href: waybackMachineUrl
        });
        vue.items.push({
            href: waybackMachineUrl
        });
        vue.isLoadingWaybackMachine = false
    }).catch((err) => {
        console.log(err)
        vue.isLoadingWaybackMachine = false
    });
    findWaybackMachineOldest(tabsArray[0].url).then((waybackMachineUrl) => {
        console.log({
            href: waybackMachineUrl
        });
        vue.items.push({
            href: waybackMachineUrl
        });
        vue.isLoadingWaybackMachine = false

    }).catch((err) => {
        console.log(err)
        vue.isLoadingWaybackMachine = false
    });

});
```
↑async/awaitで見やすくなるのは分かってるんだけどね……面倒なのでそのまま。
chrome.tabs.queryで今見てるタブのURLを取得後、非同期×5を回しています。
chrome.系はChrome拡張の時しか使えないapiで、ほかにも色々便利です。

##はまったポイント
最初

```javascript
let fuga = true
vue = new Vue({
        el: '#example-1',
        data: {
            isLoadingGyotaku: fuga,
        }
    })
fuga = false//バインドされてるはずなのに動かない!!!
```
って事が有って大変でしたね。本当は

```javascript
vue = new Vue({
        el: '#example-1',
        data: {
            isLoadingGyotaku: true,
        }
    })
vue.isLoadingGyotaku = false//バインドされてて動く!!!
```
で良かったっていう。公式ドキュメントをちゃんと読みましょう。

後、動作確認にevent pagesを使ってしまって、popupのみで動くapiを動かせなかったりとか……
popupページで検証する方法を知らなかったとか……（左クリックでポップアップされたのを右クリ）

計何時間使ったでしょうかね。今はどうでも良いけど。

##最後に

2020/04に大学生になる(浪人しなければ!)ので、良かったらインターンに誘ってください。
github:https://github.com/negitorogithub
gmail:bakeratta35@gmail.com

お読み頂きありがとうございました。　～～旦⊂(･∀･ )