Help us understand the problem. What is going on with this article?

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

はじめに

Qiita もはてなもこよなく愛する方のために、以下画像のように Qiita の記事の終わりにはてなブックマークのコメントを読み込んで表示する拡張機能 Hatiina を作った。

※拡張機能を公開したのが 2019/10/12 なので、もしかしたら審査リジェクトで見えなくなる期間が出てくるかも。

image.png

TL:DR;

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

作ろうと思った動機

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

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

image.png

image.png

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

image.png

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

image.png

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

というわけで作った。

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

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

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

拡張機能の紹介

Hatiina - Chrome ウェブストア

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

できること

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

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

読み込み直後

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

image.png

ブコメを展開

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

image.png

ブコメが無い場合

こうなる。

image.png

できないこと

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

技術的な話

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

リポジトリ: 17number/chrome-extension-hatiina: Chrome extension for qiita

開発環境

macBook Pro で開発した。

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

$ node -v
v12.7.0

$ npm -v
6.10.3

使用パッケージ

package.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 より

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

app.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 拡張機能の開発中にホットリロード(自動読み込み)する方法

ビルド関連

ビルドは vue-cli-service で行う。Content Scripts 側と Background Scripts 側とでコマンドを分けている。

Content Scripts 側

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

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

ポイントは vue.config.js による設定で、filenameHashingproductionSourceMapfalse にしていること。

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

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

productionSourceMap の設定は、ソースマップファイルは不要だよね、というだけ。

Background Scripts 側

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

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

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

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

Chrome 拡張機能の開発中にホットリロード(自動読み込み)する方法

その他

FontAwesome

FontAwesome は Vue.js Project で使用可能な公式パッケージがあるのでそれを利用。

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 より

@fortawesome/vue-fontawesomeUsage に書かれている通り、importして Vue のインスタンス生成前にゴニョゴニョすれば、<font-awesome-icon /> というタグで表示できる。

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) とか) とアイコン名を指定する。

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

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

Font awesome を Vue.js で使ってみよう - Qiita

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

Vue.js で FontAwesome を使う方法

画像・アイコン

はてなスター

はてなスターの画像は安定のいらすとやから拝借して利用。

いろいろな色の星のイラスト | かわいいフリー素材集 いらすとや

上記をダウンロードして、画像サイズを小さくした後に、TinyPNG で圧縮。

はてなブックマーク

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

素材集 - 株式会社はてな

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

Canva で作成。

フォントはうずらフォントを使用、理由はなんとなく気に入ったから

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

Hatiina のイケてない点

API Request 数が多い

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

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

なんかスター数が多い

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

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

ブコメ数が多いとダルい

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

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

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

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

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

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

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

あとがき

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

Hatiina - Chrome ウェブストア

※拡張機能を公開したのが 2019/10/12 なので、もしかしたら審査リジェクトで見えなくなる期間が出てくるかも。

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

17number/chrome-extension-hatiina: Chrome extension for qiita

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした