cross-browser-compatibility
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCross-Browser Extension Compatibility
跨浏览器扩展兼容性
Comprehensive guide to writing browser extensions that work across Chrome, Firefox, Safari, and Edge with proper feature detection and polyfills.
本文是一份编写可在Chrome、Firefox、Safari和Edge中运行的浏览器扩展的综合指南,包含正确的特性检测和Polyfill使用方法。
Overview
概述
Browser extensions share a common WebExtensions API standard, but implementations differ significantly. This skill covers how to handle those differences.
This skill covers:
- API compatibility matrices
- Polyfill usage and patterns
- Feature detection techniques
- Browser-specific workarounds
- Manifest differences
This skill does NOT cover:
- General JavaScript compatibility (use caniuse.com)
- Extension store submission (see skill)
extension-anti-patterns - UI framework differences
浏览器扩展遵循通用的WebExtensions API标准,但各浏览器的实现差异显著。本技能将介绍如何处理这些差异。
本技能涵盖:
- API兼容性矩阵
- Polyfill的使用与模式
- 特性检测技巧
- 浏览器特定的解决方案
- Manifest差异
本技能不涵盖:
- 通用JavaScript兼容性(可参考caniuse.com)
- 扩展商店提交流程(请查看技能)
extension-anti-patterns - UI框架差异
Quick Reference
快速参考
Browser API Namespaces
浏览器API命名空间
| Browser | Namespace | Promises | Polyfill Needed |
|---|---|---|---|
| Chrome | | Callbacks | Yes |
| Firefox | | Native | No |
| Safari | | Native | No |
| Edge | | Callbacks | Yes |
| 浏览器 | 命名空间 | Promise支持 | 是否需要Polyfill |
|---|---|---|---|
| Chrome | | 回调式 | 是 |
| Firefox | | 原生支持 | 否 |
| Safari | | 原生支持 | 否 |
| Edge | | 回调式 | 是 |
Universal Pattern
通用模式
typescript
// Use webextension-polyfill for consistent API
import browser from 'webextension-polyfill';
// Now works in all browsers with Promises
const tabs = await browser.tabs.query({ active: true });typescript
// Use webextension-polyfill for consistent API
import browser from 'webextension-polyfill';
// Now works in all browsers with Promises
const tabs = await browser.tabs.query({ active: true });API Compatibility Matrix
API兼容性矩阵
Core APIs
核心API
| API | Chrome | Firefox | Safari | Edge | Notes |
|---|---|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ | MV3 only |
| ✓ | ✓ | ✓ | ✓ | Standard |
| ✓ | ✓ | ✗ | ✓ | Safari: no support |
| MV2 | ✓ | MV2 | MV2 | Use |
| ✓ | ✓ | ◐ | ✓ | Safari: limited |
| ✓ | ✓ | ✓ | ✓ | Standard |
| ✓ | ✓ | ◐ | ✓ | Safari: restrictions |
| ✓ | ✓ | ✗ | ✓ | Safari: no support |
| ✓ | ✓ | ✗ | ✓ | Safari: no support |
| ✓ | ✓ | ✓ | ✓ | Standard |
| ✓ | ◐ | ✗ | ✓ | Firefox: partial |
| ✓ | ✓ | ✗ | ✓ | Safari: no support |
| ✓ | ✓ | ✗ | ✓ | Safari: no support |
| ✓ | ✓ | ✗ | ✓ | Safari: no support |
| ✓ | ✓ | ◐ | ✓ | Safari: limited |
| ✓ | ✓ | ✓ | ✓ | Standard |
| ✓ | ✓ | ◐ | ✓ | Safari: limited |
| ✓ | ✓ | ✓ | ✓ | Standard |
| ✓ | ✓ | ◐ | ✓ | Safari: some limits |
| ✓ | ✓ | ◐ | ✓ | Safari: limited |
| ✓ | ✓ | ◐ | ✓ | Safari: observe only |
| ✓ | ✓ | ◐ | ✓ | Safari: limited |
| API | Chrome | Firefox | Safari | Edge | 说明 |
|---|---|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ | 仅支持MV3 |
| ✓ | ✓ | ✓ | ✓ | 标准API |
| ✓ | ✓ | ✗ | ✓ | Safari:不支持 |
| MV2 | ✓ | MV2 | MV2 | MV3中请使用 |
| ✓ | ✓ | ◐ | ✓ | Safari:支持有限 |
| ✓ | ✓ | ✓ | ✓ | 标准API |
| ✓ | ✓ | ◐ | ✓ | Safari:存在限制 |
| ✓ | ✓ | ✗ | ✓ | Safari:不支持 |
| ✓ | ✓ | ✗ | ✓ | Safari:不支持 |
| ✓ | ✓ | ✓ | ✓ | 标准API |
| ✓ | ◐ | ✗ | ✓ | Firefox:部分支持 |
| ✓ | ✓ | ✗ | ✓ | Safari:不支持 |
| ✓ | ✓ | ✗ | ✓ | Safari:不支持 |
| ✓ | ✓ | ✗ | ✓ | Safari:不支持 |
| ✓ | ✓ | ◐ | ✓ | Safari:支持有限 |
| ✓ | ✓ | ✓ | ✓ | 标准API |
| ✓ | ✓ | ◐ | ✓ | Safari:支持有限 |
| ✓ | ✓ | ✓ | ✓ | 标准API |
| ✓ | ✓ | ◐ | ✓ | Safari:存在部分限制 |
| ✓ | ✓ | ◐ | ✓ | Safari:支持有限 |
| ✓ | ✓ | ◐ | ✓ | Safari:仅支持观测 |
| ✓ | ✓ | ◐ | ✓ | Safari:支持有限 |
Advanced APIs
高级API
| API | Chrome | Firefox | Safari | Edge | Workaround |
|---|---|---|---|---|---|
| ✓ | ◐ | ◐ | ✓ | Use webRequest |
| 109+ | ✗ | ✗ | 109+ | Content script |
| 114+ | ✗ | ✗ | 114+ | Use popup |
| 102+ | 115+ | 16.4+ | 102+ | Use local + clear |
| 120+ | ✓ | ✗ | 120+ | Content scripts |
| API | Chrome | Firefox | Safari | Edge | 替代方案 |
|---|---|---|---|---|---|
| ✓ | ◐ | ◐ | ✓ | 使用webRequest |
| 109+ | ✗ | ✗ | 109+ | 内容脚本 |
| 114+ | ✗ | ✗ | 114+ | 使用弹窗 |
| 102+ | 115+ | 16.4+ | 102+ | 使用local存储+手动清理 |
| 120+ | ✓ | ✗ | 120+ | 内容脚本 |
Polyfill Setup
Polyfill配置
Using webextension-polyfill
使用webextension-polyfill
The Mozilla webextension-polyfill normalizes the Chrome callback-style API to Firefox's Promise-based API.
Mozilla的webextension-polyfill可将Chrome的回调式API统一为Firefox的Promise式API。
Installation
安装
bash
npm install webextension-polyfillbash
npm install webextension-polyfillTypeScript types
TypeScript类型定义
npm install -D @anthropic-ai/anthropic-sdk-types/webextension-polyfill
undefinednpm install -D @anthropic-ai/anthropic-sdk-types/webextension-polyfill
undefinedUsage in Background Script
后台脚本中的使用
typescript
// background.ts
import browser from 'webextension-polyfill';
browser.runtime.onMessage.addListener(async (message, sender) => {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
return { tabId: tabs[0]?.id };
});typescript
// background.ts
import browser from 'webextension-polyfill';
browser.runtime.onMessage.addListener(async (message, sender) => {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
return { tabId: tabs[0]?.id };
});Usage in Content Script
内容脚本中的使用
typescript
// content.ts
import browser from 'webextension-polyfill';
const response = await browser.runtime.sendMessage({ type: 'getData' });
console.log(response);typescript
// content.ts
import browser from 'webextension-polyfill';
const response = await browser.runtime.sendMessage({ type: 'getData' });
console.log(response);WXT Framework (Recommended)
WXT框架(推荐)
WXT provides built-in polyfill support:
typescript
// No import needed - browser is global
export default defineContentScript({
matches: ['*://*.example.com/*'],
main() {
// browser.* works everywhere
browser.runtime.sendMessage({ type: 'init' });
},
});WXT框架内置了Polyfill支持:
typescript
// No import needed - browser is global
export default defineContentScript({
matches: ['*://*.example.com/*'],
main() {
// browser.* works everywhere
browser.runtime.sendMessage({ type: 'init' });
},
});Feature Detection Patterns
特性检测模式
Check API Availability
检查API可用性
typescript
// Check if API exists
function hasAPI(api: string): boolean {
const parts = api.split('.');
let obj: any = typeof browser !== 'undefined' ? browser : chrome;
for (const part of parts) {
if (obj && typeof obj[part] !== 'undefined') {
obj = obj[part];
} else {
return false;
}
}
return true;
}
// Usage
if (hasAPI('sidePanel.open')) {
browser.sidePanel.open({ windowId });
} else {
// Fallback to popup
browser.action.openPopup();
}typescript
// Check if API exists
function hasAPI(api: string): boolean {
const parts = api.split('.');
let obj: any = typeof browser !== 'undefined' ? browser : chrome;
for (const part of parts) {
if (obj && typeof obj[part] !== 'undefined') {
obj = obj[part];
} else {
return false;
}
}
return true;
}
// Usage
if (hasAPI('sidePanel.open')) {
browser.sidePanel.open({ windowId });
} else {
// Fallback to popup
browser.action.openPopup();
}Runtime Browser Detection
运行时浏览器检测
typescript
// Detect browser at runtime
function getBrowser(): 'chrome' | 'firefox' | 'safari' | 'edge' | 'unknown' {
const ua = navigator.userAgent;
if (ua.includes('Firefox')) return 'firefox';
if (ua.includes('Safari') && !ua.includes('Chrome')) return 'safari';
if (ua.includes('Edg/')) return 'edge';
if (ua.includes('Chrome')) return 'chrome';
return 'unknown';
}
// Detect from extension APIs
function getBrowserFromAPIs(): 'chrome' | 'firefox' | 'safari' | 'edge' {
if (typeof browser !== 'undefined') {
// @anthropic-ai/anthropic-sdk-ts-expect-error - browser_specific_settings only in Firefox
if (browser.runtime.getBrowserInfo) return 'firefox';
return 'safari';
}
if (navigator.userAgent.includes('Edg/')) return 'edge';
return 'chrome';
}typescript
// Detect browser at runtime
function getBrowser(): 'chrome' | 'firefox' | 'safari' | 'edge' | 'unknown' {
const ua = navigator.userAgent;
if (ua.includes('Firefox')) return 'firefox';
if (ua.includes('Safari') && !ua.includes('Chrome')) return 'safari';
if (ua.includes('Edg/')) return 'edge';
if (ua.includes('Chrome')) return 'chrome';
return 'unknown';
}
// Detect from extension APIs
function getBrowserFromAPIs(): 'chrome' | 'firefox' | 'safari' | 'edge' {
if (typeof browser !== 'undefined') {
// @anthropic-ai/anthropic-sdk-ts-expect-error - browser_specific_settings only in Firefox
if (browser.runtime.getBrowserInfo) return 'firefox';
return 'safari';
}
if (navigator.userAgent.includes('Edg/')) return 'edge';
return 'chrome';
}Feature Flags Pattern
特性标志模式
typescript
// features.ts
export const FEATURES = {
sidePanel: hasAPI('sidePanel'),
offscreen: hasAPI('offscreen'),
sessionStorage: hasAPI('storage.session'),
userScripts: hasAPI('userScripts'),
declarativeNetRequest: hasAPI('declarativeNetRequest'),
} as const;
// Usage
import { FEATURES } from './features';
if (FEATURES.sidePanel) {
// Use side panel
} else {
// Use popup alternative
}typescript
// features.ts
export const FEATURES = {
sidePanel: hasAPI('sidePanel'),
offscreen: hasAPI('offscreen'),
sessionStorage: hasAPI('storage.session'),
userScripts: hasAPI('userScripts'),
declarativeNetRequest: hasAPI('declarativeNetRequest'),
} as const;
// Usage
import { FEATURES } from './features';
if (FEATURES.sidePanel) {
// Use side panel
} else {
// Use popup alternative
}Browser-Specific Patterns
浏览器特定模式
Firefox-Specific
Firefox专属配置
Gecko ID (Required)
Gecko ID(必填)
json
{
"browser_specific_settings": {
"gecko": {
"id": "my-extension@example.com",
"strict_min_version": "109.0"
}
}
}json
{
"browser_specific_settings": {
"gecko": {
"id": "my-extension@example.com",
"strict_min_version": "109.0"
}
}
}Data Collection Permissions (2025+)
数据收集权限(2025年起)
json
{
"browser_specific_settings": {
"gecko": {
"id": "my-extension@example.com",
"data_collection_permissions": {
"required": [],
"optional": ["technicalAndInteraction"]
}
}
}
}json
{
"browser_specific_settings": {
"gecko": {
"id": "my-extension@example.com",
"data_collection_permissions": {
"required": [],
"optional": ["technicalAndInteraction"]
}
}
}
}Firefox Android Support
Firefox Android支持
json
{
"browser_specific_settings": {
"gecko": {
"id": "my-extension@example.com"
},
"gecko_android": {
"strict_min_version": "120.0"
}
}
}json
{
"browser_specific_settings": {
"gecko": {
"id": "my-extension@example.com"
},
"gecko_android": {
"strict_min_version": "120.0"
}
}
}Safari-Specific
Safari专属配置
Privacy Manifest Requirement
隐私清单要求
Safari extensions require a host app with :
PrivacyInfo.xcprivacyxml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
</dict>
</plist>Safari扩展需要宿主应用包含:
PrivacyInfo.xcprivacyxml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
</dict>
</plist>Safari Limitations Handling
Safari限制处理
typescript
// Safari doesn't support webRequest blocking
async function blockRequest(details: WebRequestDetails) {
const browser = getBrowser();
if (browser === 'safari') {
// Use declarativeNetRequest instead
await browser.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: 1,
action: { type: 'block' },
condition: { urlFilter: details.url }
}]
});
} else {
// Use webRequestBlocking
return { cancel: true };
}
}typescript
// Safari doesn't support webRequest blocking
async function blockRequest(details: WebRequestDetails) {
const browser = getBrowser();
if (browser === 'safari') {
// Use declarativeNetRequest instead
await browser.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: 1,
action: { type: 'block' },
condition: { urlFilter: details.url }
}]
});
} else {
// Use webRequestBlocking
return { cancel: true };
}
}Chrome-Specific
Chrome专属配置
Service Worker State Persistence
Service Worker状态持久化
typescript
// Chrome service workers terminate after ~5 minutes
// Always persist state to storage
// BAD: State lost on worker termination
let count = 0;
// GOOD: Persist to storage
const countStorage = storage.defineItem<number>('local:count', {
defaultValue: 0
});
async function increment() {
const count = await countStorage.getValue();
await countStorage.setValue(count + 1);
}typescript
// Chrome service workers terminate after ~5 minutes
// Always persist state to storage
// BAD: State lost on worker termination
let count = 0;
// GOOD: Persist to storage
const countStorage = storage.defineItem<number>('local:count', {
defaultValue: 0
});
async function increment() {
const count = await countStorage.getValue();
await countStorage.setValue(count + 1);
}Offscreen Documents (Chrome/Edge only)
离屏文档(仅Chrome/Edge支持)
typescript
// For DOM access in MV3 service worker
if (hasAPI('offscreen')) {
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_PARSER'],
justification: 'Parse HTML content'
});
}typescript
// For DOM access in MV3 service worker
if (hasAPI('offscreen')) {
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_PARSER'],
justification: 'Parse HTML content'
});
}Manifest Differences
Manifest差异
Cross-Browser Manifest Generation
跨浏览器Manifest生成
typescript
// wxt.config.ts
export default defineConfig({
manifest: ({ browser }) => ({
name: 'My Extension',
version: '1.0.0',
// Chrome/Edge
...(browser === 'chrome' && {
minimum_chrome_version: '116',
}),
// Firefox
...(browser === 'firefox' && {
browser_specific_settings: {
gecko: {
id: 'my-extension@example.com',
strict_min_version: '109.0',
},
},
}),
// Different permissions per browser
permissions: [
'storage',
'activeTab',
...(browser !== 'safari' ? ['notifications'] : []),
],
}),
});typescript
// wxt.config.ts
export default defineConfig({
manifest: ({ browser }) => ({
name: 'My Extension',
version: '1.0.0',
// Chrome/Edge
...(browser === 'chrome' && {
minimum_chrome_version: '116',
}),
// Firefox
...(browser === 'firefox' && {
browser_specific_settings: {
gecko: {
id: 'my-extension@example.com',
strict_min_version: '109.0',
},
},
}),
// Different permissions per browser
permissions: [
'storage',
'activeTab',
...(browser !== 'safari' ? ['notifications'] : []),
],
}),
});MV2 vs MV3 Differences
MV2与MV3的差异
| Feature | MV2 | MV3 |
|---|---|---|
| Background | | |
| Remote code | Allowed | Forbidden |
| Eval strings allowed | Functions only |
| Content security | Relaxed CSP | Strict CSP |
| Supported | Use DNR |
| 特性 | MV2 | MV3 |
|---|---|---|
| 后台脚本 | | |
| 远程代码 | 允许 | 禁止 |
| 允许执行字符串代码 | 仅支持函数 |
| 内容安全策略 | 宽松CSP | 严格CSP |
| 支持 | 使用DNR替代 |
Testing Cross-Browser
跨浏览器测试
Manual Testing Matrix
手动测试矩阵
markdown
| Feature | Chrome | Firefox | Safari | Edge | Notes |
|---------|--------|---------|--------|------|-------|
| Install | [ ] | [ ] | [ ] | [ ] | |
| Popup opens | [ ] | [ ] | [ ] | [ ] | |
| Content script | [ ] | [ ] | [ ] | [ ] | |
| Background messages | [ ] | [ ] | [ ] | [ ] | |
| Storage sync | [ ] | [ ] | [ ] | [ ] | |markdown
| 功能 | Chrome | Firefox | Safari | Edge | 说明 |
|---------|--------|---------|--------|------|-------|
| 安装 | [ ] | [ ] | [ ] | [ ] | |
| 弹窗打开 | [ ] | [ ] | [ ] | [ ] | |
| 内容脚本 | [ ] | [ ] | [ ] | [ ] | |
| 后台消息通信 | [ ] | [ ] | [ ] | [ ] | |
| 存储同步 | [ ] | [ ] | [ ] | [ ] | |Automated Testing
自动化测试
typescript
// tests/browser-compat.test.ts
import { describe, it, expect } from 'vitest';
import { fakeBrowser } from 'wxt/testing';
describe('cross-browser compatibility', () => {
it('handles missing sidePanel API', async () => {
// Simulate Safari (no sidePanel)
delete (fakeBrowser as any).sidePanel;
const result = await openUI();
expect(result.method).toBe('popup');
});
it('handles missing notifications API', async () => {
delete (fakeBrowser as any).notifications;
const result = await notify('Test');
expect(result.fallback).toBe('console');
});
});typescript
// tests/browser-compat.test.ts
import { describe, it, expect } from 'vitest';
import { fakeBrowser } from 'wxt/testing';
describe('cross-browser compatibility', () => {
it('handles missing sidePanel API', async () => {
// Simulate Safari (no sidePanel)
delete (fakeBrowser as any).sidePanel;
const result = await openUI();
expect(result.method).toBe('popup');
});
it('handles missing notifications API', async () => {
delete (fakeBrowser as any).notifications;
const result = await notify('Test');
expect(result.fallback).toBe('console');
});
});Common Compatibility Issues
常见兼容性问题
Issue: tabs.query Returns Different Results
问题:tabs.query返回结果不一致
Problem: Safari returns fewer tab properties.
Solution:
typescript
const tabs = await browser.tabs.query({ active: true });
const tab = tabs[0];
// Always check property existence
const url = tab?.url ?? 'unknown';
const favIconUrl = tab?.favIconUrl ?? '/default-icon.png';问题: Safari返回的标签页属性更少。
解决方案:
typescript
const tabs = await browser.tabs.query({ active: true });
const tab = tabs[0];
// Always check property existence
const url = tab?.url ?? 'unknown';
const favIconUrl = tab?.favIconUrl ?? '/default-icon.png';Issue: Storage Quota Differences
问题:存储配额差异
| Browser | Local | Sync | Session |
|---|---|---|---|
| Chrome | 10MB | 100KB | 10MB |
| Firefox | Unlimited | 100KB | 10MB |
| Safari | 10MB | 100KB | 10MB |
Solution:
typescript
async function safeStore(key: string, data: unknown) {
const size = new Blob([JSON.stringify(data)]).size;
if (size > 100 * 1024 && storageArea === 'sync') {
console.warn('Data too large for sync, using local');
await browser.storage.local.set({ [key]: data });
} else {
await browser.storage[storageArea].set({ [key]: data });
}
}| 浏览器 | Local存储 | Sync存储 | Session存储 |
|---|---|---|---|
| Chrome | 10MB | 100KB | 10MB |
| Firefox | 无限制 | 100KB | 10MB |
| Safari | 10MB | 100KB | 10MB |
解决方案:
typescript
async function safeStore(key: string, data: unknown) {
const size = new Blob([JSON.stringify(data)]).size;
if (size > 100 * 1024 && storageArea === 'sync') {
console.warn('Data too large for sync, using local');
await browser.storage.local.set({ [key]: data });
} else {
await browser.storage[storageArea].set({ [key]: data });
}
}Issue: webRequest Blocking Not Working
问题:webRequest拦截失效
Problem: Safari doesn't support blocking webRequests.
Solution: Use declarativeNetRequest for all browsers:
typescript
// Works in all browsers
await browser.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [1],
addRules: [{
id: 1,
priority: 1,
action: { type: 'block' },
condition: {
urlFilter: '*://ads.example.com/*',
resourceTypes: ['script', 'image']
}
}]
});问题: Safari不支持webRequest拦截。
解决方案: 对所有浏览器使用declarativeNetRequest:
typescript
// Works in all browsers
await browser.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [1],
addRules: [{
id: 1,
priority: 1,
action: { type: 'block' },
condition: {
urlFilter: '*://ads.example.com/*',
resourceTypes: ['script', 'image']
}
}]
});References
参考资料
- MDN WebExtensions API
- Chrome Extensions Reference
- Safari Web Extensions
- webextension-polyfill
- browser-compatibility-matrix style