5分で始める#
最小のプラグインは 2ファイル(plugin.json と main.js)だけで動きます。
ここでは example.com を開くと右下に 👋 ボタンを表示する「Hello World」を作ってみましょう。
このガイドは WSI 本体がインストール済みである前提です。未インストールの場合は、 Chrome Web Store で「Web System Injection」を検索 してインストールしてください。インストール後、ブラウザのツールバーに WSI アイコンが表示されます。
Step 1. フォルダを作る
mkdir my-first-plugin
cd my-first-plugin
Step 2. plugin.json を作る
プラグイン定義ファイル。ID・対象ドメイン・エントリースクリプトを指定します。
{
"id": "my-first-plugin",
"name": "My First Plugin",
"version": "1.0.0",
"description": "初めてのWSIプラグイン",
"domains": ["example.com"],
"scripts": { "main": "main.js" }
}
Step 3. main.js を作る
プラグイン本体。WSI オブジェクト経由で拡張機能のAPIにアクセスします。
WSI.addButton({
text: '👋',
position: 'bottom-right',
onClick: () => {
alert('Hello from WSI!');
},
});
WSI.log('プラグインが読み込まれました');
Step 4. ZIPにまとめる
2ファイルを ZIP 圧縮します。フォルダを圧縮するのではなく、ファイルを選択して圧縮してください(plugin.json が ZIP のルートに来る必要があります)。
my-first-plugin/ フォルダごと圧縮すると、ZIP内部が my-first-plugin/plugin.json の階層になってしまい、WSIが plugin.json を見つけられません。必ず中身のファイルを選択して圧縮してください。
Step 5. WSIにインポート
- Chrome で
chrome://extensionsを開き、WSI が読み込まれていることを確認 - WSI アイコンをクリックしてポップアップを開く
- 「プラグインを追加」をクリック
- 作った ZIP ファイルをドラッグ&ドロップ
- プレビュー内容を確認して「インポート」
- https://example.com を開く → 右下に 👋 ボタンが出たら成功 🎉
ボタンはドラッグで移動可能です。使いやすい位置に配置できます。位置は chrome.storage.local に保存され、次回以降は覚えてくれます。
プラグインの構造#
WSI プラグインは以下の最大3ファイルで構成されます。
my-plugin.zip
├── plugin.json # 必須: プラグイン定義
├── main.js # 必須: プラグイン本体
└── style.css # 任意: CSS(複数可)
plugin.json のフィールド
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
id | string | 必須 | 一意識別子。英数字とハイフンのみ (/^[a-zA-Z0-9-]+$/) |
name | string | 必須 | 表示名。WSI ポップアップに表示されます |
version | string | 必須 | セマンティックバージョニング(例: 1.0.0) |
description | string | 任意 | 説明文。プラグインカードに表示 |
author | string | 任意 | 作者名 |
domains | string[] | 必須 | 対象ドメインの配列(1つ以上)。詳細は ドメインマッチング |
scripts.main | string | 必須 | メインJSファイル名(通常 main.js) |
scripts.runAt | string | 任意 | 注入タイミング。デフォルト document_idle |
styles | string[] | 任意 | CSSファイル名の配列。複数指定可 |
config | object | 任意 | プラグイン固有の設定オブジェクト。WSI.getConfig() で取得 |
main.js の実行環境
プラグインコードは対象ページの メインワールド(ページ自身のJSと同じ実行コンテキスト)で実行されます。
つまり window, document, localStorage など、ページ側の全APIにアクセス可能です。
ただし chrome.* 拡張機能APIは直接使えません。
これは WSI がセキュリティのため意図的に切っているもので、代わりに必要な機能は WSI.* SDK 経由で提供されます。
Chrome 拡張には「Isolated World」(拡張機能専用のサンドボックス)と「Main World」(ページ自身の世界)があります。WSI プラグインは後者で動くため、ページのJSと同じDOM・同じJSグローバルを共有します。結果、ページ側の React や Vue の内部状態にもアクセス可能です(自己責任)。
SDK リファレンス#
プラグインコードには WSI という引数でSDKオブジェクトが渡されます。以下のAPIが利用可能です。
WSI.addButton(options)
画面にフローティングボタンを追加します。ユーザはドラッグで自由に移動可能で、位置は chrome.storage.local に永続化されます。
| パラメータ | 型 | 説明 |
|---|---|---|
text | string | ボタンのラベル |
icon | string | 任意のアイコン文字(絵文字など)。指定するとtextの前に表示 |
position | string | 初期位置: bottom-right / bottom-left / top-right / top-left |
onClick | function | クリック時コールバック(ドラッグ直後は自動抑制) |
WSI.addButton({
text: 'Say Hi',
icon: '👋',
position: 'bottom-right',
onClick: () => alert('Hi!'),
});
WSI.addPanel(options)
画面サイドにスライドパネルを追加します。目次・検索結果・設定画面などに使えます。
| パラメータ | 型 | 説明 |
|---|---|---|
title | string | パネルヘッダーのタイトル |
width | string | CSS幅(例: "300px", "25vw") |
position | string | right(デフォルト)または left |
content | string | パネル本文のHTML文字列(innerHTMLにセット) |
onOpen | function | 表示直後のコールバック |
onClose | function | ×ボタンで閉じた直後のコールバック |
const panel = WSI.addPanel({
title: '設定',
width: '320px',
position: 'right',
content: '<p>ここに内容</p>',
onClose: () => console.log('panel closed'),
});
content は innerHTML にセットされるため、ユーザ入力や外部文字列を直接渡すと XSS 脆弱性になります。必ずエスケープしてください。
WSI.storage
プラグイン固有の永続ストレージ。chrome.storage.local にプラグインID名前空間で保存されます。
すべて非同期(Promise を返す)です。
// 保存
await WSI.storage.set('userName', '山田太郎');
// 取得
const name = await WSI.storage.get('userName');
// 全件取得(プラグインのストレージ全体)
const all = await WSI.storage.getAll();
// 削除
await WSI.storage.remove('userName');
URL別にデータを分けたいときは、キー名に location.origin + location.pathname を含めると自然に名前空間を切れます。Highlighter サンプルがこの方式を使っています。
WSI.fetch(url, options)
CORS制限をバイパスして任意のURLにリクエストを送れる特別な fetch。
内部的には Service Worker で fetch() を実行するため、ページのCORSポリシーに縛られません。
| オプション | 型 | 説明 |
|---|---|---|
method | string | HTTPメソッド。デフォルト "HEAD" |
redirect | string | "follow"(デフォルト)/ "manual" / "error" |
headers | object | リクエストヘッダー |
body | string | リクエストボディ(POST/PUT 等) |
戻り値(成功時)
{
ok: boolean, // HTTP 2xx なら true
status: number, // HTTPステータスコード
url: string, // リダイレクト後の最終URL
redirected: boolean, // リダイレクトが発生したか
body: string, // レスポンスボディ(HEAD時は空文字列)
}
戻り値(失敗時)
{ error: string, ok: false, status: 0 }
const res = await WSI.fetch(
'https://jisho.org/api/v1/search/words?keyword=猫',
{ method: 'GET' }
);
if (res.ok) {
const data = JSON.parse(res.body);
console.log(data);
} else {
console.error('fetch error', res.error || res.status);
}
WSI.onPageLoad(callback)
SPA等で location.href が変わった瞬間に callback が呼ばれます。
React Router や Next.js のクライアントサイド遷移にも反応します(MutationObserver + popstate 実装)。
WSI.onPageLoad((url) => {
WSI.log(`ナビゲーション: ${url}`);
// 再初期化処理
});
onPageLoad は最初のページロード時には発火しません。最初の処理は main.js のトップレベルコードに書き、SPA遷移への追従のみ onPageLoad で処理してください。
WSI.getConfig()
plugin.json の config フィールドの値を deep-copy で返します。
プラグインをユーザー設定可能にするための主要な仕組みです。
{
"id": "my-plugin",
"config": {
"message": "こんにちは",
"color": "#4688F1"
}
}
const config = WSI.getConfig();
console.log(config.message); // "こんにちは"
console.log(config.color); // "#4688F1"
WSI.log(message)
プラグインIDをプレフィックスとして付けた console.log。どのプラグインが何をログったか明確になります。
WSI.log('プラグイン起動');
// コンソール出力: [WSI:my-plugin] プラグイン起動
ドメインマッチング#
domains 配列に書いたパターンのどれかに一致するドメインで、プラグインが実行されます。
| パターン | マッチする例 | マッチしない例 |
|---|---|---|
"example.com" |
example.com |
www.example.com / example.org |
"*.example.com" |
example.com / www.example.com / foo.bar.example.com |
example.org |
"*" (v1.2.0〜) |
あらゆるドメイン | — |
複数パターンの組み合わせ
{
"domains": [
"qiita.com",
"*.qiita.com",
"zenn.dev",
"*.hatena.ne.jp"
]
}
chrome:// / edge:// では動きませんChrome 内部ページや拡張機能ストアなど、特権ページには拡張機能のスクリプト注入自体がブロックされます(Chrome仕様)。ドメイン指定に関係なく、これらのページでは WSI プラグインは動きません。
設定 (config)#
plugin.json の config フィールドは任意のJSONオブジェクトです。
プラグイン内から WSI.getConfig() で取得し、振る舞いを変えられる仕組みに使えます。
典型的な使い方
{
"id": "highlighter",
"config": {
"color": "#fff59d",
"buttonPosition": "bottom-left",
"maxHighlightsPerPage": 100
}
}
const config = WSI.getConfig();
const color = config.color || '#fff59d'; // デフォルト値
const buttonPosition = config.buttonPosition || 'bottom-right';
const maxHighlights = config.maxHighlightsPerPage ?? 50;
ユーザーが簡単にカスタマイズできるよう、ハードコードせず config から読む習慣をつけると、自分や他人が再利用しやすくなります。色、閾値、対象セレクタなどは config に出しましょう。
パッケージング#
プラグインは ZIP形式で WSI にインポートします。ZIP の作り方をOS別・方法別にまとめます。
Windows (エクスプローラ)
plugin.json,main.js,style.cssを選択- 右クリック → 「送る」→「圧縮 (zip 形式) フォルダー」
- できた
.zipファイル名を任意のものに変更
macOS / Linux (コマンドライン)
cd my-plugin
zip ../my-plugin.zip plugin.json main.js style.css
Windows PowerShell
Compress-Archive -Path plugin.json, main.js, style.css -DestinationPath my-plugin.zip
Node.js (jszip) で自動化
開発が加速してくると、スクリプトでZIPを作るのが便利です。WSI リポジトリのサンプル ZIP もこの方式で作っています。
const fs = require('fs');
const JSZip = require('jszip');
const zip = new JSZip();
['plugin.json', 'main.js', 'style.css'].forEach((f) => {
zip.file(f, fs.readFileSync(f));
});
zip.generateAsync({ type: 'nodebuffer' }).then((buf) => {
fs.writeFileSync('my-plugin.zip', buf);
console.log('ZIP built:', buf.length, 'bytes');
});
バージョニングのルール
ZIPを再作成するたびに plugin.json の version をインクリメントしましょう。
| 変更タイプ | 例 |
|---|---|
| パッチ修正(バグ修正) | 1.0.0 → 1.0.1 |
| 機能追加(後方互換あり) | 1.0.5 → 1.1.0 |
| 破壊的変更 | 1.3.0 → 2.0.0 |
id が同じプラグインをインポートすると、WSI は上書き確認を出します。設定値・ストレージはそのまま残り、コードだけが新しくなります。
サンプルギャラリー#
公式サンプル 6 個は、それぞれ異なる WSI SDK 機能を学べるよう設計されています。 ZIPをインポートすればすぐ動きます。ソースコードは各カードから GitHub で読めます。
指定ドメインで 👋 ボタンを表示する入門サンプル。
短縮URLにホバー → リダイレクト先をツールチップ表示。フィッシング対策に。
選択テキストをMarkdown形式(リンクは [text](url))でクリップボードにコピー。
ハイライトをURL単位で永続化。再訪時に自動復元。Alt+クリックで削除。
ページの h1〜h6 見出しからTOCサイドパネルを生成。アクティブ見出しを追従ハイライト。
日本語を選択 → Jisho.org API で和英辞書検索 → パネル表示。外部JSON API連携の教材。
デバッグ#
プラグインコードのログを見る
対象ページで F12 で DevTools を開き「Console」タブ。
WSI.log() の出力は [WSI:your-plugin-id] プレフィックス付きで表示されます。
ストレージの中身を確認する
chrome://extensionsを開く- WSI の詳細 → 「Service Worker」リンクをクリック → DevTools が開く
- Console で以下を実行:
// 全ストレージを表示
chrome.storage.local.get(null, console.log);
// 特定プラグインのデータ
chrome.storage.local.get('pluginData_my-plugin', console.log);
// ボタン位置キャッシュ
chrome.storage.local.get('wsiButtonPositions', console.log);
よくあるエラー
| 症状 | 原因・対処 |
|---|---|
| インポート時「plugin.json が見つかりません」 | ZIP内部がフォルダ階層になっている。ファイルを選択して圧縮し直す |
| ボタンが表示されない | (1) プラグインが有効か確認 (2) ドメインパターンが合っているか (3) DevTools Consoleでエラー確認 |
| コード変更が反映されない | プラグインを一度削除してから ZIP を作り直して再インポート。version バンプも忘れずに |
WSI is not defined |
注入タイミングが早すぎる可能性。scripts.runAt を document_idle に |
WSI.fetch がエラー |
戻り値の error フィールドにメッセージあり。CORSではなくネットワーク自体のエラーの可能性 |
ベストプラクティス#
DO
- 1プラグイン = 1機能 に絞る(テストしやすく、再利用しやすい)
- 設定値は
configへ(色、閾値、セレクタなどはハードコードしない) - エラーは try-catch で握りつぶさない(WSI.logで見えるようにする)
- CSSクラスにプラグインID等のプレフィックスをつける(
.wsi-xxxxx-)。ページ側のCSSと衝突しないよう - innerHTML に入れる前にエスケープ(XSSを防ぐ)
- storage キーに
location.pathnameを含める(URL別の状態管理) - ZIP再作成時に
versionをバンプ(何が変わったか追跡できる)
DON'T
chrome.*API を直接呼ぼうとしない(メインワールドからは使えない)- ページのグローバル変数を書き換えない(
window.$等との衝突要因) - 永続化なしの重いDOM計算を
onPageLoadで毎回(キャッシュする) - 外部ライブラリをCDNから動的
<script>読み込み(Manifest V3 のリモートコード禁止ルールに抵触する可能性。ZIPに同梱すべき) - signalhigh-frequencyなイベント(scroll/mousemove)にデバウンスなしリスナ(パフォーマンス劣化)
公式サンプルの main.js はいずれも 200行以下に収めています。何をするプラグインか一目でわかる粒度が理想です。
FAQ#
WSI 本体(Chrome拡張)はどこで入手できますか?
Chrome Web Store で「Web System Injection」を検索 してインストールしてください。インストール後、ブラウザツールバーの WSI アイコンからポップアップを開いて、プラグインZIPをインポートできます。
プラグインは同時に何個まで入れられますか?
明確な上限はありません。chrome.storage.local の容量上限(約10MB)まで。実用上は数十個まで問題なく動きます。
他のユーザーが作ったプラグインを使うのは安全ですか?
WSI プラグインは JavaScript がフル実行されるため、ソースコードを読めない相手から配られた ZIP は絶対に入れないでください。Chrome拡張機能本体と同じレベルのリスクがあります。公式サンプルや自分で書いたものだけ使うのが安全です。
プラグイン同士はデータを共有できますか?
デフォルトでは別名前空間で完全分離されています(pluginData_<pluginId> キーで分離)。どうしても共有したい場合は、メインワールド上で独自の window.myShared = ... 的な仕組みを使うことは可能です(非推奨)。
React/Vue で書かれたページにも機能追加できますか?
はい、問題なく動きます。ただし SPA では onPageLoad で URL 変更を検知する必要があります。React の state や props に直接触ることもできますが、内部実装に依存するので脆いです。可能ならDOM経由でアクセスしましょう。
プラグインで外部ライブラリ (lodash, axios 等) を使いたい
CDNから動的ロードはManifest V3 のルール違反になる可能性があります。ライブラリのJSファイルをプラグインZIPに同梱し、plugin.json の scripts.main より前に別のscriptとして読む… という仕組みは WSI に無いため、必要な関数をインライン実装するか、ZIP内の別ファイルを import() するのが現実解です。
ボタンの位置をリセットしたい
Service Worker Console で以下を実行:
chrome.storage.local.remove('wsiButtonPositions');
または該当プラグインを削除 → 再インポートでもリセットされます。
サンプルと同じディレクトリ構造で作るべきですか?
いいえ、必須ではありません。ZIPの内部構造がフラット(plugin.json がルート)であれば、開発時のフォルダ配置は自由です。
プラグインをChrome Web Storeで配布できますか?
いいえ、WSIプラグインは WSI 本体に取り込まれる形で動作します。配布したい場合は、ZIPファイルを自分のサイト/GitHub等でホストし、「WSIをインストールした上でこのZIPをインポートしてください」と案内する形になります。
どのSDKを使うべきか迷ったら?
以下が目安です:
- 単発のアクション →
addButton(例: 1クリックでコピー/通知/検索) - 情報を表示するUI →
addPanel(目次、検索結果、詳細) - 状態を永続化 →
storage(設定、履歴、マーキング) - 外部サイトへリクエスト →
fetch(API連携、URL検証) - SPAに対応 →
onPageLoad(URL変更時に再処理)
既存のプラグインをベースに作りたい
サンプルZIPを展開して、plugin.jsonのidとnameを変更、main.jsを書き換え、ZIP化すればオリジナルプラグインになります。サンプルギャラリーの気に入ったものから始めるのがおすすめ。