Google Chromeを日頃から利用している方は、おそらく拡張機能を使用した経験があるはず。Chromeの拡張機能は、実は自分でも構築することができます。

今回はChromeの拡張機能、特にKinstaでホストするWordPressサイトのプラグイン管理用に、ReactとKinsta APIを使って拡張機能を開発する方法をご紹介します。

Chromeの拡張機能とは

Chromeの拡張機能は、Chromeブラウザにインストールされ、機能を強化するためのプログラムです。ツールバーのシンプルなアイコンボタンから、ブラウジング体験に深く関わる完全に統合される機能までさまざまです。

Chrome拡張機能を構築する

Chromeの拡張機能を作成する手順は、ウェブアプリケーションの開発と似ていますが、manifest.jsonと呼ばれるJSON形式のファイルが必要になります。このファイルが拡張機能の中核となり、設定、権限、組み込む機能を指示します。

まずは、すべてのファイルを格納するフォルダを作成し、このフォルダ内にmanifest.jsonファイルを作成します。

Chrome拡張機能の基本となるmanifest.jsonファイルには、拡張機能の基本設定を定義する主要なプロパティが含まれます。以下は動作に必要なフィールドを含むmanifest.jsonファイルの例です。

{
  "manifest_version": 3,
  "name": "My Chrome extension",
  "version": "1.0",
  "description": "拡張機能の説明文"
}

これを解凍した拡張機能としてChromeに読み込んでテストしてみます。ブラウザでchrome://extensionsに移動し、「デベロッパー モード」に切り替えてから(画面右上のトグルスイッチ)、「パッケージ化されていない拡張機能を読み込む」をクリックします。出現したファイルブラウザで作成したディレクトリを選択します。

デベロッパーモードで「パッケージ化されていない拡張機能を読み込む」をクリックして拡張機能を読み込む
デベロッパーモードで「パッケージ化されていない拡張機能を読み込む」をクリックして拡張機能を読み込む

ユーザーインターフェースを作成していないため、この時点で拡張機能アイコンをクリックしても、何も起こりません。

Chrome拡張機能のユーザーインターフェース(ポップアップ)を作成する

ウェブアプリケーションと同じように、拡張機能のユーザーインターフェース(UI)もHTMLでコンテンツを構造化し、CSSでスタイルを設定して、JavaScriptでインタラクティブな要素を追加していきます。

これらのファイルをすべて活用して、基本的なUIを作成してみます。まずは、HTMLファイル(popup.html)を作成し、テキスト、見出し、画像、ボタンなどのUI要素の構造を定義します。以下のコードを貼り付けます。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Hello World</title>
        <link rel="stylesheet" href="popup.css" />
    </head>
    <body>
        <h1>Hello World!</h1>
        <p>My first Chrome Extension</p>
        <button> id="sayHello">Say Hello</button>
        <script> src="popup.js"></script>
    </body>
</html>

上のコードは、見出し、段落、ボタンを作成するもので、CSSファイルとJavaScriptファイルもリンクされています。続いて、popup.cssファイルにスタイルを追加します。

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: Arial, sans-serif;
    background-color: aliceblue;
    padding: 20px;
}

popup.jsファイルにイベントリスナーを追加し、ボタンをクリックした際にメッセージが表示されるようにします。

const sayHelloBtn = document.getElementById('sayHello');
sayHelloBtn.addEventListener('click', async () => {
    let tab = await chrome.tabs.query({ active: true });
    chrome.scripting.executeScript({
        target: { tabId: tab[0].id },
        function: () => alert('Hello from the extension!'),
    });
});

上のJavaScript コードは、現在アクティブなタブを取得し、Chrome Scripting APIを使用して、「Say Hello」ボタンがクリックされた際に、挨拶を含むメッセージを表示するスクリプトを実行します。これで拡張機能に基本的な対話機能が導入できます。

基本的なテキスト、スタイル、機能を含む、シンプルなポップアップUIの完成です。

