これはただの集団 Advent Calendar 2020の13日目の記事です。
Chromeの拡張機能が html
と js
で作れると知りまして、
自分がよく使っている Vue.js
でやってみようと思いまして試しに作ってみた。
・・・ところ見事に失敗した話。
◇ 参考
- vue-web-extension を使って Chrome 拡張機能を開発する方法
- Vue.jsを使ったブラウザ拡張機能の作り方
- Vue.js を使ってChrome 拡張機能を作った話(はてなブックマークのコメントを Qiita 記事内に表示)
- Chrome Developers API Reference
- whois API
■ 開発環境
- macOS:Catalina
- Node.js:v14.7.0
- nodebrew:v8.9.4
- yarn:v1.22.10
- Chrome:v87.0 (x86)
■ 導入手順
Kocal/vue-web-extensionを使って作って行きます。
$ mkdir myDir
$ cd myDir
$ vue init kocal/vue-web-extension sample-extension
Command vue init requires a global addon to be installed.
Please run yarn global add @vue/cli-init and try again.
◇ 何やら失敗したので色々確認・・・
# vue-cliは入ってる。
$ vue --version
@vue/cli 4.5.6
# しばらくやってなかったのでupgradeする。
$ yarn global upgrade
# 指示通りinitをいれる。
$ yarn global add @vue/cli-init
# 再挑戦してみたが・・・
$ vue init kocal/vue-web-extension sample-extension
vue-cli · ENOENT: no such file or directory,'/Users/hogehoge/.vue-templates/kocal-vue-web-extension/template'
◇ Readmeを読んだら・・・・
init
ではなく create
を使う方法に変わっていた。
$ vue create --preset kocal/vue-web-extension sample-extension
終わるまで結構時間がかかります。
◇ オプション
@vue/cli-plugin-eslint
# lintは好みで選択しました
? Pick an ESLint config: Standard
? Pick additional lint features: Lint on save
vue-cli-plugin-browser-extension
# 簡単にpopupを作る予定なので2つだけ
? Which browser extension components do you wish to generate? background, popup
# ストアに公開せず個人の範疇で利用するので、今回は無し
? Generate a new signing key (danger)? No
Preset options:
? Install axios? Yes
■ 構成
◇ package.json
{
"name": "sample-extension",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service build --mode development --watch",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.20.0",
"core-js": "^3.6.5",
"vue": "^2.6.11",
"vue-router": "^3.2.0",
"vuex": "^3.4.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-standard": "^5.1.2",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.2.2",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"vue-cli-plugin-browser-extension": "latest",
"vue-template-compiler": "^2.6.11"
}
}
◇ manifest.json
とりあえず初期設定のままで。
{
"manifest_version": 2,
"name": "__MSG_extName__",
"homepage_url": "http://localhost:8080/",
"description": "A Vue Browser Extension",
"default_locale": "en",
"permissions": [
"<all_urls>",
"*://*/*"
],
"icons": {
"16": "icons/16.png",
"48": "icons/48.png",
"128": "icons/128.png"
},
"background": {
"scripts": [
"js/background.js"
],
"persistent": false
},
"browser_action": {
"default_popup": "popup.html",
"default_title": "__MSG_extName__",
"default_icon": {
"19": "icons/19.png",
"38": "icons/38.png"
}
}
}
◇ ディレクトリ
sample-extension/
├── node_modules/
├── public/
│ ├── _locales/
│ │ └── en
│ ├── icons/
│ │ ├── 128.png
│ │ ├── 16.png
│ │ ├── 19.png
│ │ ├── 38.png
│ │ └── 48.png
│ ├── browser-extension.html
│ ├── favicon.ico
│ └── index.html
├── src/
│ ├── assets/
│ │ └── logo.png
│ ├── components/
│ │ └── HelloWorld.vue
│ ├── popup/ (今回はこれを使う)
│ │ ├── App.vue
│ │ └── main.js
│ ├── router/
│ │ └── index.js
│ ├── store/
│ │ └── index.js
│ ├── views/
│ │ ├── About.vue
│ │ └── Home.vue
│ ├── App.vue
│ ├── background.js
│ ├── main.js
│ └── manifest.json
├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
└── vue.config.js
■ 開発手順
- Chromeの拡張機能画面から
デベロッパーモード
をONにする -
dist
ディレクトリをパッケージとしてブラウザに登録する -
yarn serve
でwatch
を開始する - あとはコードを書いていく (HMRが効いているので再読み込みとかはいらないはず・・・)
-
yarn build
で本番ビルド
■ サンプルコード
◇ Axiosを使えるようにする
import Vue from 'vue'
import axios from 'axios'
import App from './App.vue' //←これを追加
Vue.prototype.$axios = axios //←これを追加
/* eslint-disable no-new */
new Vue({
el: '#app',
render: h => h(App)
})
◇ Before
アイコンをクリックして出てくるポップアップの内容が変更されたのを確認。
変更するファイルはこれで良さそう。
<template>
<hello-world />
</template>
<script>
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'App',
components: { HelloWorld }
}
</script>
<style>
html {
width: 400px;
height: 400px;
}
</style>
◇ After
- アイコンをクリック
- アクティブになっているタブのURLを取得
- 取得したURLを元にリクエストを投げる
- レスポンスの情報を表示する
・・・みたいな処理になるはず。
<template>
<div>
<dl>
<dt>currentUrl</dt>
<dd>{{ currentUrl }}</dd>
</dl>
<dl>
<dt>originDomain</dt>
<dd>{{ originDomain }}</dd>
</dl>
<dl>
<dt>responseData</dt>
<dd>request:{{ targetUrl }}</dd>
<dd>{{ responseData }}</dd>
</dl>
</div>
</template>
<script>
export default {
name: 'App',
data () {
return {
rawUrl: '',
originDomain: '',
targetUrl: '',
baseUrl: 'https://api.whoisproxy.info/whois/',
responseData: {}
}
},
created () {
this.initialize()
this.urlRequest(this.targetUrl)
},
methods: {
initialize () {
chrome.tabs.query({ active: true }, (tabs) => {
this.setRawUrl(tabs)
this.setOriginDomain(this.rawUrl)
this.setTargetUrl(this.originDomain)
})
},
setCurrentUrl (tabs) {
this.rawUrl = tabs[0].url
},
setOriginDomain (url) {
let candidate = []
candidate = url.split('/')
this.originDomain = `${candidate[2]}`
},
setTargetUrl (domainData) {
this.targetUrl = `${this.baseUrl}${domainData}`
},
urlRequest (url) {
this.$axios
.get(url)
.then(response => (this.responseData = response))
.catch(error => console.log(error))
},
}
}
</script>
<style>
html {
min-width: 400px;
min-height: 400px;
}
</style>
ところがレスポンスの内容が見たことのある HTML
のみ・・・
想定通りであれば JSON
が返ってくるはず。
■ 結論
おそらくですが、 Content Security Policy
に引っかかったようです。
manifest.json
のContent-Security-Policyあたりが怪しいですね。
(background.js
を利用すればできるのかもしれませんが力尽きました・・・)
良い方法があればご教示いただけると大変ありがたいです。
参考:
https://developer.chrome.com/docs/apps/contentSecurityPolicy/
https://developers.google.com/web/fundamentals/security/csp
https://oxynotes.com/?p=8895
https://xxxx7.com/2014/04/07/152039
◇ まとめ
Chrome extensionは javascript
で簡単に作れる。(jQueryやモダンなAngular・Reactでもできそう)
ただし外部リソースの利用については注意が必要である。