חלונות פופ-אפ של תוספי Chrome נסגרים כאשר לוחצים מחוץ להם — זה קריטי לכלים הדורשים גישה מתמדת. ממשק ה-Side Panel API פותר בעיה זו על ידי מתן פאנל צדדי קבוע. משתמשים בתוספים לרישום הערות, ניטור או כלי פיתוח מאבדים לעיתים קרובות נתונים כאשר הפופ-אפ נסגר בטעות. ה-Side Panel נשאר פתוח, שומר על המצב בעת מעבר בין טאבים. אנו מיישמים פאנלים כאלה במפתח מלא תוך 3–9 ימים. נבחן את הפרויקט שלך — צור קשר. הניסיון שלנו עם Chrome API הוא למעלה מ-5 שנים, ואנו מבטיחים תמיכה לאחר ההשקה. קבל ייעוץ בנושא אינטגרציה.
לפי Side Panel API, ניתן להגדיר את הפאנל בנפרד עבור כל טאב. בפרויקט אחד, החלפנו את הפופ-אפ ב-Side Panel עבור תוסף CRM. משתמשים הפסיקו לאבד הזמנות פעילות בעת מעבר בין טאבים, וזמן העבודה עם התוסף גדל ב-40%.
כיצד Side Panel פותר את בעיית אובדן ההקשר?
הפופ-אפ חי רק כל עוד הטאב פעיל והמשתמש לא לחץ מחוץ לחלון. ה-Side Panel נשאר פתוח בעת מעבר בין טאבים ואפילו בעבודה בחלון אחר. זה קריטי לתוספים דמויי DevTools, ניטור, רישום הערות וצ'אטים. מעבר בין טאבים אינו דורש טעינה מחדש של הפאנל — הנתונים נשמרים בזיכרון.
הבדלים בין Side Panel לפופ-אפ
| מאפיין | פופ-אפ | Side Panel |
|---|---|---|
| רוחב | מקסימום 800 פיקסלים | ~400 פיקסלים (קבוע על ידי Chrome) |
| מחזור חיים | עד אובדן פוקוס | קבוע, שורד מעברי טאבים |
| הקשר טאב | קשור לטאב הפעיל | יכול להיות גלובלי או לכל טאב |
| זמינות | מאז Chrome 4 | מאז Chrome 114 |
| Firefox/Safari | אין אנלוג | אין אנלוג |
כיצד להגדיר Side Panel עבור טאבים שונים?
הפאנל יכול להיות שונה עבור כל טאב או משותף לכולם. זה מוגדר באמצעות chrome.sidePanel.setOptions עם הפרמטר tabId. לדוגמה, עבור GitHub אנו מציגים פאנל עם משימות, עבור Figma — עם תגובות, עבור אחרים — מנטרלים אותו. חיסכון במשאבים: הפאנל לא נטען באתרים מיותרים.
הגדרת Manifest V3
{
"manifest_version": 3,
"name": "My Side Panel Extension",
"version": "1.0.0",
"permissions": ["storage", "tabs", "activeTab", "scripting", "sidePanel"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_icon": "icons/icon48.png",
"default_title": "Открыть панель"
},
"side_panel": {
"default_path": "panel/panel.html"
}
} תצורת manifest.json מפורטת
ודא שכללת את כל ההרשאות הנדרשות. ההרשאה { "manifest_version": 3, "name": "My Side Panel Extension", "version": "1.0.0", "permissions": ["storage", "tabs", "activeTab", "scripting", "sidePanel"], "background": { "service_worker": "background.js" }, "action": { "default_icon": "icons/icon48.png", "default_title": "Открыть панель" }, "side_panel": { "default_path": "panel/panel.html" } } היא חובה. בנוסף, sidePanel נדרשת אם אתה שומר נתונים. לגישה לתוכן הטאב, הוסף storage ו-activeTab.
פתיחת פאנל ולוגיקת טאבים (background.js מלא)
// background.js — объединённый Service Worker
// Открытие по клику на иконку
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(console.error);
// Per-tab панели (разные для GitHub и Figma)
chrome.tabs.onUpdated.addListener(async (tabId, info, tab) => {
if (info.status !== 'complete') return;
if (tab.url?.includes('github.com')) {
await chrome.sidePanel.setOptions({ tabId, path: 'panel/github-panel.html', enabled: true });
} else if (tab.url?.includes('figma.com')) {
await chrome.sidePanel.setOptions({ tabId, path: 'panel/design-panel.html', enabled: true });
} else {
await chrome.sidePanel.setOptions({ tabId, enabled: false });
}
});
// Постоянное соединение с Side Panel через Port
const sidePanelPorts = new Map();
chrome.runtime.onConnect.addListener((port) => {
if (port.name !== 'side-panel') return;
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
sidePanelPorts.set(tab.id, port);
port.onDisconnect.addListener(() => sidePanelPorts.delete(tab.id));
});
});
// Пересылка сообщений от content script в панель
chrome.runtime.onMessage.addListener((msg, sender) => {
if (msg.type === 'SELECTION_CHANGED') {
const port = sidePanelPorts.get(sender.tab?.id);
port?.postMessage({ type: 'SELECTION_CHANGED', text: msg.text });
}
});
// Отслеживание смены вкладок
chrome.tabs.onActivated.addListener(async ({ tabId }) => {
const tab = await chrome.tabs.get(tabId);
const port = sidePanelPorts.get(tabId);
port?.postMessage({ type: 'PAGE_CHANGED', url: tab.url, title: tab.title });
});
chrome.tabs.onUpdated.addListener((tabId, info, tab) => {
if (info.status !== 'complete') return;
const port = sidePanelPorts.get(tabId);
port?.postMessage({ type: 'PAGE_CHANGED', url: tab.url, title: tab.title });
});
יישום React ב-Side Panel
המבנה של scripting דומה לפופ-אפ, אך יש יותר מקום. אנו משתמשים ב-React לבניית הממשק — זה מאפשר שימוש חוזר ברכיבים ומפשט את התחזוקה.
// panel/App.tsx
import { useEffect, useState, useRef } from 'react';
import browser from 'webextension-polyfill';
export function SidePanel() {
const [notes, setNotes] = useState<string[]>([]);
const [currentUrl, setCurrentUrl] = useState('');
const portRef = useRef<browser.Runtime.Port | null>(null);
useEffect(() => {
portRef.current = browser.runtime.connect({ name: 'side-panel' });
portRef.current.onMessage.addListener((msg) => {
if (msg.type === 'PAGE_CHANGED') {
setCurrentUrl(msg.url);
loadNotesForUrl(msg.url);
}
if (msg.type === 'SELECTION_CHANGED') {
handleNewSelection(msg.text);
}
});
browser.tabs.query({ active: true, currentWindow: true }).then(([tab]) => {
if (tab.url) {
setCurrentUrl(tab.url);
loadNotesForUrl(tab.url);
}
});
return () => portRef.current?.disconnect();
}, []);
async function loadNotesForUrl(url: string) {
const domain = new URL(url).hostname;
const { notes } = await browser.storage.local.get(`notes_${domain}`);
setNotes(notes ?? []);
}
async function addNote(text: string) {
const domain = new URL(currentUrl).hostname;
const updated = [...notes, text];
setNotes(updated);
await browser.storage.local.set({ [`notes_${domain}`]: updated });
}
return (
<div className="side-panel">
<header className="side-panel__header">
<h2>Заметки</h2>
<span className="side-panel__url">{new URL(currentUrl).hostname}</span>
</header>
<div className="side-panel__notes">
{notes.map((note, i) => (
<div key={i} className="note">{note}</div>
))}
</div>
<NoteInput onAdd={addNote} />
</div>
);
} תקשורת עם Content Script דרך Port
ה-Side Panel חי זמן רב — נוח להשתמש בחיבורים קבועים במקום sendMessage חד-פעמי. ה-Content Script עוקב אחר בחירת טקסט ושולח אותו ל-background, שמעביר אותו לפאנל.
// content.js — следит за выделением текста
document.addEventListener('mouseup', () => {
const selection = window.getSelection()?.toString().trim();
if (selection && selection.length > 3) {
chrome.runtime.sendMessage({ type: 'SELECTION_CHANGED', text: selection });
}
});
למה לבחור ב-React עבור Side Panel?
React מאפשר פירוק הממשק לרכיבים, ניהול מצב בקלות ושימוש חוזר בלוגיקה. בשילוב עם Port API, אנו מקבלים פאנל ראקטיבי שמגיב מיידית לפעולות המשתמש בדף. לדוגמה, כאשר טקסט נבחר בדף, הפאנל מציג אותו מיד — עיכוב מתחת ל-50 אלפיות השנייה. ה-Side Panel צורך פחות זיכרון מדפי background ואינו משפיע על LCP. אנו מייעלים טעינה: פיצול חבילות, tree-shake, מיניפיקציה של רכיבי React. זמן פתיחת הפאנל הוא מתחת ל-200 אלפיות השנייה.
השוואה: React לעומת Vanilla JS עבור Side Panel
| מאפיין | Vanilla JS | React |
|---|---|---|
| ניהול מצב | ידני | מובנה (useState) |
| מדרגיות | מוגבלת | מבוסס רכיבים |
| מהירות פיתוח | בינונית | גבוהה (שימוש חוזר) |
כיצד להוסיף Side Panel לתוסף שלך
- הוסף הרשאת
// background.js — объединённый Service Worker // Открытие по клику на иконку chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(console.error); // Per-tab панели (разные для GitHub и Figma) chrome.tabs.onUpdated.addListener(async (tabId, info, tab) => { if (info.status !== 'complete') return; if (tab.url?.includes('github.com')) { await chrome.sidePanel.setOptions({ tabId, path: 'panel/github-panel.html', enabled: true }); } else if (tab.url?.includes('figma.com')) { await chrome.sidePanel.setOptions({ tabId, path: 'panel/design-panel.html', enabled: true }); } else { await chrome.sidePanel.setOptions({ tabId, enabled: false }); } }); // Постоянное соединение с Side Panel через Port const sidePanelPorts = new Map(); chrome.runtime.onConnect.addListener((port) => { if (port.name !== 'side-panel') return; chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { sidePanelPorts.set(tab.id, port); port.onDisconnect.addListener(() => sidePanelPorts.delete(tab.id)); }); }); // Пересылка сообщений от content script в панель chrome.runtime.onMessage.addListener((msg, sender) => { if (msg.type === 'SELECTION_CHANGED') { const port = sidePanelPorts.get(sender.tab?.id); port?.postMessage({ type: 'SELECTION_CHANGED', text: msg.text }); } }); // Отслеживание смены вкладок chrome.tabs.onActivated.addListener(async ({ tabId }) => { const tab = await chrome.tabs.get(tabId); const port = sidePanelPorts.get(tabId); port?.postMessage({ type: 'PAGE_CHANGED', url: tab.url, title: tab.title }); }); chrome.tabs.onUpdated.addListener((tabId, info, tab) => { if (info.status !== 'complete') return; const port = sidePanelPorts.get(tabId); port?.postMessage({ type: 'PAGE_CHANGED', url: tab.url, title: tab.title }); });ב-manifest.json. - צור קובץ HTML לפאנל (לדוגמה, panel.html).
- ב-service worker, קרא ל-
panel/panel.html. - הגדר לוגיקת טאבים דרך
// panel/App.tsx import { useEffect, useState, useRef } from 'react'; import browser from 'webextension-polyfill'; export function SidePanel() { const [notes, setNotes] = useState<string[]>([]); const [currentUrl, setCurrentUrl] = useState(''); const portRef = useRef<browser.Runtime.Port | null>(null); useEffect(() => { portRef.current = browser.runtime.connect({ name: 'side-panel' }); portRef.current.onMessage.addListener((msg) => { if (msg.type === 'PAGE_CHANGED') { setCurrentUrl(msg.url); loadNotesForUrl(msg.url); } if (msg.type === 'SELECTION_CHANGED') { handleNewSelection(msg.text); } }); browser.tabs.query({ active: true, currentWindow: true }).then(([tab]) => { if (tab.url) { setCurrentUrl(tab.url); loadNotesForUrl(tab.url); } }); return () => portRef.current?.disconnect(); }, []); async function loadNotesForUrl(url: string) { const domain = new URL(url).hostname; const { notes } = await browser.storage.local.get(`notes_${domain}`); setNotes(notes ?? []); } async function addNote(text: string) { const domain = new URL(currentUrl).hostname; const updated = [...notes, text]; setNotes(updated); await browser.storage.local.set({ [`notes_${domain}`]: updated }); } return ( <div className="side-panel"> <header className="side-panel__header"> <h2>Заметки</h2> <span className="side-panel__url">{new URL(currentUrl).hostname}</span> </header> <div className="side-panel__notes"> {notes.map((note, i) => ( <div key={i} className="note">{note}</div> ))} </div> <NoteInput onAdd={addNote} /> </div> ); }. - הגדר React וקבע את Port API לתקשורת עם התוכן.
מה כלול בעבודה?
- ארכיטקטורה והגדרת Manifest V3
- יישום Side Panel עם React (או פריימוורק אחר)
- הגדרת לוגיקת טאבים (פאנלים שונים לאתרים שונים)
- אינטגרציה עם Content Script דרך Port API
- אחסון נתונים ב-chrome.storage
- בדיקות ואופטימיזציה (Core Web Vitals, LCP, CLS)
לוח זמנים
Side Panel בסיסי עם React, חיבור קבוע ל-background, סנכרון טאבים ואחסון נתונים — 3–5 ימי עסקים. פאנל עם אינטראקציה בזמן אמת עם תוכן הדף, היסטוריה וחיפוש — 6–9 ימים. העלות מחושבת בנפרד.
נבחן את הפרויקט שלך — צור קשר. אנו מבטיחים תמיכה לאחר ההשקה ותיקוני באגים תוך 30 יום. קבל ייעוץ בנושא אינטגרציה של Side Panel לתוסף שלך.