最後に、いくつかの権限を追加してmanifest.jsonファイルでポップアップファイルを有効にします。

{
    . . . ,
    "action": {
        "default_popup": "popup.html"
    },
    "permissions": [
        "scripting",
        "tabs"
    ],
    "host_permissions": [
        "http://*/*",
        "https://*/*"
    ]
}

上の設定では、default_popupキーがユーザーが拡張機能と対話する際、popup.htmlがデフォルトのUIになることを指定します。permissions配列には、拡張機能がタブと対話し、ブラウザのスクリプト機能を使用するために必要なscriptingtabsが含まれます。

host_permissions配列は、拡張機能がやり取りできるサイトを指定します。パターンhttp://*/*https://*/*は、拡張機能がHTTPおよびHTTPSプロトコルでアクセスされるすべてのサイトと対話できることを示します。

これらの設定をmanifest.jsonファイルに記述することで、拡張機能がポップアップを表示し、適切にスクリプトを実行するように設定されます。

Chrome拡張機能を再読み込みする

これらの変更をローカルフォルダに反映したら、Chromeに読み込まれた解凍済みフォルダを更新します。Chromeの拡張機能ページを開き、拡張機能の再読み込みアイコン(以下参照)をクリックします。

更新アイコンをクリックして拡張機能を再読み込みする
更新アイコンをクリックして拡張機能を再読み込みする

拡張機能アイコンをクリックすると、ポップアップが表示され、「Say Hello」ボタンをクリックするとメッセージが表示されます。

以上が、Chrome拡張機能の開発に必要な基礎知識です。これをベースにしてサイトのUIを操作したり、APIリクエストを行ったり、URLからデータを取得して特定の操作を実行したりなど、さまざまなことを実現できます。

ReactでChrome拡張機能を構築する

先に触れた通り、Chrome拡張機能の作成はウェブアプリケーション開発に似ているため、Reactのような一般的なウェブフレームワークを使用することができます。

Reactの場合、publicフォルダにmanifest.jsonファイルが生成されます。このフォルダは、Webpack(またはCreate React AppのようなツールでReactが内部で使用する類似バンドラー)によって処理されない静的アセットに使用されます。

Reactアプリケーションをビルドする際、ビルドプロセスはpublicフォルダのすべてのコンテンツをdistフォルダにコピーします。以下、ReactでChrome拡張機能を構築する手順を見ていきましょう。

  1. Reactアプリケーションを作成する。ターミナルで以下のコマンドを実行して、ローカル開発環境Viteを使用する。
npm create vite@latest

プロジェクトに名前を付け、フレームワークにReactを選択します。その後、プロジェクトフォルダに移動して依存関係をインストールします。

cd <project-name>
npm install
  1. Reactプロジェクトのpublicフォルダにmanifest.jsonファイルを作成し、以下の設定を貼り付ける。
{
    "manifest_version": 3,
    "name": "React Chrome extension",
    "description": "Reactで構築したChrome拡張機能",
    "version": "0.1.0",
    "action": {
        "default_popup": "index.html"
    },
    "permissions": [
        "tabs"
    ],
    "host_permissions": [
        "http://*/*",
        "https://*/*"
    ]
}

上のコードには、拡張機能のアイコンがクリックされた際のデフォルトポップアップとして、index.htmlを設定するactionオブジェクトが含まれます。これはReactアプリケーションのビルド時に生成される静的なHTMLファイルです。

  1. Reactアプリケーションを開発。APIリクエストを作成したり、任意のスタイルを与えたり、React Hooksを使用したりなどの作業を行う。
  1. 拡張機能のUIを構築したら、Reactでビルドコマンドを実行(npm run build)。これにより、manifest.jsonファイルやReactが生成したindex.htmlなど、すべてのアセットがdistまたはbuildフォルダに移動する。
  2. 拡張機能をChromeで読み込む。chrome://extensions/を開き、 拡張機能を再読み込みする。

