Skip to content

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.

  • 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.
Terminal window
npm install kkrpc

First, define the types for the APIs you want to expose from different parts of your extension.

types.ts
export interface BackgroundAPI {
getExtensionVersion: () => Promise<string>
}
export interface ContentAPI {
getPageTitle: () => Promise<string>
}

The background script listens for incoming connections from other parts of the extension.

background.ts
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 tab
const 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...
})

The content script initiates a connection to the background script.

content.ts
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 Usage
async function logVersion() {
try {
const version = await backgroundAPI.getExtensionVersion()
console.log(`[Content] Extension version: ${version}`)
} catch (error) {
console.error("[Content] RPC call failed:", error)
}
}
logVersion()

Other UI components like the popup connect in the same way as the content script.

popup.ts
import { ChromePortIO, RPCChannel } from "kkrpc/chrome-extension"
import type { BackgroundAPI } from "./types"
// Popups don't usually expose APIs to the background script
const 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 clicked
document.getElementById("get-version-btn")?.addEventListener("click", async () => {
const version = await backgroundAPI.getExtensionVersion()
document.getElementById("version-display")!.textContent = version
})

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"
}
}
  1. 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.
  2. Named Ports: Use distinct names for ports (chrome.runtime.connect({ name: '...' })) to identify the connecting component in the background script.
  3. Cleanup: The ChromePortIO handles listener cleanup, but ensure you also clean up your RPCChannel instances and any other related state in the onDisconnect listener.
  4. 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).