Реализация Popup-интерфейса браузерного расширения
Popup — это HTML-страница, которая открывается при клике на иконку расширения в тулбаре. Она живёт от нажатия до закрытия (потеря фокуса или явное закрытие) и не сохраняет состояние между открытиями — это нужно учитывать при проектировании.
Ограничения Popup
- Максимальный размер: ~800×600 px, Chrome ограничивает высоту viewport
- Жизненный цикл: создаётся при открытии, уничтожается при закрытии
- Нет
alert(),confirm()— заблокированы браузером - CSP запрещает
inline scriptsиeval— весь JS должен быть в.jsфайлах - Нельзя открывать по программе из content script без user gesture (в MV3)
Структура файлов
popup/
├── popup.html
├── popup.js (или popup.tsx + сборка)
└── popup.css
<!-- popup.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=350">
<link rel="stylesheet" href="popup.css">
<!-- NO inline scripts — нарушение CSP -->
</head>
<body>
<div id="app"></div>
<script src="popup.js"></script>
</body>
</html>
React в Popup
Popup с React — стандартная практика для сложных интерфейсов:
// popup/App.tsx
import { useEffect, useState } from 'react';
import browser from 'webextension-polyfill';
interface TabInfo {
url: string;
title: string;
}
export function App() {
const [tab, setTab] = useState<TabInfo | null>(null);
const [enabled, setEnabled] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function init() {
// Получить данные активной вкладки
const [activeTab] = await browser.tabs.query({ active: true, currentWindow: true });
setTab({ url: activeTab.url ?? '', title: activeTab.title ?? '' });
// Загрузить сохранённые настройки
const { extensionEnabled } = await browser.storage.local.get('extensionEnabled');
setEnabled(extensionEnabled ?? false);
setLoading(false);
}
init();
}, []);
async function toggleEnabled() {
const next = !enabled;
setEnabled(next);
await browser.storage.local.set({ extensionEnabled: next });
// Уведомить content script на активной вкладке
if (tab) {
const [activeTab] = await browser.tabs.query({ active: true, currentWindow: true });
if (activeTab.id) {
browser.tabs.sendMessage(activeTab.id, { type: 'TOGGLE', enabled: next });
}
}
}
if (loading) return <div className="popup-loading">Загрузка...</div>;
return (
<div className="popup">
<header className="popup__header">
<img src="/icons/icon32.png" alt="" />
<h1>My Extension</h1>
</header>
<section className="popup__body">
<div className="popup__url" title={tab?.url}>{tab?.title}</div>
<label className="toggle">
<input
type="checkbox"
checked={enabled}
onChange={toggleEnabled}
/>
<span className="toggle__label">
{enabled ? 'Включено' : 'Выключено'}
</span>
</label>
</section>
<footer className="popup__footer">
<button onClick={() => browser.runtime.openOptionsPage()}>
Настройки
</button>
<button onClick={() => browser.tabs.create({ url: 'https://example.com/help' })}>
Помощь
</button>
</footer>
</div>
);
}
Размер и стилизация
Popup должен быть удобен в диапазоне 320–400 px ширины. Типичная вёрстка:
/* popup.css */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
width: 360px;
min-height: 200px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
color: #1a1a1a;
background: #fff;
}
.popup {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.popup__header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
}
.popup__header h1 {
font-size: 15px;
font-weight: 600;
}
.popup__body {
flex: 1;
padding: 16px;
}
.popup__footer {
display: flex;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid #e5e7eb;
}
.popup__footer button {
flex: 1;
padding: 6px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: #fff;
cursor: pointer;
font-size: 13px;
}
.popup__footer button:hover {
background: #f3f4f6;
}
Связь Popup ↔ Background ↔ Content Script
// Паттерн запроса данных у content script через background
async function getPageData() {
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
if (!tab.id || !tab.url?.startsWith('http')) {
return null; // расширение не может работать на chrome:// страницах
}
try {
const data = await browser.tabs.sendMessage(tab.id, { type: 'GET_PAGE_DATA' });
return data;
} catch {
// Content script не загружен на этой странице
return null;
}
}
Сохранение состояния между открытиями
Popup уничтожается при закрытии, поэтому состояние хранится в storage:
// Сохранять позицию скролла, открытый таб, введённый текст
async function saveUiState(state) {
await browser.storage.session.set({ popupUiState: state }); // session — только до закрытия браузера
}
async function loadUiState() {
const { popupUiState } = await browser.storage.session.get('popupUiState');
return popupUiState ?? defaultState;
}
chrome.storage.session доступен с Chrome 102 и поддерживается в Edge/Opera. Firefox пока не поддерживает — там fallback на storage.local.
Badge на иконке
Popup часто дополняется badge — счётчиком или индикатором на иконке:
// Из background service worker
chrome.action.setBadgeText({ tabId: tab.id, text: '5' });
chrome.action.setBadgeBackgroundColor({ color: '#ef4444' });
chrome.action.setBadgeTextColor({ color: '#ffffff' }); // Chrome 110+
// Убрать badge
chrome.action.setBadgeText({ tabId: tab.id, text: '' });
Сроки
Popup с React, загрузкой данных вкладки, переключателями и навигацией к настройкам — 2–3 рабочих дня. Popup с комплексным UI (несколько вкладок, формы, реалтайм-данные из content script) — 4–6 дней.