Kinsta APIでサイトのプラグインを管理するChrome拡張機能を構築する

続いて、以下のような外観のChrome拡張機能を構築する手順をご紹介します。

KinstaAPIと相互作用するReactで構築したChrome拡張機能
KinstaAPIと相互作用するReactで構築したChrome拡張機能

拡張機能をクリックすると、古いプラグインのあるMyKinsta上のサイトが一覧表示されます。プラグインの「MyKinstaで表示」ボタンをクリックして、サイトの「テーマとプラグイン」画面に移動して、各プラグインを更新することができます。

Kinsta APIとは

Kinsta APIは、WordPress専用マネージドホスティングなどのKinstaサービスとプログラムでやり取りできる強力なツールです。サイトの作成サイト情報の取得サイトのステータスの取得バックアップの復元参照など、 WordPressの管理に関連するさまざまなタスクを自動化することができます。

Kinsta APIを使用するには、MyKinstaに少なくとも1つのWordPressサイト、アプリケーション、またはデータベースを所有している必要があります。また、アカウントを認証してアクセスするためにAPIキーを生成しなければなりません。

APIキーは以下の手順で作成します。

  1. MyKinstaにログインする
  2. 「APIキー」画面に移動する(右上のユーザー名をクリックし、「企業の設定」>「APIキー」)
  3. APIキーを作成」をクリックする
  4. 有効期限を選択するか、「カスタム」で任意の有効期限を設定
  5. キーに一意の名前を付ける
  6. 生成」をクリックする

APIキーは作成後、コピーして安全な場所に保管してください(パスワードマネージャーの使用を推奨)。APIキーは複数作成することができ、作成したものは「APIキー」画面に表示されます。不要になったAPIキーは、「取り消す」をクリックして削除することも可能です。

Kinsta APIとReactでサイトのプラグインを管理する

まずは、Reactでユーザーインターフェースを構築することから始めます。以下でご紹介する手順には、ReactとAPIインタラクションの基礎知識が必要になります。

環境のセットアップ

まずコードの重複を避けるため、App.jsxファイルでKinsta API URLの定数を定義します。

const KinstaAPIUrl = 'https://api.kinsta.com/v2';

セキュリティ上の理由から、APIキーやMyKinstaの企業IDなどの機密データを.env.localファイルに保存し、ソースコードから安全に除外します。

VITE_KINSTA_COMPANY_ID=あなたの企業ID
VITE_KINSTA_API_KEY=あなたのAPIキー

Kinsta APIでデータを取得する

