kansiho's blog

ruby, python, javascript. Rails, wordpress, OpenCV, heroku...

コードと公式ドキュメントから読む:Chrome拡張機能開発、ゼロから解説

f:id:serendipity4u:20170618125453j:plain

Chrome拡張機能開発のイロハからマネタイズモデルまで説明します。

Chrome 拡張機能を利用するメリット

拡張機能にしか提供されていないChromeのネイティブのAPI群を利用してアプリケーションが開発・連携できるところ。 例えば、Chrome Tab API で開かれているタブのタイトルを取得したり、開かれているページのDOMを取得するなど。

Chrome拡張機能の構成

ファイル構成から見るより、manifest.jsonから見る方がわかりやすいので、次はmanifest.jsonを紹介する。

manifest.json の中身

それぞれの意味の説明は後述する。

{
  "manifest_version": 2,
  "name": "Sample Extension",
  "description": "This extension is sample.",
  "version": "1.0",
  "icons": {
    "16": "images/icon/icon_16.png",
    "48": "images/icon/icon_48.png",
    "128": "images/icon/icon_128.png"
  },
  "background": { // 解説
    "scripts": [
      "js/background.js"
    ],
    "persistent": false
  },
  "browser_action": {  // 解説
    "default_icon": {
      "19": "images/icon/icon_19.png",
      "38": "images/icon/icon_38.png"
    },
    "default_title": "Sample Extension",
    "default_popup": "popup.html" // 解説
  },
  "content_scripts": [ // 解説
    {
      "matches": [ // 解説
        "http://*/*",
        "https://*/*"
      ],
      "css": ["css/sample.css"],
      "js": ["js/content_scripts/sample.js"]
    }
  ],
  "permissions": [ // 解説
    "tabs",
    "https://*/*",
    "http://*/*",
    "storage"
  ]
}

上記は最小構成に近いが、公式ドキュメントによると以下の項目がある。

