Vue.js を使ってChrome 拡張機能を作った話(はてなブックマークのコメントを Qiita 記事内に表示)

# はじめに

Qiita もはてなもこよなく愛する方のために、以下画像のように Qiita の記事の終わりに[はてなブックマーク](https://b.hatena.ne.jp)のコメントを読み込んで表示する拡張機能 [Hatiina](https://chrome.google.com/webstore/detail/hatiina/hlnbapojfhhnelglfjilcefaefjbpcng) を作った。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72108/b9fd91a9-5ae9-65fc-b034-64291b8cb59c.png)

## 宣伝

別の拡張機能も作ったので、良ければこちらも見ていって。(こっちは Vue.js 使ってないけど)

[Qiitaの有益(そうな)コメントを簡単に判別できるChrome拡張機能を作った](https://qiita.com//17number/items/7c1b2c6051a24915b93f)


# TL:DR;

- クエリパラメータ(`?hoge=fuga&foo=bar`)やアンカー(`#xxx`)付きの URL でもブコメを表示できる
    - メールなどから Qiita にアクセスした場合とか
- スター獲得数順にソート
- 実装には Vue.js (とその他いくつかの NPM packages)を利用



# 作ろうと思った動機

## 1. 普段の作業を簡略化するため

自分はググって Qiita を見ることが多いが、毎週来る Qiita のニュースメールや Twitter からの通知メールから気になった記事を見ることも結構ある。こういうやつ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72108/1bd774df-18a3-62cd-d70b-fc9a0709553d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72108/feb2cb2b-b46f-5bef-7b96-3576b6a139c8.png)

で、記事本文だけでなく、なるべく記事中のコメントやはてなブックマークのコメントも見ることで、「記事中で言及されている内容に対して、他の人や玄人はどう考えているのか」や「他に良いツール/やり方/考え方は無いか」などの参考にしている。のだが、↑のリンクから飛ぶと**[はてなブックマーク拡張機能](https://chrome.google.com/webstore/detail/はてなブックマーク/dnlfpnhinnjdgmjfpccajboogcjocdla)でコメントが読み込めない**。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72108/81ab7913-411b-552d-ccfc-c94dbf69388c.png)

画像から分かるかもしれないが、**原因は URL 内のクエリパラメータやアンカー**にある。記事本文の**素の URL にページ移動するとコメントが正しく表示できる**。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72108/96f75be3-9404-dfe0-78f8-b97103fb9000.png)

…できるんだけど、**「メールから記事開く」→「アドレスバーにフォーカス」→「URL 修正して移動」→「はてなブックマーク拡張機能をクリック」という手順が無駄**だし面倒すぎてイライラしていた。

というわけで作った。

## 2. 技術的に試したいことがあった

Vue.js を使った拡張機能の記事をいくつか見たが、どれもメインページ側(という表現で良いか不安)に作用するものではなく、ポップアップページ(拡張機能アイコンのクリックで表示されるやつ)に関するものだった。(自分が見た範囲では)

- [Vue\.jsを使ったブラウザ拡張機能の作り方 \- 物置小屋](https://mnao305.hatenablog.com/entry/2019/02/05/191044)
- [Chrome拡張機能開発でVue\.jsを動かす – myMemoBlog by 256hax](https://blog.tanebox.com/archives/459/)
- [Vue\.jsを使ってTwilio管理コンソールのアカウントを切り替えるChrome拡張機能作った \| \| 針は飲まれぬ](http://harinoma.info/?p=340)

**「俺が Vue.js を使いたいのはそこじゃないんだよ！」**と思って「無いなら試してしまえ」の精神で作った。ちなみに「使ってみたかった」なので、「Vue.js じゃないと作れない」というものではない。


# 拡張機能の紹介

[Hatiina - Chrome ウェブストア](https://chrome.google.com/webstore/detail/hatiina/hlnbapojfhhnelglfjilcefaefjbpcng)

ウェブストアの画像や説明が雑なのはご愛嬌ということで…

## できること

ページ遷移なしに「素の URL」に付いているブコメを読み込んで表示。

もうほぼ説明することは無い(前章までに書いてある)ので、簡単にスクショで紹介。

### 読み込み直後

ブコメは閉じた状態、クリックすると展開される

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72108/d405f976-f9ef-b04c-e8fb-e5b27cde790f.png)

### ブコメを展開

スター数順にソートされた状態。ブックマーク追加ページへのリンクや、各ユーザーへのリンクなども表示。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72108/8bc89245-8907-ae7f-73d7-d6c15eb7940e.png)

### ブコメが無い場合

こうなる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72108/b15863a2-6c20-9382-a4b4-f35b028cc8a1.png)


## できないこと

あくまで「読み込んで表示」だけなので、ページ内でブックマークしたり、コメントにスターを付けたりはできない。ただし、リンク付けてるので(面倒だけど)ページ遷移すればできるはず。


# 技術的な話

(初歩的な内容も含むが)拡張機能内で使っているツールや設定などを書いていく。「もっと他にいいやり方あるよ」とかあればコメント貰えると嬉しい。

リポジトリ: [17number/chrome\-extension\-hatiina: Chrome extension for qiita](https://github.com/17number/chrome-extension-hatiina)

## 開発環境

macBook Pro で開発した。

```bash
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.15
BuildVersion:	19A583

$ node -v
v12.7.0

$ npm -v
6.10.3
```

## 使用パッケージ

[`package.json`](https://github.com/17number/chrome-extension-hatiina/blob/master/package.json) より

```json:package.json
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "^1.2.25",
    "@fortawesome/free-brands-svg-icons": "^5.11.2",
    "@fortawesome/free-regular-svg-icons": "^5.11.2",
    "@fortawesome/free-solid-svg-icons": "^5.11.2",
    "@fortawesome/vue-fontawesome": "^0.1.7",
    "axios": "^0.19.0",
    "crx-hotreload": "^1.0.4",
    "moment-timezone": "^0.5.26",
    "vue": "^2.6.10",
    "webextension-polyfill": "^0.5.0"
  },
  "devDependencies": {
    "@vue/cli-service": "^3.11.0",
    "cpx": "^1.5.0",
    "mkdirp2": "^1.0.4",
    "node-sass": "^4.12.0",
    "npm-run-all": "^4.1.5",
    "npm-watch": "^0.6.0",
    "rimraf": "^3.0.0",
    "sass-loader": "^8.0.0",
    "terser-webpack-plugin": "^2.1.3",
    "vue-svg-loader": "^0.12.0",
    "vue-template-compiler": "^2.6.10"
  }
```


## Vue.js を主ページ側で使う方法

整理すればそんなに難しいことじゃない。~~(ここに辿り着くまでに割と右往左往した)~~

- `manifest.json` で先に Vue.js (の定義されたファイル)を読み込む
- Vue.js のターゲットとなる HTML 要素を埋め込む
- 埋め込まれた HTML 要素に対して Vue のインスタンスを生成する

[`manifest.json`](https://github.com/17number/chrome-extension-hatiina/blob/master/src/manifest.json) より

```json:manifest.json
{
  ...
  "content_scripts": [
    {
      "matches": [
        "https://qiita.com/*/items*"
      ],
      "js": [
        "js/chunk-vendors.js",  # Vue.js のコードはここに入っている(ので先に読み込む)
        "js/app.js"             # ここで Vue.js のインスタンス生成とか
      ],
    }
  ],
  ...
}
```


[`app.js`](https://github.com/17number/chrome-extension-hatiina/blob/master/src/app.js)(エントリポイント)

```js:app.js
import Vue from "vue";
import App from "./App.vue";

(() => {
  'use strict';

  document
    .querySelector(".p-items > .p-items_container > .p-items_main")
    .insertAdjacentHTML(
      `afterBegin`,
      `<div id="hatiina"></div>`
    );

  new Vue({
    render: h => h(App),
  }).$mount("#hatiina");
})();
```

この辺りの話は自分のブログに書いているので合わせて載せておく。

[Chrome 拡張機能の開発中にホットリロード\(自動読み込み\)する方法](https://r17n.page/2019/10/05/chrome-extension-auto-reload-on-development/)


## ビルド関連

ビルドは [`vue-cli-service`](https://cli.vuejs.org/guide/cli-service.html) で行う。[Content Scripts](https://developer.chrome.com/extensions/content_scripts) 側と [Background Scripts](https://developer.chrome.com/extensions/background_pages) 側とでコマンドを分けている。

### Content Scripts 側

`--no-clean` オプションを付けている理由は後述。

```json:package.json
  "scripts": {
    "compile:content": "vue-cli-service build --no-clean src/app.js",
  },
```

ポイントは `vue.config.js` による設定で、`filenameHashing` と `productionSourceMap` を `false` にしていること。

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

[`filenameHashing`](https://cli.vuejs.org/config/#filenamehashing) の設定をしていないと、`app.1234abcd.js` のようなファイル名で出力されてしまい、`manifest.json` で指定するソースファイルの記述を修正しないといけなくなる。頑張れば自動で変更できると思うが、それよりはファイル名を固定にした方が良い。

[`productionSourceMap`](https://cli.vuejs.org/config/#productionsourcemap) の設定は、ソースマップファイルは不要だよね、というだけ。


### Background Scripts 側

Background Scripts 側のビルドは色々と試した結果、以下の設定だと上手く動作した。

```json:package.json
  "scripts": {
    "compile:background": "vue-cli-service build --target lib --formats umd --dest dist/js --no-clean --name background src/js/background.js",
  },
```

これで `src/js/background.js`(とそこから読み込まれているパッケージなど)が、`dist/js/background.umd.js` に出力される。「なぜそのオプションにしたのか」については、何となくは分かるが明確に言語化できない~~(= 理解が浅い)~~ので、誤解がうまれるのを避けるためにも言及しない。

~~※よくよく考えると、別に Vue.js 関係ない(はてなブックマークの API を叩いて返すだけ)から、別に `vue-cli-service` 使う必要はなかったんじゃ…~~


### `--no-clean` オプションの理由

以下の記事を参考に、ファイル変更を検知してホットリロードさせている。

[Chrome Extensionの開発時にホットリロードさせる \- Qiita](https://qiita.com/takeyuichi/items/3638064379f7a314df57)

(試してないので確実ではないが) `--no-clean` を付けずにビルドして、`dist` ディレクトリごと削除されると挙動がおかしくなるのでは？と思い、既存コードは残したまま上書きするようにした。

自分のブログで NPM 使う設定例も書いているので、ついでに載ｓ

[Chrome 拡張機能の開発中にホットリロード\(自動読み込み\)する方法](https://r17n.page/2019/10/05/chrome-extension-auto-reload-on-development/)



## その他

### [FontAwesome](https://fontawesome.com)

[FontAwesome](https://fontawesome.com) は Vue.js Project で使用可能な[公式パッケージ](https://www.npmjs.com/package/@fortawesome/vue-fontawesome)があるのでそれを利用。

> Font Awesome now has an official Vue.js component that’s available for all who want to use our icons in their Vue.js projects.
> [Vue\.js \| Font Awesome](https://fontawesome.com/how-to-use/on-the-web/using-with/vuejs) より

[@fortawesome/vue\-fontawesome](https://www.npmjs.com/package/@fortawesome/vue-fontawesome) の [Usage](https://www.npmjs.com/package/@fortawesome/vue-fontawesome#usage) に書かれている通り、`import`して Vue のインスタンス生成前にゴニョゴニョすれば、`<font-awesome-icon />` というタグで表示できる。

```js:app.js
import { library } from '@fortawesome/fontawesome-svg-core';
// ↓は使いたいアイコン
// 今回は bookmark(https://fontawesome.com/icons/bookmark?style=solid) なので faBookmark
import { faBookmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';

(() => {
  ...

  // FontAwesome Setup
  library.add(faBookmark)
  Vue.component('font-awesome-icon', FontAwesomeIcon)
  Vue.config.productionTip = false

  new Vue({
    render: h => h(App),
  }).$mount("#hatiina");
})();
```

`:icon` で Style(Regular(`far`) とか Solid(`fas`) とか) とアイコン名を指定する。

```vue:xxx.vue
<template>
  <div id="app">
    <font-awesome-icon :icon="['fas', 'bookmark']" />
  </div>
</template>
```

以下の記事が分かりやすい。

[Font awesome を Vue\.js で使ってみよう \- Qiita](https://qiita.com/kurararara/items/d76776a7dc2d763a068b)

自分のブログにもまとめた。

[Vue\.js で FontAwesome を使う方法](https://r17n.page/2019/10/14/vue-with-fontawesome/)


### 画像・アイコン

#### はてなスター

はてなスターの画像は安定の[いらすとや](https://www.irasutoya.com)から拝借して利用。

[いろいろな色の星のイラスト \| かわいいフリー素材集 いらすとや](https://www.irasutoya.com/2016/06/blog-post_817.html)

上記をダウンロードして、画像サイズを小さくした後に、[TinyPNG](https://tinypng.com/) で圧縮。


#### はてなブックマーク

公式素材があるのでそれを利用。SVG だったので、そのままプロジェクト内に取り込み。

[素材集 \- 株式会社はてな](https://hatenacorp.jp/press/resource)


#### 拡張機能アイコン・ウェブストア画像

[Canva](https://www.canva.com) で作成。

フォントは[うずらフォント](http://azukifont.com/font/uzura.html)を使用~~、理由はなんとなく気に入ったから~~。

ウェブストアの説明や画像はそのうちどうにかする予定…


# Hatiina のイケてない点

## ~~API Request 数が多い~~

~~はてなスター数を取ってくるのに API を連打してしまっている。これは解決手段が分かってるので、別途修正予定。~~

修正した v1.0.1 をリリースした。

## なんかスター数が多い

公式拡張機能のカウント数と見比べると分かるんだけど、なんか表示されるスター数に差がある。

とは言え、全体的に多くカウントされる(= みんな多い)だけなので、(個人的には)大した問題じゃないと思って放置予定。

## ブコメ数が多いとダルい

ブコメを閉じるには、一番上の箇所をクリックするしかないんだけど、全200コメント中の100コメントあたりを見ている状態だと、閉じるにはスクロールしまくるしかない。

## ウィンドウ幅を狭くした時の見た目

めっちゃ狭くするとユーザーアイコンの表示が変になったり、特定の幅で Qiita 本体のレイアウトと適合しない配置になったりする。

本質的な問題じゃないので放置。


## 共通化などが中途半端(な気がする)

もう少しコンポーネント分割できそうな気がする。

あと、Vue.js の(把握できていない)便利機能とかで、もっとシンプルに書けそうな気もする。


# あとがき

メールとかから飛んでもブコメ見れるから、はてブ民は良ければ使ってくれよな。

[Hatiina - Chrome ウェブストア](https://chrome.google.com/webstore/detail/hatiina/hlnbapojfhhnelglfjilcefaefjbpcng)

リポジトリも公開してるから「変な拡張機能怖い」って人とか、「おかしな実装ないか調べたろ！」って人も安心だよ。

[17number/chrome\-extension\-hatiina: Chrome extension for qiita](https://github.com/17number/chrome-extension-hatiina)

## その他

別の拡張機能

[Qiitaの有益(そうな)コメントを簡単に判別できるChrome拡張機能を作った](https://qiita.com//17number/items/7c1b2c6051a24915b93f)