App.jsxファイルでは、サイトとそのプラグインに関する情報を取得するため、Kinsta APIにいくつかのリクエストを行う必要があります。

  1. 企業のサイトを取得:MyKinstaの企業アカウント上のサイト一覧を取得することから始めます。サイト情報の配列を返すGETリクエストで企業IDを使用します。
    const getListOfCompanySites = async () => {
          const query = new URLSearchParams({
            company: import.meta.env.VITE_KINSTA_COMPANY_ID,
          }).toString();
          const resp = await fetch(`${KinstaAPIUrl}/sites?${query}`, {
            method: 'GET',
            headers: {
              Authorization: `Bearer ${import.meta.env.VITE_KINSTA_API_KEY}`,
            },
          });
          const data = await resp.json();
          const companySites = data.company.sites;
          return companySites;
        }
  2. 各サイトの環境情報を取得:各サイトについて、さらなるリクエストに必要な環境IDを含む環境を取得します。各サイトをマッピングし、/sites/${siteId}/environmentsのエンドポイントにAPI呼び出しを行うことで実行可能です。
     const companySites = await getListOfCompanySites();
        // 各サイトのすべての環境を取得
    
        const sitesEnvironmentData = companySites.map(async (site) => {
          const siteId = site.id;
          const resp = await fetch(`${KinstaAPIUrl}/sites/${siteId}/environments`, {
            method: 'GET',
            headers: {
              Authorization: `Bearer ${import.meta.env.VITE_KINSTA_API_KEY}`,
            },
          });
          const data = await resp.json();
          const environments = data.site.environments;
          return {
            id: siteId,
            name: site.display_name,
            environments: environments,
          };
        });
  3. 各サイトの環境のプラグインを取得:各サイトのプラグインを取得するために環境IDを使用します。マッピング関数と各環境の/sites/environments/${environmentId}/pluginsエンドポイントへのAPI呼び出しを行います。
    // すべてのpromiseが解決するのを待つ
        const sitesData = await Promise.all(sitesEnvironmentData);
    
        // 各環境のすべてのプラグインを取得
        const sitesWithPlugin = sitesData.map(async (site) => {
          const environmentId = site.environments[0].id;
          const resp = await fetch(
            `${KinstaAPIUrl}/sites/environments/${environmentId}/plugins`,
            {
              method: 'GET',
              headers: {
                Authorization: `Bearer ${import.meta.env.VITE_KINSTA_API_KEY}`,
              },
            }
          );
          const data = await resp.json();
          const plugins = data.environment.container_info;
          return {
            env_id: environmentId,
            name: site.name,
            site_id: site.id,
            plugins: plugins,
          };
        });

    それぞれのサイトとそのプラグインに関する基本情報を含むサイトの最終的な配列を返すために使用する関数に、これらのリクエストをまとめます。

    const getSitesWithPluginData = async () => {
      const getListOfCompanySites = async () => {
        const query = new URLSearchParams({
          company: import.meta.env.VITE_KINSTA_COMPANY_ID,
        }).toString();
        const resp = await fetch(`${KinstaAPIUrl}/sites?${query}`, {
          method: 'GET',
          headers: {
            Authorization: `Bearer ${import.meta.env.VITE_KINSTA_API_KEY}`,
          },
        });
        const data = await resp.json();
        const companySites = data.company.sites;
        return companySites;
      }
    
      const companySites = await getListOfCompanySites();
    
      // 各サイトのすべての環境を取得
      const sitesEnvironmentData = companySites.map(async (site) => {
        const siteId = site.id;
        const resp = await fetch(`${KinstaAPIUrl}/sites/${siteId}/environments`, {
          method: 'GET',
          headers: {
            Authorization: `Bearer ${import.meta.env.VITE_KINSTA_API_KEY}`,
          },
        });
        const data = await resp.json();
        const environments = data.site.environments;
        return {
          id: siteId,
          name: site.display_name,
          environments: environments,
        };
      });
    
      // すべてのpromiseが解決するのを待つ
      const sitesData = await Promise.all(sitesEnvironmentData);
    
      // 各環境のすべてのプラグインを取得
      const sitesWithPlugin = sitesData.map(async (site) => {
        const environmentId = site.environments[0].id;
        const resp = await fetch(
          `${KinstaAPIUrl}/sites/environments/${environmentId}/plugins`,
          {
            method: 'GET',
            headers: {
              Authorization: `Bearer ${import.meta.env.VITE_KINSTA_API_KEY}`,
            },
          }
        );
        const data = await resp.json();
        const plugins = data.environment.container_info;
        return {
          env_id: environmentId,
          name: site.name,
          site_id: site.id,
          plugins: plugins,
        };
      });
    
      // すべてのpromiseが解決するのを待つ
      const sitesWithPluginData = await Promise.all(sitesWithPlugin);
      return sitesWithPluginData;
    }

サイトデータの表示

useStateフックで状態を作成し、古いプラグインを含むサイトを保存します。useEffectフックはgetSitesWithPluginData()も呼び出し、コンポーネントがマウントされた際にサイトの情報を抽出します。

useEffectフックで、各サイトをループして古いプラグインのあるサイトをフィルタリングし、状態に格納する関数を作成します。

