Розробка кроссбраузерного розширення (WebExtension API)
WebExtension API — це стандарт, який підтримують Chrome, Firefox, Edge, Opera та (частково) Safari. Єдина кодова база з умовними патчами під браузерні різниці — правильний підхід до кроссбраузерної розробки. Копіювати та підтримувати окремі версії для кожного браузера — антипаттерн.
Сумісність API за браузерами
| API | Chrome | Firefox | Edge | Opera | Safari |
|---|---|---|---|---|---|
storage.local |
MV2/3 | MV2/3 | MV2/3 | MV2/3 | 14+ |
storage.sync |
✓ | ✓ | ✓ | ✓ | 15+ |
tabs |
✓ | ✓ | ✓ | ✓ | 14+ |
scripting (MV3) |
✓ | 101+ | ✓ | 92+ | 15.4+ |
declarativeNetRequest |
✓ | 113+ | ✓ | 92+ | 15.4+ |
sidePanel |
114+ | — | — | — | — |
webRequest |
Тільки MV2 | ✓ | Тільки MV2 | Тільки MV2 | — |
Полід: webextension-polyfill
Mozilla розробила webextension-polyfill — вона перетворює chrome.* (Callback) на browser.* (Promise) та згладжує різниці між браузерами:
npm install webextension-polyfill
import browser from 'webextension-polyfill';
// Одинаковий код для Chrome та Firefox
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
await browser.storage.local.set({ data: 'value' });
const result = await browser.storage.local.get('data');
Єдиний маніфест: MV2 vs MV3
Підтримувати два маніфести паралельно — стандартна практика для максимального охоплення:
// scripts/build.js
const fs = require('fs');
const target = process.env.TARGET_BROWSER; // 'chrome', 'firefox', 'safari'
const manifests = {
chrome: {
manifest_version: 3,
background: { service_worker: 'background.js' },
action: { default_popup: 'popup.html' },
},
firefox: {
manifest_version: 2,
background: { scripts: ['background.js'], persistent: false },
browser_action: { default_popup: 'popup.html' },
browser_specific_settings: {
gecko: { id: '[email protected]', strict_min_version: '109.0' },
},
},
safari: {
manifest_version: 3,
background: { service_worker: 'background.js' },
action: { default_popup: 'popup.html' },
browser_specific_settings: {
safari: { strict_min_version: '15.4' },
},
},
};
const base = JSON.parse(fs.readFileSync('./manifest.base.json', 'utf8'));
const merged = { ...base, ...manifests[target] };
fs.writeFileSync('./dist/manifest.json', JSON.stringify(merged, null, 2));
Структура проекту
my-extension/
├── src/
│ ├── background/
│ │ ├── index.ts
│ │ └── handlers/
│ ├── content/
│ │ ├── index.ts
│ │ └── modules/
│ ├── popup/
│ │ ├── App.tsx
│ │ └── index.tsx
│ ├── options/
│ │ └── App.tsx
│ └── shared/
│ ├── storage.ts # обгортка над browser.storage
│ ├── messaging.ts # типізовані повідомлення
│ └── browser.ts # визначення поточного браузера
├── manifest.base.json
├── manifests/
│ ├── mv2.json
│ └── mv3.json
├── vite.config.ts
└── package.json
Типізовані повідомлення
Сувора типізація повідомлень між контекстами — запорука відсутності тихих помилок:
// shared/messages.ts
export type Message =
| { type: 'GET_PAGE_DATA'; url: string }
| { type: 'SET_BADGE'; count: number; color?: string }
| { type: 'OPEN_OPTIONS' }
| { type: 'EXTRACT_TEXT'; selector: string };
export type MessageResponse<T extends Message> =
T extends { type: 'GET_PAGE_DATA' } ? { title: string; meta: Record<string, string> } :
T extends { type: 'EXTRACT_TEXT' } ? { text: string } :
void;
// Типізований sendMessage
export async function sendMessage<T extends Message>(
message: T
): Promise<MessageResponse<T>> {
return browser.runtime.sendMessage(message) as Promise<MessageResponse<T>>;
}
// Типізований onMessage
export function onMessage<T extends Message['type']>(
type: T,
handler: (msg: Extract<Message, { type: T }>, sender: browser.runtime.MessageSender) => Promise<MessageResponse<Extract<Message, { type: T }>>>
) {
browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === type) {
handler(msg, sender).then(sendResponse);
return true;
}
});
}
Визначення браузера та умовні шляхи
// shared/browser.ts
export type BrowserName = 'chrome' | 'firefox' | 'safari' | 'edge' | 'opera';
export function detectBrowser(): BrowserName {
// @ts-ignore
if (typeof browser !== 'undefined' && browser.runtime?.id) {
const ua = navigator.userAgent;
if (ua.includes('Firefox/')) return 'firefox';
if (ua.includes('OPR/')) return 'opera';
if (ua.includes('Edg/')) return 'edge';
// Safari визначається через маніфест
if (!ua.includes('Chrome')) return 'safari';
}
return 'chrome';
}
export const IS_FIREFOX = detectBrowser() === 'firefox';
export const IS_SAFARI = detectBrowser() === 'safari';
export const SUPPORTS_SIDE_PANEL = detectBrowser() === 'chrome';
Збирання для декількох браузерів із Vite
// vite.config.ts
import { defineConfig } from 'vite';
import { crx } from '@crxjs/vite-plugin';
const browser = process.env.TARGET_BROWSER ?? 'chrome';
const manifestFile = browser === 'firefox'
? './manifests/mv2.json'
: './manifests/mv3.json';
export default defineConfig({
define: {
__BROWSER__: JSON.stringify(browser),
__IS_FIREFOX__: browser === 'firefox',
},
plugins: [crx({ manifest: require(manifestFile) })],
build: {
outDir: `dist/${browser}`,
},
});
// package.json scripts
{
"build:chrome": "TARGET_BROWSER=chrome vite build",
"build:firefox": "TARGET_BROWSER=firefox vite build",
"build:edge": "TARGET_BROWSER=edge vite build",
"build:safari": "TARGET_BROWSER=safari vite build",
"build:all": "npm run build:chrome && npm run build:firefox && npm run build:edge"
}
CI/CD: збирання та публікація всіх версій
# .github/workflows/publish.yml
name: Publish Extensions
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build:all
- name: Publish to Chrome Web Store
uses: trstringer/manual-approval@v1
env:
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
- name: Sign & Publish to Firefox AMO
run: |
npx web-ext sign \
--source-dir dist/firefox \
--api-key ${{ secrets.AMO_JWT_ISSUER }} \
--api-secret ${{ secrets.AMO_JWT_SECRET }} \
--channel listed
Терміни
Кроссбраузерне розширення (Chrome + Firefox + Edge) з єдиною кодовою базою, типізацією, автосборкою та публікацією в три магазини — 10–16 робочих днів залежно від функціональності. Додавання Safari (Xcode-обгортка + App Store) додає 5–8 днів. Терміни ревю магазинів — окремо та паралельно.







