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
Section titled “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
Section titled “Installation”npm install kkrpcAPI Definition
Section titled “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
Section titled “Implementation”Background Script
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “Best Practices”- Centralize RPC Setup: Consider creating a helper file (like the
lib/kkrpc.tsin 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
ChromePortIOhandles listener cleanup, but ensure you also clean up yourRPCChannelinstances and any other related state in theonDisconnectlistener. - Error Handling: Wrap your RPC calls in
try...catchblocks to gracefully handle cases where the connection might be lost (e.g., the background service worker becomes inactive).