const [sitesWithOutdatedPlugin, setSitesWithOutdatedPlugin] = useState([]);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
  const checkSitesWithPluginUpdate = async () => {
    const sitesWithPluginData = await getSitesWithPluginData();
    const sitesWithOutdatedPlugin = sitesWithPluginData.map((site) => {
      const plugins = site.plugins.wp_plugins.data;
      const outdatedPlugins = plugins.filter((plugin) => plugin.update === "available");
      if (outdatedPlugins.length > 0) {
        const kinstaDashboardPluginPageURL = `https://my.kinsta.com/sites/plugins/${site.site_id}/${site.env_id}?idCompany=${import.meta.env.VITE_KINSTA_COMPANY_ID}`;
        return {
          name: site.name,
          plugins: outdatedPlugins,
          url: kinstaDashboardPluginPageURL,
        };
      }
    });

    setSitesWithOutdatedPlugin(sitesWithOutdatedPlugin);

  checkSitesWithPluginUpdate();
  setIsLoading(false);
}, []);

上のコードでは、読み込み状態も作成され、デフォルトでtrueに設定されています。これは、データの表示方法を制御するのに使用します。すべてのデータが読み込まれると、falseに設定されます。

UI内でサイトデータとプラグインをレンダリングするためのマークアップは以下の通りです。

import { useEffect, useState } from "react"
import KinstaLogo from './assets/kinsta-logo.png'
import PluginPage from './components/PluginsPage'

function App() {
  // APIからデータを読み込む
  return (
    <div className="container">
        <div>
          <div> className="title-section">
            <img src={KinstaLogo} className="logo" alt="" />
          </div>
          <p> className="info-box">
            更新が必要なプラグインに関する情報を取得します。
          </p>
          {isLoading ? (
            <p>読み込み中...</p>
          ) : (
            <>
              <div className="content">
                <p>以下のサイトに更新が必要なプラグインがあります。</p>
                {sitesWithOutdatedPlugin.map((site, index) => {
                  return (
                    <PluginPage key={index} {...site} />
                  );
                })}
              </div>
            </>
          )}
        </div>
    </div>
  )
}
export default App

このコードには、ロゴを含むヘッダーと情報を提供する段落があります。UIのコンテンツは、isLoadingの状態に基づいて条件付きでレンダリングされます。データの読み込み中には読み込み中のメッセージを表示し、データが読み込まれると、サイトと更新が必要なプラグインに関するデータを表示します。

また、PluginPagePluginPage.jsx)コンポーネントは、個々のサイトとそのプラグインの情報を表示するためのものです。プラグインの情報表示を切り替える機能も含まれます。

import { useState } from "react"
import { FaRegEye } from "react-icons/fa";
import { FaRegEyeSlash } from "react-icons/fa";

const PluginUse = (site) => {
    const [viewPlugin, setViewPlugin] = useState(false);

    return (
        <>
            <div className="site-card">
                <div className="site-card-details">
                    <p>{site.name}</p>
                    <div className="both-btns">
                        <a> href={site.url} target="_blank" rel="noreferrer" className="btn">
                            MyKinstaで表示
                        </a>
                        <button onClick={() => setViewPlugin(!viewPlugin)} className="btn" title="View Plugins">
                            {viewPlugin ? <FaRegEyeSlash /> : <FaRegEye />}
                        </button>
                    </div>
                </div>
                {viewPlugin && (
                    <div className="plugin-list">
                        {site.plugins.map((plugin, index) => {
                            return (
                                <div key={index} className="plugin-card">
                                    <p>{plugin.name}</p>
                                    <div className="plugin-version-info">
                                        <p>現在のバージョン:{plugin.version}</p>
                                        <p>最新のバージョン:{plugin.update_version}</p>
                                    </div>
                                </div>
                            );
                        })}
                    </div>
                )}
            </div>
        </>
    )
}
export default PluginUse

manifest.jsonファイルの設定

ユーザーインターフェースと機能をChrome 拡張機能に変換するには、manifest.jsonファイルを設定する必要があります。

