Chrome Extension
This guide demonstrates how to implement robust bidirectional communication between different parts of a Chrome extension (like background scripts, content scripts, popups, etc.) using kkrpc
.
kkrpc
provides a ChromePortIO
adapter that uses chrome.runtime.Port
for persistent, two-way connections.
Features
- Port-based communication: More reliable for long-lived connections compared to one-off messages.
- Bidirectional: Any component can expose an API and call remote APIs.
- Type-safe: Full TypeScript support for your APIs.
- Automatic cleanup: Manages listeners and resources when a connection is closed.
Installation
npm install kkrpc
API Definition
First, define the types for the APIs you want to expose from different parts of your extension.
export interface BackgroundAPI { getExtensionVersion: () => Promise<string>}
export interface ContentAPI { getPageTitle: () => Promise<string>}
Implementation
Background Script
The background script listens for incoming connections from other parts of the extension.
import { ChromePortIO, RPCChannel } from "kkrpc/chrome-extension"import type { BackgroundAPI, ContentAPI } from "./types"
const backgroundAPI: BackgroundAPI = { async getExtensionVersion() { return chrome.runtime.getManifest().version }}
// A map to hold RPC channels, e.g., for each content script tabconst contentChannels = new Map<number, RPCChannel<BackgroundAPI, ContentAPI>>()
chrome.runtime.onConnect.addListener((port) => { console.log(`[Background] Connection from: ${port.name}`)
// Example: Differentiating connections if (port.name === "content-to-background") { const tabId = port.sender?.tab?.id if (tabId) { const io = new ChromePortIO(port) const rpc = new RPCChannel(io, { expose: backgroundAPI }) contentChannels.set(tabId, rpc)
port.onDisconnect.addListener(() => { io.destroy() contentChannels.delete(tabId) console.log(`[Background] Disconnected from tab ${tabId}`) }) } } // Add handlers for other components like popup, sidepanel...})
Content Script
The content script initiates a connection to the background script.
import { ChromePortIO, RPCChannel } from "kkrpc/chrome-extension"import type { BackgroundAPI, ContentAPI } from "./types"
const contentAPI: ContentAPI = { async getPageTitle() { return document.title }}
const port = chrome.runtime.connect({ name: "content-to-background" })const io = new ChromePortIO(port)const rpc = new RPCChannel<ContentAPI, BackgroundAPI>(io, { expose: contentAPI})
const backgroundAPI = rpc.getAPI()
// Example Usageasync function logVersion() { try { const version = await backgroundAPI.getExtensionVersion() console.log(`[Content] Extension version: ${version}`) } catch (error) { console.error("[Content] RPC call failed:", error) }}
logVersion()
Popup / Side Panel / Options Page
Other UI components like the popup connect in the same way as the content script.
import { ChromePortIO, RPCChannel } from "kkrpc/chrome-extension"import type { BackgroundAPI } from "./types"
// Popups don't usually expose APIs to the background scriptconst port = chrome.runtime.connect({ name: "popup-to-background" })const io = new ChromePortIO(port)const rpc = new RPCChannel<{}, BackgroundAPI>(io)
const backgroundAPI = rpc.getAPI()
// Example: Get version when popup button is clickeddocument.getElementById("get-version-btn")?.addEventListener("click", async () => { const version = await backgroundAPI.getExtensionVersion() document.getElementById("version-display")!.textContent = version})
Manifest V3 Configuration
Ensure your manifest.json
is set up correctly.
{ "manifest_version": 3, "name": "My kkrpc Extension", "version": "1.0", "background": { "service_worker": "background.js" }, "content_scripts": [ { "matches": ["<all_urls>"], "js": ["content.js"] } ], "action": { "default_popup": "popup.html" }}
Best Practices
- Centralize RPC Setup: Consider creating a helper file (like the
lib/kkrpc.ts
in the example project) to manage RPC creation for different components. This avoids code duplication. - Named Ports: Use distinct names for ports (
chrome.runtime.connect({ name: '...' })
) to identify the connecting component in the background script. - Cleanup: The
ChromePortIO
handles listener cleanup, but ensure you also clean up yourRPCChannel
instances and any other related state in theonDisconnect
listener. - Error Handling: Wrap your RPC calls in
try...catch
blocks to gracefully handle cases where the connection might be lost (e.g., the background service worker becomes inactive).