Vue3 で Threads for Google Chat という chrome 拡張を作ってみた！

# 何を作ったか？

- [Threads for Google Chats](https://chrome.google.com/webstore/detail/threads-for-google-chat/ofeljikmiggiajkgahjkfadmdipacbmm/related?hl=ja) という Google Chat のための拡張機能を作りました :laughing:
  - 拡張をインストールして Google Chat のルームを開くと、**スレッドをリストにして表示**してくれます
  - スレッドのタイトルを押すことで**そのスレッドまでリンク**させることができます

<details><summary>**2021-07-04 追記**</summary><div>

* :warning: Google Chat のアップデートによりチャットのコンテンツが `<iframe/>` で提供されるようになり、Chrome 拡張機能からスレッド情報を参照できなくなったため上記の拡張機能を非公開にしました.

</div></details>

<details><summary>**2021-10-23 追記**</summary><div>

* :tada: [前職の同期が拡張機能を直してくれた](https://github.com/Nag729/chat-thread-extension/pull/14) ので再度公開しました
* **持つべきものは友です.**

</div></details>

![extension.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266244/73ce042a-5fff-fdbb-fe68-3beb90bbe681.jpeg)

Google Chat ユーザーの方はぜひ一度使ってみてください！

# どう作ったか？

- 拡張機能の表示領域については、**[Vue.js 3.0](https://v3.vuejs.org/)** を使ってみました！
- Google Chat 上の DOM 要素の取得などは普通の JavaScript で書いています

GitHub のリポジトリを載せておくので、よければ参考にしてください。

> https://github.com/Nag729/chat-thread-extension

# Vue.js 3.0 で chrome 拡張を作る方法

この記事の本題です。

- 前提として、VueCLI でアプリケーションを作ったことがあるくらいのレベルを想定しています
- chrome 拡張の基本的な作成方法について体系的な説明はしていません ~~よく分かっていないので~~
  - chrome 拡張自体からキャッチアップしたい方は、**[Chrome の拡張機能を自作する - 虎の穴開発室ブログ](https://toranoana-lab.hatenablog.com/entry/2020/04/23/174421)** などを参考にしてください

## :link: Link

`Vue.js × chrome拡張` では、以下の記事が大変参考になりました。
この記事では Vue 2.x で実装されていますが、基本的な書き方はほとんど変わらなかったです。

- :link: **[Vue.js を使って Chrome 拡張機能を作った話(はてなブックマークのコメントを Qiita 記事内に表示)](https://qiita.com/17number/items/e78af9bff52b19d5cb42)**

## :wrench: プロジェクトの作り方

- [VueCLI](https://cli.vuejs.org/) でプロジェクトを作って、作成時のオプションで **Vue 3** を選択しました
  - :warning: [VueCLI v4.5 以降でないと Vue 3 プロジェクトを作成できない](https://v3.vuejs.org/guide/installation.html#cli) ので、注意してください

## :star: Vue.js を chrome 拡張でどう動かすか

### `manifest.json` の定義

chrome 拡張では **`manifest.json` というファイルに定義された JS / CSS ファイルが動作する** という仕組みになっています。
今回の拡張では、以下のような指定をしました。

:star: `manifest.json`

```json-doc
{
  ...
  "content_scripts": [
    {
      "matches": ["https://chat.google.com/*"], // Google Chat のページで有効化
      "js": ["js/chunk-vendors.js", "js/app.js"], // ビルドされたJSファイルを指定
      "css": ["css/app.css"], // ビルドされたCSSファイルを指定
      "run_at": "document_end" // ページが読み込み完了したタイミングで拡張を動かす
    }
  ],
  ...
}
```

VueCLI でプロジェクトを作ったことがあれば何となく想像がつくと思いますが、
**`npm run build` で出力されるファイルを読み込むように `manifest.json` を定義**しています。

### `dist/` に動作する chrome 拡張フォルダを作るための方法

`npm run build` で出力されるのは Vue のソースだけなので、**chrome 拡張として動作させるためにはいくつかのファイルのコピー・削除等が必要**です。
その辺りの操作は、**npm scripts で解決**しています。

:star: `package.json`

```json-doc
...
"scripts": {
  "compile:content": "vue-cli-service build --no-clean src/main.js", // Vue build
  "compile": "run-s compile:content",
  "copy:image": "cpx src/assets/48.png dist/", // 拡張のiconをコピー
  "copy:manifest": "cpx src/manifest.json dist/", // manifest.jsonをコピー
  "copy": "run-p copy:manifest copy:image",
  "remove": "rimraf dist/index.html", // index.htmlは不要なので削除
  "build": "run-s compile copy remove" // 上記のscriptsをまとめて呼び出す
},
...
```

また、VueCLI はデフォルトだとファイル名にハッシュを付けるので、`vue.config.js` を編集します。
ついでに、sourceMap も不要なので `false` にしておきます。

:star: `vue.config.js`

```js
module.exports = {
  filenameHashing: false,
  productionSourceMap: false,
};
```

### `main.js` の定義

Vue インスタンスを作成するための `main.js` についても簡単に説明しておきます。
**画面の DOM 要素を取得して `<div>` タグを挿入し、挿入したタグに Vue をマウント**しています。

:star: `main.js`

```js
import { FontAwesomeIcon } from "@/plugins/font-awesome";
import { createApp } from "vue";
import App from "./App.vue";

const extensionStart = () => {
  // DOM を更新
  const element = document.querySelector("body > div");

  // メインの div に Vue を読み込ませる用のタグを追加
  const createdDom = document.createElement("div");
  createdDom.id = "thread-extension";
  element.insertAdjacentElement("beforeend", createdDom);

  // Vue インスタンスの生成
  createApp(App).component("fa", FontAwesomeIcon).mount("#thread-extension");
};

extensionStart();
```

ここまで準備すれば、後は通常の Vue と同じように書くことが可能です :tada:
※ただし、元サイトの DOM は Vue で管理されていないので `document.querySelectorAll` などを利用して要素を取得する必要があります。

---

## 終わりに

上記の手順は多少面倒でしたが、慣れている Vue で拡張を作れるのは実装しやすかった & 作っていて楽しかったです！
今回のものは小規模ですが、もう少し大規模になってくると Vue のコンポーネント分割の恩恵も受けられると思います。

もしより良い導入方法/実装方法などがあればコメント等で教えていただけると嬉しいです :blush: