wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

Plasmoでブラウザ拡張を作ってみた

最近、社内で記事の共有などをする機会が増え、ブラウザのブックマークやGoogle Keepを使用していたが、仕事とプライベートで使用しているブラウザ、Googleアカウントなどが違ったりして、どこに保存したか分からなくなることが多かったので、自作でchrome拡張を作成して運用してみることにした。

まだ作成途中だが、結構簡単に作れたので記事にしてみた。

リポジトリ

github.com

作成したもの

  • URLを保存できる
  • URLの追加/削除が簡単にできる
  • 簡単にURLをMarkdown形式でコピーできる
  • 右クリックで追加/削除できる

使用技術

Plasmoを使用して開発

docs.plasmo.com

  • ブラウザ拡張機能を作成するためのReactフレームワーク
  • TypeScript & Reactで簡単にChrome拡張が作成できる
  • 従来のChrome拡張で必要だった設定周りフレームワーク側で抽象化されている

コードの例

以下のコマンドでプロジェクトを作成

$ pnpm create plasmo

プロジェクトの作成時は以下のようなコードが生成される

import { useState } from "react"

function IndexPopup() {
  const [data, setData] = useState("")
  return (
    <div>
      <h2>
        Welcome to your{" "}
        <a href="https://www.plasmo.com" target="_blank">
          Plasmo
        </a>{" "}
        Extension!
      </h2>
      <input onChange={(e) => setData(e.target.value)} value={data} />
      <a href="https://docs.plasmo.com" target="_blank">
        View Docs
      </a>
    </div>
  )
}
export default IndexPopup

上記のコードで、こんな感じのchrome拡張を作成できる

実装

Plasmoを作ってブラウザ拡張を作っていく。

まず、UIはさくっと作るためにTailwind CSSを利用。PlasmoでTailwind CSSを使用するには以下で参考に実装。

docs.plasmo.com

Plasmoで現在ブラウザで開いているURLの取得は以下のコードで取得できる。

popup.tsx

  const [currentPage, setCurrentPage] = useState<Data>({
    title: "",
    url: "",
    favIconUrl: ""
  })

  useEffect(() => {
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      const activeTab = tabs[0]
      if (!activeTab) return

      setCurrentPage({
        title: activeTab.title,
        url: activeTab.url,
        favIconUrl: activeTab.favIconUrl
      })

  }, [items, removeButton])

ここで取得した値をPlasmoのStorage APIを使って保存できる。コードは、以下の通り。

popup.tsx

import { useStorage } from "@plasmohq/storage/hook"

(...略)

  const [items, setItems] = useStorage<Data[]>("saveItems", [])

これで以下の通りUIを整えれば、まずは最小限な実装で以下みたいにブラウザ拡張が作れます。

www.youtube.com

次に右クリックのContextMenuに追加するには、Background Service Workerを利用。

docs.plasmo.com

コードは以下のように追加。

background.ts

chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "save",
    title: "ページを追加/削除",
    contexts: ["all"],
  });
});

ContextMenuをクリックした時の動作追加のコードはは以下の通り

background.ts

chrome.contextMenus.onClicked.addListener(async (info) => {
  if (info.menuItemId === "save") {
    const activeTab = await storage.get<Data>("activeTab");
    if (!activeTab) return;

    const items = (await storage.get<Data[]>("saveItems")) ?? [];
    const isCurrentPageURL = items.some((v) => v.url === activeTab.url);
    if (isCurrentPageURL) {
      const newItems = items.filter((v) => v.url !== activeTab.url);
      await storage.set("saveItems", newItems);
    } else {
      items.push(activeTab);
      await storage.set("saveItems", items);
    }
    chrome.runtime.sendMessage({
      type: "UPDATE",
    });
  }
});

後は、backgroundで現在の開いているURLを取得するのは以下のコードで実装可能

background.ts

function getActiveTabInfo() {
  const queryInfo = {
    active: true,
    currentWindow: true,
  };

  chrome.tabs.query(queryInfo, (tabs) => {
    const activeTab = tabs[0];
    console.log("Active Tab URL:", activeTab.url);
    if (!activeTab) return;

    storage.set("activeTab", activeTab);
  });
}

// アクティブタブが変更されたときのイベントリスナー
chrome.tabs.onActivated.addListener((activeInfo) => {
  getActiveTabInfo();
});

// タブが更新されたときのイベントリスナー
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  // タブが完全に読み込まれたときに情報を取得
  if (changeInfo.status === "complete") {
    getActiveTabInfo();
  }
});

// 初回起動時にもタブ情報を取得
getActiveTabInfo();

ここまで実装すると以下の動画みたいに右クリックからURLの保存が実装できる。

www.youtube.com