Qiitaでユーザーをブロック（非表示に）する拡張機能を作った

# 経緯
率直に言うとGW中に**何か**作りたかった。
一応**Web開発エンジニア**なのですが、ずっとPythonでデータを加工している為に**JS未経験**という致命的な経歴を何とかしたい。

### 何が欲しいか
エンジニアになってから毎朝Qiitaのランキングを見る習慣を付けているのですが、さすがに1年間毎日記事を見ていると心ない言葉を発している人がちらほら見受けられます。
Qiita運営は素晴らしく対応が迅速なので報告後10分も経てばコメントが消えていることもあります。
ですが私の気持ち的にはそのような人とは金輪際目にもせず関わらないようにしたいので、拡張機能を作りました。
**嫌だから見ない**の精神です。

### でもQiitaってミュート機能あるよね
Qiitaのミュート機能は「コメント」「ランキング」「おすすめユーザー」等を非表示にすることができません。
「金輪際、目にもせず関わらない」が目的なのですべての表示から抹消することをゴールとしました。
（そもそもランキングに入るような人は問題ないと思いますが非表示対象に入れておきました。）

あと、ミュートしに行かなくても簡単に非表示にしたい。

### Qiitaのガイドライン、利用規約に違反しない？
一応自身で確認しましたが怪しい規約はこの一文でしょうか。

