📘

Chrome拡張機能の作る際にクリップボードにアクセスする方法

2023/06/27に公開

拡張機能を今まで作ったことがなかったので、とりあえず簡単なものから...と考えて作ってみましたが、クリップボードにアクセスする方法がわからず、少しハマりました。

作ろうとしたもの

ショートカットキーで、開いているページのリンクを[タイトル](URL)形式で、クリップボードにコピーする拡張機能。

ハマった原因

  • manifest v2 と manifest v3 の違い
  • NavigatorWorkerNavigatorの違い
  • バックグラウンドスクリプトとコンテンツスクリプトについて知らなかった

簡単なまとめ

Navigatorと違って、WorkerNavigatorはクリップボードAPIを提供していません。
https://developer.mozilla.org/ja/docs/Web/API/Clipboard_API
バックグラウンドスクリプトからコンテンツスクリプトへのメッセージングを使用し、コンテンツスクリプト側にあるNavigatorを動かしてクリップボードAPIにアクセスする必要があります。
MDNをよく読もう!

コンテンツスクリプト

開いているウェブページのコンテンツ(DOM)にアクセスし、その内容を読み取ったり変更したりできるスクリプト。
そのページでロードされたJavaScriptで定義された変数にはアクセスできない。
https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Content_scripts

バックグラウンドスクリプト

拡張そのもののコンテキストで実行されるスクリプト。
開いているページのコンテキストで実行されるコンテンツスクリプトとは対照的。
Webページのコンテンツにアクセスすることはできないが、WebExtension JavaScript
APIには全てアクセスできる。(コンテンツスクリプトは一部だけ)
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Background_scripts

Navigator インターフェイスは、ユーザーエージェントの状態や身元情報を表します。

クリップボードAPIを提供している。
https://developer.mozilla.org/ja/docs/Web/API/Navigator

WorkerNavigator

WorkerNavigator インターフェイスは Navigator インターフェイスのサブセットで、ワーカー (Worker) からアクセスできるものです。

クリップボードAPIを提供していない。
https://developer.mozilla.org/ja/docs/Web/API/WorkerNavigator

なぜバックグラウンドスクリプトからNavigatorにアクセスできないのか

Navigatorは、Webページのコンテキストに存在するwindowオブジェクトの子オブジェクトであるため、バックグランドスクリプトからはアクセスできないからです。
一方で、WorkerNavigatorはWebページのコンテキスト外にあるWeb Worker環境のオブジェクトであるため、アクセスすることができます。
ここら辺の理解が弱い、特にWeb Workerについては勉強不足。

作り方

拡張機能のフォルダを適当に作成します。
今回の拡張機能には、三つのファイル(manifest.json,service-worker.js,content-script.js)が必要です。
service-worker.jsは、バックグラウンドスクリプトのことです。

1. manifest.json

拡張機能の情報と設定を記述するファイル

{
"name": "TitleURL Copier",
"version": "1.0",
"manifest_version": 3,
"description": "Copies the URL and title of the current page to the clipboard in a specified format",
"background": {
  "service_worker": "service-worker.js"
},
"content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content-script.js"]
    }
  ],
"commands": {
  "inject-script": {
    "suggested_key": {
     "default": "Ctrl+Shift+Y",   
     "mac": "Command+Shift+Y"  
    },
    "description": "Inject a script on the page"
  }
},
"permissions": ["clipboardWrite","activeTab"]
}

このファイルでは、ショートカットキーをCtrl+Shift+Yに設定しています。
また、クリップボードへの書き込み権限とアクティブになっているタブへのアクセス権限を要求しています。

2. service-worker.js

コマンドが発行されたときに、現在アクティブなタブのタイトルとURLを取得。
Markdown形式のリンクとして整形、Content Scriptにメッセージを送信するスクリプトです。

chrome.commands.onCommand.addListener(function(command) {
  chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
    var activeTab = tabs[0];
    var title = activeTab.title;
    var url = activeTab.url;
    var result = '[' + title + '](' + url + ')';
    // Send a message to the content script
    (async () => {
      chrome.tabs.sendMessage(activeTab.id, {command: "copy", text: result})
    })();
  });
})
chrome.commands.onCommand.addListener(function(command) {

特定のコマンド(例えば、キーボードショートカット)が実行されたときに呼び出されるリスナーを登録しています。

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {

chrome.tabs.query()メソッドは、特定のクエリに一致するタブを検索します。
この例では、現在アクティブで、かつ現在のウィンドウに存在するタブを検索しています。
このメソッドは、クエリに一致するすべてのタブを要素とする配列を引数としてコールバック関数を呼び出します。

(async () => {
  chrome.tabs.sendMessage(activeTab.id, {command: "copy", text: result})
})();

この非同期関数内では、chrome.tabs.sendMessage()メソッドを使用して、現在アクティブなタブのContent Scriptにメッセージを送信します。

3. content-script.js

他のスクリプト(今回はservice-worker.js)から送られてくるメッセージを待ち受けます。
そのメッセージが特定のコマンド(この場合は"copy")を含む場合、メッセージに含まれるテキストをクリップボードに書き込みます。

chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
    if (message.command == "copy") {
      navigator.clipboard.writeText(message.text).then(function() {
        /* clipboard successfully set */
      }, function() {
        /* clipboard write failed */
      });
    }
});

4. 諸々の設定

これらのファイルを作成したら、Chromeブラウザでchrome://extensions/を開き、右上の「デベロッパーモード」をオンにします。
その後、「パッケージ化されていない拡張機能を読み込む」をクリックし、作成した拡張機能のディレクトリを選択します。

これで拡張機能がインストールされ、設定したショートカットキーを押すと、現在アクティブなタブのタイトルとURLが指定した形式でクリップボードにコピーされるはず。

Discussion