Qiitaのトレンド記事を無理やり見させるChrome拡張をTypeScriptで作った

 皆さん、新年あけましておめでとうございます。
年越しそば、食べましたか。家の近くの立ち食いそば屋さんが、普段は普通の客入りなのに大晦日のときは人でごった返していました。
コロナで人の往来がめっきり減っている中、そんな光景を見て少しだけホッとしました(大きな声では言えませんが……)

正月でもサボらずに勉強したかった

 正月休み、美味しいもの食べて寝たり、テレビが面白かったりで、ついつい勉強をサボっちゃうこと、あると思うんです。
誰かケツを叩いてくれたら一番ありがたいんですけど、正月はみんな休みたいのが本音だと思うんです。

そこで、強制的に勉強させるアプリケーションを作ろうと思いました。
勉強といっても、実際にコーディングするアウトプットの勉強だったり、最新の技術情報とかを調べるインプットの勉強がありますよね。
今回は、Chromeを起動中にQiitaのトレンドランキングを強制的に見せることで無理やりインプットの勉強を行わせる拡張機能を作成することにしました。

実際に実装していく

 Chromeの拡張機能を作成するのは初めてだったので、いろいろ調べながら作成していきます。
Chrome拡張の肝は、manifest.json という拡張機能の概要を定義したファイルです。正直、この manifest.json 以外は普通にJavaScript使ってるのとほとんど同じ感覚です。
今回の実装では、manifest.json は以下のようになりました。

{
  "name": "qiita-miro",
  "version": "1.0.0",
  "manifest_version": 2,
  "description": "Force to look Qiita article of trend",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["./js/force.ts.bundle.js"]
    }
  ],
  "icons": {
    "16": "icon16.png",
    "48": "icon48.png",
    "128": "icon128.png"
  },
  "permissions": ["storage"],
  "web_accessible_resources": ["src/js/*"]
}

permissions の storage はブラウザのローカルストレージに読み書きを行う上で必要です。
今回の拡張機能では閲覧したQiitaのトレンド記事を保持するために指定しています。
(現在閲覧しているQiitaの記事を取得するためには Tab のpermissionも必要ですが、JavaScript の location.href でURLを取得できたのでそちらを採用しています)

また、content_scripts の matches が all_urls となっていますが、これを指定することで全てのページ閲覧において拡張機能を動作させることができます。
しかし、どうやらホスト権限とやらが必要になるらしく、実際に公開する時には「ホスト権限が要求されてるから審査厳しくなるぜ」的なことを言われます。
(実際に自分は審査まだ通っていない泣 ホスト権限を要求せずに全てのページ閲覧で動作させる方法があったら教えてください……)

後は、Qiitaのトレンド記事を取得する部分ですが、どうやらAPIが公開されていないみたいなので

https://qiita.com/HelloRusk/items/803f9599cde72810f1a8

こちらの記事様のコードを全面的に採用しています。

private static async getQiitaArticleOfTrend(): Promise<Article[]> {
  const url = "https://qiita.com/";

  axios.defaults.baseURL = "http://localhost:3000";
  axios.defaults.headers.post["Content-Type"] =
    "application/json;charset=utf-8";
  axios.defaults.headers.post["Access-Control-Allow-Origin"] = "*";
  return await axios
    .get(url)
    .then(({ data }) => {
      return this.fetchTrend(data);
    })
    .catch((err) => {
      return [];
    });
}

private static fetchTrend(html: string): Article[] {
  const $ = cheerio.load(html);
  const raw =
    $("script[data-component-name=HomeArticleTrendFeed]").html() ?? "";
  if (raw === undefined) return [];
  const rawData = JSON.parse(raw).trend.edges;

  return rawData.map((obj: any) => {
    delete obj.followingLikers;
    delete obj.isLikedByViewer;
    return obj;
  });
}

ほとんど、丸コピです。

TypeScriptで書いたコードをwebpackでビルドする

 tsconfig.json は tsc –init で自動生成したそのままで大丈夫です。

webpackの設定ですが、今回は manifest.json の content_scripts で複数のエントリポイントが指定される可能性があることから、複数ファイルをビルドで出力する必要があります。
また、manifest.json がリソースファイルであるため、ビルド時にそのままコピーして出力をしてほしいです。

まず前者の複数ファイルをビルドで出力する設定ですが、

こちらの記事様を参考にさせていただきました。
特に関係ない設定については省いて記載していますので、このままコピペしても動きません。多分

// モジュールインポート
const path = require("path");
const glob = require("glob");

// ソースファイルを検索してビルド用の配列を生成する
const srcDir = "./src";
const entries = glob
  .sync("**/*.ts", {
    ignore: "**/_*.ts",
    cwd: srcDir
  })
  .map(function (key) {
    return [key + ".bandle.js", path.resolve(srcDir, key)];
  });
const entryObj = Object.fromEntries(entries);

module.exports = {
  entry: entryObj, // 上記で生成したビルド用の配列を指定する
  output: {
    path: path.join(__dirname, "public"),
    filename: "[name]"
  }
};

これで、複数ファイルをビルドで出力することができました。
次に、manifest.jsonなどの静的なリソースファイルをそのままビルド時にコピーする方法ですが、

https://yoshinorin.net/2019/06/29/webpack-copy-node-modules-minfile/

こちらのサイトを参考にしております。(さっきから参考にしてばっかだな……)

const CopyPlugin = require("copy-webpack-plugin");

module.exports = {
  plugins: [
    new CopyPlugin({
      patterns: [
        {
          from: "./src/manifest.json",
          to: "manifest.json"
        }
      ]
    })
  ]
};

地味に躓きポイントなんですが、CopyPluginのコンストラクタの引数はpatternsプロパティを含めたオブジェクトで指定してあげる必要があります。

完成

 上記で説明したつまづきポイントを乗り越え、どうにか機能するところまで持っていったので、とりあえず完成としました。
あとはハードコーディングしてる部分がいくつかあるので、設定画面などを作成してユーザーが設定できるようにしたいですね。
ソースコードについてはgithubで公開しています。

https://github.com/straistr/qiita-miro-chrome-extension

わかりづれーよハゲ!!!!みたいなところがいっぱいあると思うのでコードの方を見てください(コードが参考になるとは限らない)

あとはChromeの拡張機能として公開したいんですが、試しに一回投げてみたら審査で拒否されちゃいました。
ちゃんと通るように修正したいんですが、どこが悪かったか何も言ってくれないのでめちゃくちゃ不親切ですよね。
考えられるとこをちまちま修正していこうと思います……