{
  // Required
  "manifest_version": 2,
  "name": "My Extension",
  "version": "versionString",

  // Recommended
  "default_locale": "en",
  "description": "A plain text description",
  "icons": {...},

  // Pick one (or none)
  "browser_action": {...},           
  "page_action": {...},

  // Optional
  "author": ...,
  "automation": ...,
  "background": {
    // Recommended
    "persistent": false
  },
  "background_page": ...,
  "chrome_settings_overrides": {...},
  "chrome_ui_overrides": {
    "bookmarks_ui": {
      "remove_bookmark_shortcut": true,
      "remove_button": true
    }
  },
  "chrome_url_overrides": {...},
  "commands": {...},
  "content_capabilities": ...,
  "content_scripts": [{...}],
  "content_security_policy": "policyString",
  "converted_from_user_script": ...,
  "current_locale": ...,
  "devtools_page": "devtools.html",
  "event_rules": [{...}],
  "externally_connectable": {
    "matches": ["*://*.example.com/*"]
  },
  "file_browser_handlers": [...],
  "file_system_provider_capabilities": {
    "configurable": true,
    "multiple_mounts": true,
    "source": "network"
  },
  "homepage_url": "http://path/to/homepage",
  "import": [{"id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}],
  "incognito": "spanning, split, or not_allowed",
  "input_components": ...,
  "key": "publicKey",
  "minimum_chrome_version": "versionString",
  "nacl_modules": [...],
  "oauth2": ...,
  "offline_enabled": true,
  "omnibox": {
    "keyword": "aString"
  },
  "optional_permissions": ["tabs"],
  "options_page": "options.html",
  "options_ui": {
    "chrome_style": true,
    "page": "options.html"
  },
  "permissions": ["tabs"],
  "platforms": ...,
  "plugins": [...],
  "requirements": {...},
  "sandbox": [...],
  "short_name": "Short Name",
  "signature": ...,
  "spellcheck": ...,
  "storage": {
    "managed_schema": "schema.json"
  },
  "system_indicator": ...,
  "tts_engine": {...},
  "update_url": "http://path/to/updateInfo.xml",
  "version_name": "aString",
  "web_accessible_resources": [...]
}

Browser Action

Browser actionとは、Chrome拡張機能の3つのうちの1つで、もっともベーシックなタイプである、ツールバー右に並んで表示される拡張機能のことを指す。

f:id:serendipity4u:20170618100209p:plain

これらのアイコンをクリックすると、例えば下図のようにポップアップが表示されるが、

f:id:serendipity4u:20170618100312p:plain

このポップアップの内容は

  "browser_action": {
    "default_icon": {
      "19": "images/icon/icon_19.png",
      "38": "images/icon/icon_38.png"
    },
    "default_title": "Sample Extension",
    "default_popup": "popup.html"
  },

で定義されているというわけだ。 popup.htmlは、 例えばこのような形になっている。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>Sample Extension</title>
    <link rel="stylesheet" href="css/popup.css" type="text/css">
    <script src="js/jquery.js"></script>
    <script src="js/popup.js"></script>
  </head>
  <body'>
    <p>Title: <span id="title"></span></p>
  </body>
</html>

このように、外部スタイルシートを読み込んだり、普通のWebページ制作と何ら変わらない。

Content Script

Chromeで開いているWebページにおいて動作させる」cssjavascriptを定義する。 どうしてこれが必要なのかというと、 popup.html 上で実行するjavascriptも、backgroundで実行するjavascriptも、直接的には開いているページのDOM 要素にアクセスできないからだ。なぜかというと、既存のWebページと拡張機能が注入するjsファイルなどとのコンフリクトを避けるためだ(jQueryの2重読み込みなど)。 一部のChrome Tab API を利用できない制限があるのだが、これはbackgroundに処理を渡すことで、解決できる。 また、おそらくセキュリティ上の理由から、ページ内で定義されている変数や関数にはアクセスができない。

"content_scripts": [ // 解説
    {
      "matches": [ // 解説
        "http://*/*",
        "https://*/*"
      ],
      "css": ["css/sample.css"],
      "js": ["js/content_scripts/sample.js"]
    }

このmatches は、常にExtensionを実行したいわけではないだろう、例えばpdfページとか、google.comでだけ動作させたいとか、localhostでは実行したくないとか、そういう需要もあるであろうという了解のもとにある項目で、content scriptを実行するURL条件を指定することができる。

公式ドキュメントでは

Required. Specifies which pages this content script will be injected into. See Match Patterns for more details on the syntax of these strings and Match patterns and globs for information on how to exclude URLs.

な項目として書かれている。

Background

Backgroupd Pagesはスクリプトを裏側でずっと実行できるようにするための仕組み。 Chromeが立ち上がっている間、ずっと実行しておきたい処理や、保持しておきたい状態を持つときに利用する。Content Script 内で、API利用制限(前述)があるので、backgroundに回したりする。

 "background": { // 解説
    "scripts": [
      "js/background.js"
    ],
    "persistent": false
  },

persistent: false は、 必要になったら動き、不必要になったら遊休状態になり、メモリが開放させる設定を表す。 通常、persistent: false 設定にする。このように常駐しないよう設定したものをevent ページと呼ぶ。 拡張機能によってはeventページでは難しいものもあるので、絶対にeventページにしなければいけないということはない。

Content Script から Backgroundに処理を回す

さて、Content Script には制限があるため、Backgroundに処理を渡すことがあると書いた。今から、その具体的な方法を説明する。

Content Scriptからメッセージを送信し、Backgroundに送る

メッセージを送るには、chrome.runtime.sendMessage API を利用する。

// Content Script で実行する js ファイル
chrome.runtime.sendMessage('Hellooo');

公式ドキュメントによると

chrome.runtime.sendMessage(string extensionId, any message, object options, function responseCallback)

で、レスポンスコールバックなどを定義できる。

chrome.runtime.sendMessage({ type: 'あいさつ', content: 'こんにちは〜', name: '花子'}, 
  function(response){
    console.log(response);
  } 
);

のように、ハッシュをメッセージとして送ることもできる。

BackgroundでContent Scriptから送信されたメッセージを受け取る

メッセージを受け取るには、chrome.runtime.onMessageAPI を利用してリスナーを設定する。

chrome.runtime.onMessage.addListener(function (message) {
  console.log(message);
});

リクエストの分岐によって処理を変えたい時は、

chrome.runtime.onMessage.addListener(
  function (request, sender, sendResponse) {
    switch (request.type) {
    case "あいさつ":
      // 処理
      break;
    case "その他":
      // 処理
      break;
    default:
      console.log("Error: Unkown request.")
      console.log(request);
    }
  }
);

のようにすれば実現できる。

公式ドキュメントでは

Fired when a message is sent from either an extension process (by runtime.sendMessage) or a content script (by tabs.sendMessage). ( runtime.SendMessage もしくは Content Script の tabs.sendMessage によってメッセージが送られてきた時に、発火する。)

とある。tabs.sendMessageについては後述する。

BackgroundからContent Script にメッセージを送信する

さて、Content Script から Backgroundにメッセージを送信することについて書いた。 それでは、逆はどうなるのか。 BackgroundからContent Scriptに処理を渡すとき、chromeにタブがたくさん開かれている場合は、どのタブにおけるContent Script か指定する必要がある。 タブを対象にメッセージを送る時は、 chrome.tabs.sendMessage API を利用する。chrome.tabs.sendMessage()の第一引数には、タブ固有のIDを指定する。

「今開いているタブ」に送ると指定する時

送信側で、chrome.tabs.query API を利用し、tabs という変数の中に今開いているタブの情報を配列で受け取って、tabs[0].id とすれば、今開いているタブのIDが取得できる。つまり、以下のようになる。

//backgroundのjs ( 送信側 )

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
    console.log(response.farewell);
  });
});

