はじめに
この記事は、Qiita株式会社 Advent Calendar 2022 の 7日目の記事です。
業務で役立つ便利な Chrome 拡張機能を作ったら、社内で人気者になっただけでなく技術的な学びもとても多かったので、みんなも真似してみてね!という記事です。
背景
GitHub の便利な機能の一つとして、画像アップロード機能があります。issue や Pull Request の入力フォームに画像等のファイルをドラッグ&ドロップすると、画像がアップロードされ、 Markdown の画像埋め込みを自動で追加してくれます。ですが、この機能の注意点として、たとえプライベートリポジトリにアップロードした画像でも、URLを知っている人なら誰でも画像を見ることができてしまいます。
(↓ 詳細はこちらのGitHub公式ドキュメントに書いてあります)
例えば、まだリリースしていない新機能のスクリーンショットや個人情報が含まれる画像等をこの機能で GitHub にアップロードした場合、何らかの理由で誤ってURLが外部に漏れてしまうと、情報漏洩が発生してしまいます。
このような理由から、Qiita株式会社ではこの GitHub の画像アップロード機能は禁止になっています。
現在は GitHub にアップロードした画像もプライベートリポジトリなら権限を持ったメンバーしか見れないように改善されているため、GitHub の画像アップロード機能の制限は解除されています。
https://github.blog/changelog/2023-05-09-more-secure-private-attachments/
では、Qiitaのエンジニアは GitHub でどうやって画像のやりとりをしているかというと、Qiita Team の画像アップロード機能を使用しています。
この機能でアップロードされた画像は、チームのメンバーしか見ることができないので、画像URLが漏れてしまっても問題ありません。そのため、Qiita Team にアップロードした画像の URL を GitHub の issue や Pull Request に貼り付けて安全に画像のやりとりをしています。
ここからが本題です!!
Qiita Team にアップロードした画像を GitHub の issue や Pull Request に 表示 するためにはどうすればいいでしょうか?
例えば、以下のように Qiita Team にアップロードした画像のURL Markdown 形式で画像を埋め込む形式にしたらどうなるでしょうか?
![image](https://qiita-inc.qiita.com/files/xxxxxxxxxxxxxxxxxxxxxxxxxxxxx.png)
Markdown が変換され、以下のようなHTMLになります。
<img src="https://qiita-inc.qiita.com/files/xxxxxxxxxxxxxxxxxxxxxxxxxxxxx.png">
ですが、これだと画像を表示することはできません...
GitHub 公式ドキュメントにもある通り、ログイン等が必要な場所にある画像は表示することができません。
そのため、これまでは仕方なく Qiita Team にアップロードした画像の URL を直接張り付け、画像を見たいときは URL を別のタブで開いて確認していました。
このような画像確認方法はとても面倒だったので、「どうにか GitHub 上で Qiita Team にアップロードした画像を表示できるようにしたい!!!」と考え、拡張機能を作ることにしました。
この時は「ページを読み込んだ時にドキュメント内の Qiita Team の画像URLがある a
タグを探して、いい感じに変換すればいけるのでは?」と考えていましたが、意外といろいろ苦戦しました。
これからその試行錯誤の流れを紹介していきます。
Chrome拡張機能を作る
Chrome拡張機能はかなり前に少しだけ触ったことがありましたが、今では作りかたを忘れてしまったので調べるところから始まりました。Chrome 拡張機能の実装の流れは簡単に説明すると以下のようになります。
- マニフェストファイル (
manifest.json
) を作成する - 動かしたい JavaScript のファイルを作成し、マニフェストファイルに指定する
- Chromeでマニフェストファイル等が入ったディレクトリを読み込む
- 動く!
マニフェストファイル (manifest.json
) を作成する
マニフェストファイルの書き方にはバージョンが存在し、現在は V3
を使用すればよさそうということがわかりました。
一旦以下のように、 Manifest V3 の設定ファイルを作成しました。
{
"manifest_version": 3,
"name": "Display Qiita Team images",
"version": "1.0.0",
"description": "Display Qiita Team images in GitHub",
}
次に、JavaScript ファイルを作成し、マニフェストファイルにそのファイルを指定していきます。
マニフェストファイルで動かしたい JavaScript のファイルを指定する
V3 でマニフェストファイルに JavaScript ファイルを指定する方法として、以下の3種類があります。
-
action
- 拡張機能のアイコンをクリックしたときに表示したい HTMLを指定。
- このHTMLの中でJavaScriptを指定すれば、アイコンをクリックしたときに実行できる。
- 拡張機能のアイコンをクリックしたときに表示したい HTMLを指定。
-
content_scripts
- DOMの操作等を行う場合に使う
-
background
(service_worker
)- バックグラウンドで実行したい処理があるときに使う
今回はDOMの操作等 (Qiita Team の画像URLがある a
タグを探して変換)を行うので、とりあえず content_scripts
を指定すればよさそうです。
GitHub の issue と PR で行いたいので、そのURLを matches
に指定し、run_at
に document_end
を指定してページの読み込み後に実行されるようにしました。
"content_scripts": [{
"matches": [
"https://github.com/increments/*/pull/*",
"https://github.com/increments/*/issues/*"
],
"run_at": "document_end",
"js": ["content.js"]
}]
一旦ちゃんと読み込まれることだけ確認したいので、console.log('test')
だけ書いた content.js
ファイルを作成しました。
Chromeでディレクトリを読み込む
Chrome の拡張機能設定ページ (chrome://extensions/
)にアクセスし、manifest.json
と content.js
を入れたディレクトリを読みんでみると、インストール済み拡張機能一覧に表示されました。
読み込みが完了したので、試しに GitHub の issue のページに移動しデベロッパーツールを開いてみると content.js
ファイルに書いたスクリプトが動作し Console に test
と表示されました。
あとは content.js
に変換処理を書いていくだけです!
変換処理の実装
まずはページ内の画像リンクを探していきます。
ページ内のリンクは document.links
で取得できます。
その中から、Qiita Team の画像を探します。ループでひとつずつ探すだけです。
const links = document.links
const qiitaLinks = []
for (var i = 0; i < links.length; i++) {
if (links[i].href.indexOf('https://qiita-inc.qiita.com/files/') === 0) {
qiitaLinks.push(links[i])
}
}
さて、これでQiita Team の画像リンクが取得できました。
ログインが必要な場所にある画像を表示する
ここまでは割とスムーズに進みましたが、ここから画像を表示するまでが一番苦戦しました。
変数 qiitaLinks
には Qiita Team にアップロードした画像のURLが入っていますが、これを単に以下のように img
タグに変換するだけでが表示できないことは冒頭で確認しました。
<img src="[Qiita Teamの画像URL]">
ではどうやって画像を表示するかというと、データURLを使用した方法を試しました。
画像データを取得してデータURLに変換し、img
タグにセットすれば表示できるのでは?と考えました。
画像URLに対して Fetch API でデータの取得を試してみました。
試しに成功したら ok
と出力、失敗したら ng
と出力するために、以下を content.js
に追加しました。
fetch(url, {
'method': 'GET'
})
.then((response) => {
console.log("ok")
})
.catch((error) => {
console.log("ng")
})
すると、次のエラーが出ました。
CORSのエラーです。まあ GitHubのページ上でQiita Teamにアクセスするリクエストを送信してしているので、このエラーが出るのは当然のような気もします。
CORSを解決するためにどうすればいいか調べた結果、以下の記事によると Manifest v3では content_scripts
ではなく background
なら CORSを回避できるということが判明しました。
以上を踏まえ、以下の流れで実装すればGitHub上でQiita Teamの画像が表示できそうです。
-
content_scripts
でURL取得しbackground
に送信する -
background
で取得し、データURLをcontent_scripts
に返す -
content_scripts
に帰ってきたデータURLを img にセット
それぞれ実装していきます。
1. content_scripts
でURL取得し background
に送信する
background
で画像データを取得するために、content_scripts
で取得したURLを background
に渡す必要があります。
chrome.runtime
の以下メソッドを使うとcontent_scripts
から background
にメッセージを送ることができます。
-
chrome.runtime.sendMessage
- メッセージを送信し、帰ってきた結果に対して処理を行う
-
chrome.runtime.onMessage
- メッセージを受信し、処理を行う
content.js
に送信部分を追加します。
chrome.runtime.sendMessage(
{ url: url }
)
background.js
ファイルを作成し、マニフェストファイル (manifest.json
) に追加します。
chrome.runtime.onMessage.addListener((message, _sender, _sendResponse) => {
// ここに処理を書いていく
return true
})
"background": {
"service_worker": "background.js"
}
2. background
で取得し、データURLを content_scripts
に返す
以下のように試しに fetch
を追記して、fetch
が成功するか確認してみました。
chrome.runtime.onMessage.addListener((message, _sender, _sendResponse) => {
fetch(message.url, {
'method': 'GET'
})
.then((response) => {
console.log("ok")
})
.catch((error) => {
console.log("ng")
})
return true
})
今度は問題なく ok
が表示されました。
あとは response
を データURLに変換するだけです。
fetch
によって帰ってくる response
はそれをどんな値として扱うかによって使用するメソッドが異なります。
今回は画像データなので、response.blob()
で Blob
を作成します。
あとは、Blob
からデータURLを作成するメソッド URL.createObjectURL()
を使えばデータURLになるかと思いました。
が、ドキュメントをよく読んでみると、以下のように書いてありました。
メモ: この機能はメモリリークを生み出す可能性があるため、サービスワーカー内で利用することはできません。
background.js
をマニフェストファイルに追加したときのコードを覚えていますか?以下のようについかしました。
"background": {
"service_worker": "background.js"
}
そうです! background.js
はサービスワーカーなので URL.createObjectURL()
が使えません!!
終わりだ...と一瞬あきらめかけましたが、他の方法が見つかりました。それは FileReader
を使った方法です。
FileReader
には FileReader.readAsDataURL()
というメソッドがあり、これは Blob
を受け取ります。
そして、FileReader.result
でデータURLを取得することができます。
最終的に、以下のように FileReader
で Blob
から データURLに変換し、 sendResponse
で再び content_script
に返しました。
let reader = new FileReader()
reader.onload = function() {
sendResponse(reader.result)
}
fetch(message.url, {
'method': 'GET'
})
.then((response) => {
if (response && response.ok) {
return response.blob()
}
})
.then((blob) => {
reader.readAsDataURL(blob)
return true
})
3. content_scripts
に帰ってきたデータURLを img にセット
background.js
で画像データを取得し、データURLに変換できたので、あとは content_scripts
で img
を作成するだけです。
sendMessage
の第2引数に background.js
から帰ってきたデータURLを img
タグに変換する処理を書いてあげれば完成です。
chrome.runtime.sendMessage(
{ url: url },
(response) => {
const e = new Image()
e.src = response
linkElement.appendChild(e)
}
)
試しに実行してみたところ、無事に画像を表示することができました。
完成!
というわけで、完成した拡張機能を早速社内に公開したところ、Slackでたくさんの絵文字リアクションをもらったり、日報で「控えめに言って最高!」や「めちゃめちゃ便利」など多くの反響があり、社内で人気者になることができました。
まとめ
- Chrome拡張機能を作るのは楽しい!
- Chrome拡張機能を作ると様々なことを学べる!!
- Chrome拡張機能を作ると人気者になれる!!!