publicフォルダにmanifest.jsonファイルを作成し、以下のコードを貼り付けます。

{
    "manifest_version": 3,
    "name": "Kinsta Plugins Manager - Thanks to Kinsta API",
    "description": "Kinsta APIを介してKinstaのMyKinstaからWordPressサイトのプラグインを管理できます。",
    "version": "0.1.0",
    "icons": {
        "48": "kinsta-icon.png"
    },
    "action": {
        "default_popup": "index.html"
    },
    "permissions": [
        "tabs"
    ],
    "host_permissions": [
        "https://my.kinsta.com/*"
    ]
}

必ずアイコンファイルをpublicフォルダに追加してください。

この時点で、ビルドコマンド(npm run build)を実行し、manifest.jsonファイル、Reactが生成したindex.html などのファイルを含むすべてのアセットをdistまたはbuildフォルダに移動します。

続いて、chrome://extensions/に移動し、解凍された拡張機能としてChromeに読み込みます。「パッケージ化されていない拡張機能を読み込む」をクリックし、拡張機能用に作成したディレクトリを選択してください。

拡張機能を特定のサイトに制限して使用する

拡張機能はどこにアクセスしても動作する状態にあるため、MyKinstaにアクセスしたときにだけ動作するように設定します。

これを行うには、App.jsxファイルを調整します。アクティブなタブを保存する状態を作成します。

const [activeTab, setActiveTab] = useState(null);

次にuseEffectフックを更新し、getCurrentTab関数を定義して呼び出します。

const getCurrentTab = async () => {
  const queryOptions = { active: true, currentWindow: true };
  const [tab] = await chrome.tabs.query(queryOptions);
  setActiveTab(tab);
}
getCurrentTab();

上のコードでは、chrome.tabs.queryを特定のクエリオプションで使用し、現在のウィンドウでアクティブなタブのみを取得するようにします。タブが取得されると、拡張機能の状態にアクティブなタブとして設定されます。

最後に、コンポーネントのreturn文に条件付きレンダリングロジックを実装します。これにより、MyKinstaでのみプラグイン管理のUIが表示されるようになります。

return (
  <div className="container">
    {activeTab?.url.includes('my.kinsta.com') ? (
      <div >
        <div className="title-section">
          <img src={KinstaLogo} className="logo" alt="" />
        </div>
        <p className="info-box">
          更新が必要なプラグインに関する情報を取得します。
        </p>
        {isLoading ? (
          <p>読み込み中...</p>
        ) : (
          <>
            <div className="content">
              <p>次の{sitesWithPluginUpdate}サイトに更新が必要なプラグインがあります。</p>
              {sitesWithOutdatedPlugin.map((site, index) => {
                return (
                  <PluginPage key={index} {...site} />
                );
              })}
            </div >
          </>
        )}
      </div >
    ) : (
      <div >
        <div className="title-section">
          <img src={KinstaLogo} className="logo" alt="" />
        </div>
        <p className="info-box">
          この拡張機能はMyKinstaでのみ利用可能です。
        </p>
      </div>
    )}
  </div>
)

変更後はアプリケーションを再構築し、拡張機能を再読み込みします。これにより、新たなロジックと制限が適用されます。

まとめ

Chromeの拡張機能を構築するための基礎知識と、Reactを使って拡張機能を作成する手順を取り上げました。また、Kinsta APIと相互作用する拡張機能の作成方法についても触れています。

Kinstaのお客様は、サイト、アプリケーション、データベースを管理するためのソリューション開発に役立つKinsta APIの可能性と柔軟性をぜひご活用ください。

よく使用しているKinsta APIエンドポイントはありますか?以下のコメント欄でお聞かせください。

Joel Olawanle Kinsta

Kinstaでテクニカルエディターとして働くフロントエンド開発者。オープンソースをこよなく愛する講師でもあり、JavaScriptとそのフレームワークを中心に200件以上の技術記事を執筆している。