Реализация Options Page (страница настроек) браузерного расширения
Options Page — отдельная страница расширения для управления настройками. В отличие от Popup, она не закрывается сама по себе и имеет полноэкранное пространство. Открывается через chrome.runtime.openOptionsPage() или из меню расширений браузера.
Два режима отображения
Полноэкранная страница (options_page) — открывается в отдельной вкладке браузера. Больше места, привычный интерфейс.
Встроенная в браузер (options_ui) — открывается прямо в chrome://extensions/ в нижней части карточки расширения. Доступна только в Chrome/Edge.
{
"manifest_version": 3,
"options_page": "options/options.html",
// ИЛИ (взаимоисключающие):
"options_ui": {
"page": "options/options.html",
"open_in_tab": false
}
}
open_in_tab: false — показывать прямо в chrome://extensions/. open_in_tab: true — открывать в новой вкладке (аналогично options_page).
Структура React-приложения настроек
// options/App.tsx
import { useEffect, useState } from 'react';
import browser from 'webextension-polyfill';
interface Settings {
enabled: boolean;
theme: 'light' | 'dark' | 'system';
highlightColor: string;
blockedDomains: string[];
shortcut: string;
}
const defaultSettings: Settings = {
enabled: true,
theme: 'system',
highlightColor: '#fbbf24',
blockedDomains: [],
shortcut: 'Ctrl+Shift+Y',
};
export function OptionsApp() {
const [settings, setSettings] = useState<Settings>(defaultSettings);
const [saved, setSaved] = useState(false);
useEffect(() => {
browser.storage.sync.get('settings').then(({ settings: stored }) => {
if (stored) setSettings({ ...defaultSettings, ...stored });
});
}, []);
async function saveSettings(updated: Settings) {
setSettings(updated);
await browser.storage.sync.set({ settings: updated });
setSaved(true);
setTimeout(() => setSaved(false), 2000);
}
return (
<div className="options">
<h1>Настройки расширения</h1>
<GeneralSection settings={settings} onChange={saveSettings} />
<AppearanceSection settings={settings} onChange={saveSettings} />
<BlocklistSection settings={settings} onChange={saveSettings} />
{saved && <div className="options__saved">Настройки сохранены</div>}
</div>
);
}
Секции настроек
function GeneralSection({ settings, onChange }) {
return (
<section className="options__section">
<h2>Основные</h2>
<label className="options__field">
<span>Расширение активно</span>
<input
type="checkbox"
checked={settings.enabled}
onChange={e => onChange({ ...settings, enabled: e.target.checked })}
/>
</label>
</section>
);
}
function AppearanceSection({ settings, onChange }) {
return (
<section className="options__section">
<h2>Внешний вид</h2>
<label className="options__field">
<span>Тема</span>
<select
value={settings.theme}
onChange={e => onChange({ ...settings, theme: e.target.value as Settings['theme'] })}
>
<option value="system">Системная</option>
<option value="light">Светлая</option>
<option value="dark">Тёмная</option>
</select>
</label>
<label className="options__field">
<span>Цвет выделения</span>
<input
type="color"
value={settings.highlightColor}
onChange={e => onChange({ ...settings, highlightColor: e.target.value })}
/>
</label>
</section>
);
}
Список блокировок с редактированием
function BlocklistSection({ settings, onChange }) {
const [newDomain, setNewDomain] = useState('');
const addDomain = () => {
const trimmed = newDomain.trim().replace(/^https?:\/\//, '').replace(/\/$/, '');
if (!trimmed || settings.blockedDomains.includes(trimmed)) return;
onChange({ ...settings, blockedDomains: [...settings.blockedDomains, trimmed] });
setNewDomain('');
};
const removeDomain = (domain: string) => {
onChange({
...settings,
blockedDomains: settings.blockedDomains.filter(d => d !== domain),
});
};
return (
<section className="options__section">
<h2>Исключённые сайты</h2>
<div className="options__add-domain">
<input
value={newDomain}
onChange={e => setNewDomain(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addDomain()}
placeholder="example.com"
/>
<button onClick={addDomain}>Добавить</button>
</div>
<ul className="options__domain-list">
{settings.blockedDomains.map(domain => (
<li key={domain}>
<span>{domain}</span>
<button onClick={() => removeDomain(domain)}>✕</button>
</li>
))}
</ul>
</section>
);
}
Открытие Options Page из других контекстов
// Из Popup
document.getElementById('btn-settings').addEventListener('click', () => {
browser.runtime.openOptionsPage();
});
// Из Background (Service Worker)
chrome.action.onClicked.addListener(() => {
chrome.runtime.openOptionsPage();
});
// Прямая ссылка (например, в onboarding-странице)
const optionsUrl = browser.runtime.getURL('options/options.html');
browser.tabs.create({ url: optionsUrl });
Синхронизация настроек между устройствами
browser.storage.sync автоматически синхронизирует данные через Google/Microsoft аккаунт:
// Подписка на изменения — если пользователь изменил настройки на другом устройстве
browser.storage.onChanged.addListener((changes, area) => {
if (area === 'sync' && changes.settings) {
const newSettings = changes.settings.newValue;
applySettingsToContentScripts(newSettings);
}
});
async function applySettingsToContentScripts(settings) {
const tabs = await browser.tabs.query({ url: ['http://*/*', 'https://*/*'] });
for (const tab of tabs) {
browser.tabs.sendMessage(tab.id, {
type: 'SETTINGS_UPDATED',
settings,
}).catch(() => {}); // игнорируем, если content script не загружен
}
}
Сброс настроек
async function resetToDefaults() {
if (!confirm('Сбросить все настройки к значениям по умолчанию?')) return;
await browser.storage.sync.remove('settings');
setSettings(defaultSettings);
}
Сроки
Options Page с базовыми настройками (5–10 параметров), сохранением в storage.sync и открытием из Popup — 1–2 рабочих дня.







