How to Build Scalable Chrome Extensions with a Message‑Driven Architecture
This article demonstrates building a Chrome extension that redirects requests, explains the challenges of managing messages across isolated execution environments, and introduces a new framework—browser‑extension‑kit—that streamlines background, popup, and content‑script communication using RxJS and declarative decorators.
Example
We often use Chrome extensions like 1Password, Adblock, React Developer Tools, and Redux DevTools to boost productivity. You can also develop your own extension to improve team workflow. The example below shows an extension that redirects a request (similar to XSwitch) from an original URL to a user‑provided URL, enabling online debugging by pointing a page’s JavaScript to a local webpack‑dev‑server.
The UI for entering URLs appears in a popup when the extension icon is clicked, while the redirection logic lives in a background script that persists for the extension’s lifetime.
// popup.js
import React from 'react';
const port = chrome.runtime.connect();
export const App = () => {
return (
<>
<div>
Original URL: <input onChange={e => port.postMessage({ originalUrl: e.target.value })} />
</div>
<div>
New URL: <input onChange={e => port.postMessage({ newUrl: e.target.value })} />
</div>
</>
);
}; // background.js
let originalUrl = '';
let newUrl = '';
chrome.runtime.onConnect.addListener(port => {
port.onMessage.addListener(message => {
if (message.originalUrl) {
originalUrl = message.originalUrl;
} else if (message.newUrl) {
newUrl = message.newUrl;
}
});
});
chrome.webRequest.onBeforeRequest.addListener(request => {
if (!originalUrl || !newUrl || request.url !== originalUrl) {
return;
}
return { redirectUrl: newUrl };
}, {}, ['blocking']);In a real‑world extension, multiple execution environments (background, popup, content‑script, page‑script, devtools) are isolated and must communicate via messages, which quickly becomes a high‑frequency, complex task.
Actual Scenario
A full‑featured extension typically has a directory structure like:
src
background
module1
index.ts
...
module2
index.ts
...
index.ts
popup
module1
index.ts
...
module2
index.ts
...
index.ts
content‑script
...
Background scripts listen for connections, dispatch messages based on port.name, and route them to specific modules—much like Redux.
Here we use RxJS to handle message streams; the library is optional.
import { fromEventPattern, merge } from 'rxjs';
import { module1Processer, observable1 } from './module1';
import { module2Processer, observable2 } from './module2';
let devtoolPort;
let contentScript;
let pageScriptPort;
fromEventPattern(
handler => chrome.runtime.onConnect.addListener(handler),
handler => chrome.runtime.onConnect.removeListener(handler)
).subscribe(port => {
if (port.name === 'devtool') {
devtoolPort = port;
} else {
// ...
}
port.onMessage.addListener(message => {
switch (message.type) {
case 'module1':
module1Processer();
break;
case 'module2':
module2Processer();
break;
}
});
});
// external connections
fromEventPattern(
handler => chrome.runtime.onConnectExternal.addListener(handler),
handler => chrome.runtime.onConnectExternal.removeListener(handler)
).subscribe(port => {
if (port.name === 'devtool') {
devtoolPort = port;
} else {
// ...
}
});
merge(observable1, observable2).pipe(/* ... */).subscribe();UI code (e.g., popup) connects to the background via a port and updates state using a React context.
import { createContext, useState, useRef, useEffect } from 'react';
const port = chrome.runtime.connect({ name: BACKGROUND_MESSAGE_NAME });
const defaultStore = {
module1: {...},
module2: {...},
dispatch: {
module1: payload => {
port.postMessage({ type: MessageType.emit, payload: { ...payload, domain: 'module1' } });
}
}
};
export const Context = createContext(defaultStore);
export const ContextProvider = () => {
const [store, setStore] = useState(defaultStore);
const storeRef = useRef(defaultStore);
useEffect(() => {
port.onMessage.addListener(msg => {
switch (msg.type) {
case MessageType.storeChanged:
storeRef.current = { ...storeRef.current, [msg.payload.domain]: msg.payload.store };
setStore(storeRef.current);
break;
default:
break;
}
});
}, []);
return (
<Context.Provider value={store}>
<App />
</Context.Provider>
);
};Problems
Developing Chrome extensions faces two major issues:
Framework gaps: No community‑wide best‑practice framework, leading to duplicated or tangled logic for monitoring, logging, authentication, etc.
Lack of experience: Edge cases such as iframe content‑script isolation, API availability differences, and message‑size limits cause frequent pitfalls.
Message handling is especially problematic because each environment is isolated, requiring careful use of chrome.runtime.connect, chrome.runtime.onMessage, and sometimes window.postMessage. Limitations include differing API availability, connection order constraints, and serialization challenges (circular references, large payloads, complex types).
New Development Approach
The proposed solution keeps the isolation of environments but moves common concerns (message registration, logging, etc.) into a framework— browser-extension-kit. Developers write business logic in classes that extend framework base classes, automatically gaining message handling, logging, and RxJS integration.
module1/background.ts
import rxjs from 'rxjs';
import { Background } from 'browser-extension-kit/background';
import { subject, observable } from 'browser-extension-kit';
export default class MyBackground extends Background {
constructor() {
super();
this.myObservable1$.subscribe(data => {
// do something with data
});
this.on('messageID', message => {
// handle message
});
}
@subject('uniqueID')
private mySubject = new rxjs.Subject<number>();
@observable.popup
private myObservable1$ = rxjs.from(...).pipe(rxjs.shareReplay(1));
@observable(['background', 'popup'])
private myObservable2$ = rxjs.concat(rxjs.from(...), this.mySubject).pipe(rxjs.shareReplay(1));
}module1/popup.tsx
import React from 'react';
import { useMessage, usePostMessage } from 'browser-extension-kit/popup';
const App = () => {
const active = useMessage('MyBackground::active$', null);
const port = usePostMessage();
const toggleActive = React.useCallback(() => {
port.background('MyBackground::active', !active);
}, [port, active]);
return (
<div>
active: {active}
<button onClick={toggleActive}>click me</button>
</div>
);
};
export default App;This architecture centralizes all message flow in the background, uses RxJS ReplaySubject to buffer messages for late‑loading contexts, and provides a dispatcher that routes messages to the appropriate context.
Implementation Details
When any context (content‑script, page‑script, devtools, popup) loads, the framework automatically creates a single connection to the background and stores the port in a hub.
The framework subscribes to all messages for each port, shielding developers from low‑level API calls.
ReplaySubject ensures that messages sent before a context loads are delivered once it becomes active.
A dispatcher in the background checks the target of each message and forwards it accordingly.
Each context receives a controller that consumes the buffered messages and runs the business logic.
Summary
While routing every message through the background adds some overhead, it simplifies development by separating concerns and providing a uniform API. The current implementation focuses on React‑based UI (popup and devtools). Common utilities such as monitoring, analytics, and authentication remain business‑specific and are not bundled into the framework. To try the kit, install it via npm or yarn and explore the API on GitHub.
npm i browser-extension-kit -S
# or
yarn add browser-extension-kitMore usage details and API documentation are available at https://github.com/alibaba/browser-extension-kit .
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