[Qiita | サービス利用規約](https://qiita.com/terms)
>本サービス内でのページデザイン変更により、当社が標準的に表示しているヘッダ、フッタ、広告及び著作権表示を当社の許諾なく非公開にする行為

ヘッダ、フッタ、広告、著作権の非表示は本拡張機能で行うことができないので規約には反していないと思います。

# 開発
### 開発環境
* Windows
* WebStorm
* jQuery v3.4.0

### 開発期間
2日

勉強ついでに**Vue**や**React**で作成しようと思いましたが、jsの仕様や特徴を理解したかった為、今回は使用しませんでした。

## Chrome Extension（拡張機能）の作り方

参考：[developer.chrome.com | What are extensions?](https://developer.chrome.com/extensions)

chromeの拡張機能は、ウェブサイトと同じで**HTML**、**CSS**、**JavaScript**で構成されています。
**拡張機能**という名前の通り、ウェブサイトの**機能**を**拡張**することができます。

詳しくは下記の記事がとても参考になりました。
[Chrome Extension を作って公開する](https://qiita.com/sqrtxx/items/19fd2114430e9e1fb57f)
[Chromeの拡張機能ってどうやって作るの？開発ガイドライン&作り方をまとめた](https://goworkship.com/magazine/chrome-extension-basic-build/)

あと、Google公式のサンプル集があります、一番参考になりました。
[developer.chrome.com  | Sample Extensions](https://developer.chrome.com/extensions/samples)

今回使用した、拡張機能で必要なファイルなどは下記で解説しています。

# 作ったもの
### [nGiita | エヌジータ](https://chrome.google.com/webstore/detail/ngiita/aooffpdldnppkckpjjpdbllpfgabkppd/related?hl=ja&authuser=0)
QiitaとNGを掛け合わせてこのようなネーミングになりました。
（ネーミングセンスはお察しの通りです。）
![reIOMdkRZxdmHDe1556709202_1556709223.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256196/bd165f19-c6b2-df40-0052-d9a08c32baf6.jpeg)


ブロックリストに追加したユーザーをQiita配下のすべてのページで非表示にします。
※Recommendedに関しては利用規約に引っ掛かりそうなので非表示にしていません。


## ソースとか必須ファイルの解説
Githubにて公開しています。
[Github/s-tyd | nGiita](https://github.com/s-tyd/nGiita)
※html、css、popup.jsのソースは省きます。

### ディレクトリ構成
>nGiita
├ manifest.json
├ popup.html
├ css.css
├ js
│ ├ popup.js
│ ├ content.js
│ └ jquery.min.js
└ icons
　 ├ 16.png
　 ├ 48.png
　 └ 128.png

軽量の拡張機能でこんな感じになると思います。

#### popup.html / css.css / popup.js
拡張機能のボタンを押下した時に表示される、画面を構築します。
Google翻訳の画面だとこれ
![bandicam 2019-05-03 00-56-02-835.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256196/3d70c77d-5f34-4bc7-0439-50119e603fd9.jpeg)
先ほど言ったようにHTML、CSS、JSで構成されています。


## manifest.json
manifest.jsonとはChrome Extensionを開発する際に必須となるものです。
拡張機能の情報が内装されています。

```json:manifest.json
{
  "manifest_version": 2,
  "name": "nGiita",
  "version": "1.1",
  "description": "Hide users in Qiita",
  "permissions": [
    "storage"
  ],
  "browser_action": {
    "default_popup": "popup.html"
  },
  "content_scripts": [
    {
      "matches": [
        "https://qiita.com/*"
      ],
      "js": [
        "js/jquery.min.js",
        "js/content.js"
      ]
    }
  ],
  "icons": {
    "16": "icons/16.png",
    "48": "icons/48.png",
    "128": "icons/128.png"
  }
}
```
記述のほとんどは読んで字のごとくです。
需要な項目だけ解説します。
#### permissions
chromeのAPIを使用する際に定義します。
今回はchrome.storageを使用して、ブロックリストを保存したので、Storageを定義しています。
APIの一覧はこちらに記載されています。
[developer.chrome.com | Declare Permissions](https://developer.chrome.com/extensions/declare_permissions)

#### browser_action
アイコンをクリックした際に画面を表示したい場合に必要となります。

#### content_scripts
`matches`に設定した、URLにアクセスした時に`js`に設定されているjsファイルが動作します。

## 非表示処理
ページのURLを取得して別個処理を行います。
二行目でURLのパラメータを削除しています。
（Qiitaは「1日・週間・月間」タブをパラメータで指定している為）

```javascript:content.js
let url = location.href;
url = url.replace($(location).attr('search'), '');
switch (url) {
    case 'https://qiita.com/':
        // 記事非表示
        hideFunc('trends');
        window.onload = function () {
            // ランキング非表示
            hideFunc('ranking');
        };
        break;

    case 'https://qiita.com/timeline':
        //おすすめユーザー非表示
        hideFunc('timeline');
        break;

    case 'https://qiita.com/tag-feed':
        //タグフィード非表示
        hideFunc('tag-feed');
        break;

    case 'https://qiita.com/milestones':
        //マイルストーン非表示
        hideFunc('milestone');
        window.onload = function () {
            // ランキング非表示
            hideFunc(itemGet('ranking'), 'ranking');
        };
        break;

    default:
        //上記以外の場合でURLにitemが含まれている場合
        if (url.indexOf('item') !== -1) {
            //コメントの非表示処理
            hideFunc(itemGet('comment'), 'comment');
        }
        break;
}
```
下記は非表示に関する処理を行います。
ページ内に表示されている**全てのユーザーの情報**をタグレベルで指定して取得します。
ChromeStorageに保存されているブロックリストと、最初に取得したページ内の全ユーザーを比較します。
マッチしたユーザーを返して、各ページに対応する非表示処理を行います。

```javascript:content.js
// リストにブロックリストのIDがあればmuchListに追加
function muchCheck(id_list, blockList) {
    let muchList = [];
    for (let id of blockList) {
        if (id_list.indexOf(id) >= 0) {
            muchList.unshift(id);
        }
    }

    return muchList;
}

//非表示処理
function hideFunc(hide_type) {
    chrome.storage.sync.get("data", function (items) {
        // chrome storageからブロックリストの取得
        let blockList = items.data;
        if (!Array.isArray(blockList)) {
            blockList = [items.data];
        }

        let item_id = [];
        let muchList = [];
        //タイプごとに非表示処理を分岐
        switch (hide_type) {
            case 'ranking':
                item_id = $(".ra-UserList").find(".ra-User_screenname").find("a").text().split('@');
                muchList = muchCheck(item_id, blockList);
                for (let id of muchList) {
                    $(".ra-UserList").find(`a:contains(${'@' + id})`).parents('.ra-UserList_content').hide();
                }
                break;

            case 'trends':
                $(".p-home_main").find(".tr-Item_author").each(function () {
                    item_id.unshift($(this).text());
                });
                muchList = muchCheck(item_id, blockList);
                for (let id of muchList) {
                    $(".tr-Item").find(`a:contains(${id})`).parents('.tr-Item').remove();
                }
                break;

            case 'comment':
                $(".commentList").find(".commentHeader_creator").find("a").each(function () {
                    item_id.unshift($(this).text());
                });
                muchList = muchCheck(item_id, blockList);
                for (let id of muchList) {
                    $(".commentList").find(`a:contains(${id})`).parents('.comment').hide();
                }
                break;

            case 'timeline':
                item_id = $(".tl-RecommendedUserList").find(".tl-RecommendedUser_screenname").text().split('@');
                for (let id of muchList) {
                    muchList = muchCheck(item_id, blockList);
                    $(".tl-RecommendedUserList").find(`a:contains(${'@' + id})`).parents('.tl-RecommendedUserList_item').hide();
                }
                break;

            case 'tag-feed':
                $(".tf-Item.tf-Item-tagFiltered").find(".tf-ItemContent_author").each(function () {
                    item_id.unshift($(this).text());
                });
                muchList = muchCheck(item_id, blockList);
                for (let id of muchList) {
                    $(".tf-Item.tf-Item-tagFiltered").find(`a:contains(${id})`).parents('.tf-Item.tf-Item-tagFiltered').hide();
                }
                break;

            case 'milestone':
                $(".ms-Item").find(".ms-ItemContent_author").each(function () {
                    item_id.unshift($(this).text());
                });
                muchList = muchCheck(item_id, blockList);
                for (let id of muchList) {
                    $(".ms-Item").find(`.ms-ItemContent_author:contains(${id})`).parents('.ms-Item').hide();
                }
                break;
        }
    });
}

```

下記はQiitaのランキングのタブをクリックした際に発生する処理です。
現在持っているランキングの値と0.1秒前に持っていた値が変わっていたら非表示処理を実行します。

```javascript:content.js
//ランキングのタブクリック処理
$('.ra-UserList_tab').click(function () {
    let ranking_id = $(".ra-UserList").find(".ra-User_screenname").find("a").text();
    let timerId = setInterval(function () {
        let monitor_id = $(".ra-UserList").find(".ra-User_screenname").find("a").text();
        if (ranking_id !== monitor_id) {
            hideFunc('ranking');
            clearInterval(timerId);
        }
    }, 100);
});
```



# 苦労した点
* Qiitaのランキング表示が非同期で更新される為、値の取得の方法に困った。

クリックを取得してランキングが変更される前に拡張機能の関数が先に実行されてしまい、ランキング表示変更後の値が取得できない状態だった。
![bandicam 2019-05-04 00-21-12-618.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256196/71b3909a-9c86-4722-34da-b890f2cd322b.jpeg)

対処方法：クリックが押されてから0.1秒間隔でランキング情報を取得して、0.1秒前とランキング情報が違えば、非表示処理を実行する形にした。
（かなりごり押しで実装したので、できればもっと適切な方法があれば教えて頂きたいです…）

# 最後に
今回は需要を考えずに自分の作りたいものを作るという目的で作成しました。
Chromeの拡張機能開発は結構楽しかったので、今後も続けたいと思います。
最後までご拝読頂きありがとうございました。