カテゴリー
JavaScript

Chrome ExtensionのService_WorkerとContent_Script

Service_Worker(Background)とContent_Script(DOM側)の関係が少し分かりにくかったので、書きとめておきます。

以下ではService_Workerをbackground.js、Content_Scriptをcontent.jsとファイル名で呼びます。

content.jsにはDOM(Document Object Model)とのやりとりに関するコード、background.jsにはその他の処理を記述する、と捉えると分かりやすいです。

content.jsとbackground.js間のやりとりはmessagesを介して行います。

background.jsからcontent.jsへは現在アクティブなtab情報を利用してmessagesを送るので、chrome.tabs.sendMessages APIを使います。

// background.js

function setListenerOnMenus() {
  chrome.contextMenus.onClicked.addListener( async (info, tab) => {
    chrome.tabs.sendMessage(tab.id, {
      // some message. can be blank
      something: "some message",
      somethingElse: "another message"
    }, (response) => {
      // receiver of the message (content.js) can return a respond
      // in a callback
      // background.js can respond to that response here...
      let context = response.context;
      let url = response.url;
      // do something with it!...
    }
  }
}
// content.js
chrome.runtime.onMessage.addListener( (request, sender, sendResponse) => {
  // request contains messages sent from background.js. Can retrieve as below...
  let something = request.something;
  let somethingElse = request.somethingElse;
  
  // also, content.js can interact/manipulate DOM element and communicate it back to background.js
  let url = window.location.href;
  let context = window.getSelection().anchorNode.data;
  // can communicate back in the callback below...
  sendResponse({
    context: context,
    url: url
  });
}

background.jsにはonStartup listenerを追加することで、extension使用時に起動するようになります。

// background.js

chrome.runtime.onStartup.addListener(() => {
  setup();
});
カテゴリー
JavaScript

Chrome Extensions v3からGoogle Drive APIを操作する

Manifest v2とv3ではだいぶ実装方法が異なるようで、Web上にはv3の情報が少なかったため、最小実装の方法を調べて実装してみました。

(結局、Query Stringをどう書くのかを調べるのが一番大変だった、、というAPIあるある、、でしたがw)

AuthTokenをゲットする方法は前回のpostを参照ください。

fileをGoogle Drive内の特定のParentフォルダ内に作成するには以下のコードを使います。変数parentはファイルidです。

ちなみにGoogle Driveではfolderとfile、どちらもfileとして扱い、mimeTypeによって区別されます。

公式DocumentによるとSheets APIは現時点では特定のフォルダにspreadsheetを作成することができないため、作成後にファイルを移動するか、Drive APIを使用します。

また、このようなネットワーク通信などのAsynchronous(非同期)な処理はPromiseを使って、コールする時にasync/awaitできるようにしておくと勝手が良いです。

// using Drive API
function createSpreadsheet(token, parent, fileName) {
  return new Promise((resolve, reject)=>{
    const fileMetadata = {
      "name" : fileName,
      "mimeType" : "application/vnd.google-apps.spreadsheet",
      // by specifying the parents, the API will create the file in that folder
      "parents" : [parent]
    }
    fetch("https://www.googleapis.com/drive/v3/files", {
      method: "POST",
      headers: {
        "Content-Type" : "application/json", 
        "Authorization" : `Bearer ${token}`
      },
      body: JSON.stringify(fileMetadata)
    }).then((document)=>{
      return document.json();
    }).then(async (result)=>{
      menus[result.name] = result.id;
      await initialSetupOfSheet(token, result.id);
      resolve();
    }).catch((error)=>{
      console.log("in createSpreadsheet: ", error);
      reject(error);
    });
  });
}

以下は、ファイル名でサーチするコード。

ちなみに、Google Drive APIではsearch query部分を以下のようにq=mimeType=とし、また、’application/vnd.google-apps.spreadsheet’とクォートで囲まなければ認識しないようで、URLSearchParamsをすんなりと使わせてくれなかったためこのようにしています。

function getFileByName(token, fileType, fileName) {
    fetch(`https://www.googleapis.com/drive/v3/files?fields=files(name,id,kind,mimeType,trashed)&q=mimeType='application/vnd.google-apps.${fileType}'+and+name+contains+'${fileName}'`, {
      method: "GET",
      headers: { 
        "Authorization" : "Bearer " + token,
      },
      // can't have body in this method
    }).then((response) => {
      return response.json();
    });
}

Parentフォルダ内のファイルをゲットするコード

function getTheFilesOrCreateIfNone(token, fileId){
  return new Promise((resolve, reject)=>{
    fetch(`https://www.googleapis.com/drive/v3/files?q="${fileId}"+in+parents&mimeType='application/vnd.google-apps.spreadsheet'&fields=files(name,kind,id,mimeType,trashed)`, {
      method: "GET",
      headers: {
        "Authorization": "Bearer " + token
      },
    }).then((result)=>{
      console.log("checking if working: ", result);
      return result.json();
    }).then(async (result)=>{
    // do what ever...
      console.log(result.files);
      console.log("files including in trash: ", result.files.length);
      let files = result.files.filter((file)=>{
        return file.trashed == false;
      })
      // if there is (are) spreadsheet(s), set the context menu
      if (files.length > 0) {
        for (const file of files) {
          menus[file.name] = file.id;
        }
      } else {
        // create new spreadsheet called "Words" in the app folder
        await createSpreadsheet(token, _folderId, "Words");
      }
      // success
      setMenus();
      resolve();
    }).catch((error)=>{
      // fail
      reject(error);
    });
  });
}

Spreadsheetにデータを挿入するためのコード

function writeToSheet(token, fileId, text, context, url) {
  console.log("writing data to the sheet");
  return new Promise((resolve, reject) => {
    const range = "A1:E1";
    const date = Date();
    const bodyData = {
      "range" : range,
      "majorDimension" : "ROWS",
      "values" : [
        [text, context, "", url, date]
      ]
    };
    fetch(`https://sheets.googleapis.com/v4/spreadsheets/${fileId}/values/${range}:append?insertDataOption=INSERT_ROWS&valueInputOption=RAW`, {
      method: "POST",
      headers: {
        // Content-Type is needed, otherwise "parse error"
        "Content-Type" : "application/json", 
        "Authorization" : `Bearer ${token}`
      },
      body: JSON.stringify(bodyData),
    }).then((response) => {
      return response.json();
    }).then((response) => {
      console.log(response);
      resolve();
    }).catch((error) => {
      console.log(error);
      reject("in writeToSheet", error);
    });
  });
}
カテゴリー
JavaScript

Chrome ExtensionsでOAuth2 Authorization

OAuth2 AuthorizationにはExtensionのKeyが必要です。Keyを入手するには、

まず、ベーシックなChrome Extensionを作成し、chrome://extensions/ページのPack Extensionでパックします。

crxファイルとpemファイルが作成されるので、chrome://extensions/ページ内にcrxファイルをドラッグするとExtensionがインストールされます。

macOSの場合は、/Users/{username}/Library/Application Support/Google/Chrome/Default/ExtensionsにIDと同じ名前のフォルダーが作成されています。その中のmanifest.jsonにあるkeyをコピーし、開発中のExtensionのmanifest.jsonにコピーペーストします。

“permissions”に”identity”と”identity,email”を追加します。

// manifest.json
{
    "manifest_version" : 3,
    "name" : "Save The Word",
    "version" : "1.0",
    "description" : "A word saving app",

    "background" : {
        "service_worker" : "scripts/background.js"
    },

    "permissions" : [
        "contextMenus",
        "identity",
        "identity.email"
    ],

    "icons" : {
        "16" : "images/book_16.png",
        "32" : "images/book_32.png",
        "48" : "images/book_48.png",
        "128" : "images/book_128.png"
    },

    "key": "YOUR_VERY_LONG_KEY_HERE",

    "content_scripts" : [
        {
            "js" : ["scripts/content.js"],
            "run_at" : "document_idle",
            "matches" : [
                "<all_urls>"
            ]
        }
    ]
}

次に、インストールされたExtensionを削除し、Load Unpackedから開発中のExtensionを開くことで、Extensionの開発を続けることができます。

background.js内に以下を記述し、service_workerのコンソールにidentity Objectがプリントすることで、上手く働いているかを確認出来ます。

console.log(chrome.identity);

次にhttps://console.cloud.google.com/でプロジェクトを作成し、

APIs & ServicesのOAuth consent screenでExternalを選択し、Createします。

次に、CredentialsのCREATE CREDENTIALSからOAuth Client IDを選択し、

Chrome appを選択、Client名とExtension IDを入力します。

次に、LibraryからGoogle People APIをEnableします。

CREATE CREDENTIALSでAPI Keyを選択します。

manifest.jsonに以下を追記します。

// manifest.json

"oauth2" : {
    "client_id" : "YOUR_CLIENT_ID",
    "scopes" : [
        "profile email",
        "https://www.googleapis.com/auth/contacts"
    ]
},

background.jsに以下を追記すると、tokenがプリントされます。

chrome.identity.getAuthToken({ interactive: true }, (token) => {
  console.log("got token: ", token);
});

このtokenを使ってspreadsheets apiでsheetを作成するには、

chrome.identity.getAuthToken({ interactive: true }, (token) => {
  createSheet(token);
});

function createSheet(token) {
  const data = {
    // requires "properties" to be the most outer key
    "properties" : {
      "title" : "Test Sheet"
    }
  }
  fetch("https://sheets.googleapis.com/v4/spreadsheets", {
    method: "POST",
    headers: {
      "Content-Type" : "application/json",
      "Authorization" : `Bearer ${token}`
    },
    body: JSON.stringify(data)
  }).then((document) => {
    console.log(document.json());
  });
}

Google Drive APIの実装はこのPostをご参照ください。

カテゴリー
JavaScript

Chrome Extensionを作ってみた

Chrome Extensionを作る機会があったので、書きとめておきます。

まずはプロジェクトフォルダを作成し、manifest.jsonというJSONファイルを作成します。このファイルはフォルダのrootに置く必要があります。なお、このJSONファイル内のコメントはChrome Web Storeにアップロードする前に削除する必要があります。

このJSONファイルの必須Keyは”manifest_version”、”name”、”version”の三つです。

その他の色々なアクション、JacvaScriptファイルやアイコン画像の在りか(path)についてもここに記述します。

“background” 内の”service_worker”はTabに表示されるWeb pageとは関係なく実行されるJavaScriptを記述し、”content_scripts”はTabに表示されるWeb pageを操作するためのJavaScriptを記述します。”run_at”はこのscriptが実行されるタイミングを指定します。”document_start”, “document_end”, “document_idle”の三つから選択します。

manifestの仕様

// manifest.json
{
    "manifest_version" : 3,
    "name" : "Save The Word",
    "version" : "1.0",
    "description" : "A word saving app",

    // background works separate from the web page loaded in the tab
    "background" : {
        "service_worker" : "scripts/background.js"
    },

  // content_scripts works to interact and manipulate the web page loaded in the tab
    "content_scripts" : [
        {
            "js" : ["scripts/content.js"],
            "run_at" : "document_idle",
            "matches" : [
                "<all_urls>"
            ]
        }
    ]
}
// scripts/content.js

console.log("hello");

chrome://extensions/にアクセスし、Developer modeをオン、Load unpackedからプロジェクトフォルダを読み込むと、https://から始まるあらゆるサイトを読み込む時に、ブラウザのコンソールに”hello”がプリントされると、content.jsが上手く読み込まれていることが確認出来ます。

Web page内のテキストを右クリックすると、chromeのcontext menuにextensionが表示され、そのテキストを使って何かをするためのコードは以下。

var menus = {
    "Save" : "Save",
    "Copy" : "Copy"
};

chrome.runtime.onInstalled.addListener( () => {
    for(let key of Object.keys(menus)) {
        chrome.contextMenus.create({
            id: key,
            title: key,
            type: "normal",
            contexts: ["selection"]
        });
    }
});

chrome.contextMenus.onClicked.addListener( (item, tab) => {
    const text = item.selectionText;
    console.log(text);
});

selectionTextの周囲の文章もゲットしたい場合は、DOMを操作できるcontent_scriptへメッセージを送りたいため、以下のようにします。ちなみに、content_scriptからbackgroundへ送る場合はchrome.runtime APIを使いますが、backgroundからcontent_scriptへ送る場合はchrome.tab APIを使ってtabを指定する必要があります。公式document

// in background.js
chrome.contextMenus.onClicked.addListener(function(item, tab){
    const text = item.selectionText;
    chrome.tabs.sendMessage(tab.id, { greeting: text });
});

// in content.js
chrome.runtime.onMessage.addListener( (request, sender, sendResponse) => {
    console.log("selected word: ", request.greeting);
    console.log("selected context: ", window.getSelection().anchorNode.data);
});

service_workerのコンソールの表示はこちらをクリック

カテゴリー
JavaScript PHP

PHP変数をJavaScriptで扱うには(そしてその逆も)

一旦Dom Elementに値を保持させて、その後JavaScriptでその値をゲットする方法が良さそう。このリンク