content script側の受け取り方は、あとはbackgroundがcontent scriptからメッセージを受け取る時と同様で、 

chrome.runtime.onMessage.addListener(function (message) {
  console.log(message);
});

とonMessage.addListener を設定すればよい。

Tabで開いているWebページにhtmlを注入するには?

いくつか方法がありますが、もっとも直感的かつメンテが容易と思われたのが、iframeを使う方法です。

まず、embedded.htmlに、

<!DOCTYPE html>
<html><head>
    <title>Embedded Hello World</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

    <link href="test.css" rel="stylesheet" type="text/css">
    <script type="text/javascript" src="test.js"></script>
  </head><body>
    <p>Hello World!</p>
  </body>
</html>

など記述。

manifest.json

 "web_accessible_resources": ["embedded.html"]

と記載しアクセスできるようにします。 さらにmanifest.jsonに、iframeを注入するjsファイルも追加します(backgroundではなく、content scriptであることに注意)、

"content_scripts":          [ {
        "js":       [ "iframeInjector.js" ],

その(iframeInjector.jsの) 中身はこうです、

iframeInjector.js:

var iFrame  = document.createElement ("iframe");
iFrame.src  = chrome.extension.getURL ("embedded.htm");

document.body.insertBefore (iFrame, document.body.firstChild);

これで、ページトップにembedded.html が表示されるというわけです。 プログラマだけでなく、デザイナーに優しい作りですね。

出典はここです: javascript - Adding complex HTML using a Chrome content script - Stack Overflow

ブラウザアクションボタンを押したらContent Scriptで開いているページにjavascriptを注入するには?

evernote拡張機能みたいに、検索フォーム横の拡張機能のボタンを押したら、開いているページに新しいhtml要素を表示するようなことを、javascriptを利用して行いたいわけです。 f:id:serendipity4u:20170709172916p:plain

background.js

// ブラウザアクションを押したら...
chrome.browserAction.onClicked.addListener(function (tab) {
    // for the current tab, inject the "inject.js" file & execute it
    chrome.tabs.executeScript(tab.id, {
        file: 'inject.js'
    });
});

inject.js

(function() {

    // just place a div at top right
    var div = document.createElement('div');
    div.style.position = 'fixed';
    div.style.top = 0;
    div.style.right = 0;
    div.textContent = 'Injected!';
    document.body.appendChild(div);

    alert('inserted self... giggity');

})();

manifest.json

{
  ...
  "background": {
    "scripts": [
      "background.js"  // 重要
    ],
    "persistent": true  // 重要
  },
  "browser_action": {
    "default_title": "inject test" // 重要. default_popupのhtml を設定しないこと. そっちが優先されて, popup.htmlを表示して終わってしまいます.
  },
  "permissions": [
    "https://*/*",
    "http://*/*",
    "tabs"  // 重要
  ]
}

参考:  https://gist.github.com/danharper/8364399

他のchrome拡張機能のソースを見る

構成を勉強したり、セキュリティ面で不要な通信などが行われていないか調べるため、利用している他人の作ったchrome拡張のコードを見たいという場合、わざわざインストールしなくても、ソースコードを見ることができる。

chrome.google.com

この拡張機能を使うと、chrome 拡張ストアで、詳細ページを開くだけで、ソースコードを見ることができるようになる。

Chrome 拡張のマネタイズモデル

さて、chrome拡張開発者たちは、楽しい以外にどんなインセンティブを持っているのか。

1) 広告表示で、マネタイズ

gigazine.net

ポリシー内容としては、
・挙動はユーザーが見て明らかであること
・広告はいずれも何の拡張機能が表示させているものなのか明らかにすること
・本来表示されている広告や、ウェブサイトの機能を妨害しないこと
と定められています。
また、表示させる広告については
・「Chrome Web Store Developer Terms」を破らないこと
・設定によって消せるか、拡張機能やアプリを削除した際に一緒に消えること
・システムメッセージや警告を模倣しないこと
・拡張機能またはアプリを機能が不十分な状態でリリースし、フル機能を使わせるためにユーザーに広告のクリックや個人情報入力を強制させることは禁止
と決められています。

2) 有料拡張で、マネタイズ

free trial がある。でもこんだけほとんど無料(のイメージが強い)だと、なかなか有料だとはやりにくい気もする。例外があったら教えてください。

f:id:serendipity4u:20170618125225p:plain

参考

qiita.com

qiita.com

liginc.co.jp

risaiku.net