Chapter 06 Popup Interface Development
Haiyue
37min
Chapter 6: Popup Interface Development
Learning Objectives
- Understand the purpose and characteristics of Chrome extension Popups
- Master HTML, CSS, and JavaScript development for Popup pages
- Learn communication between Popup and Background Script
- Implement responsive design and user experience optimization
6.1 Popup Overview
Popup is the small window interface displayed when users click the extension icon, serving as the primary entry point for user interaction with the extension.
6.1.1 Popup Characteristics
🔄 正在渲染 Mermaid 图表...
tip Core Features
- Small window interface, typically 320-800px wide
- Automatically closes when clicking outside the extension icon area
- Reloads every time it opens
- Can communicate with Background Script
- Supports full web technology stack
6.1.2 Manifest Configuration
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0",
"action": {
"default_popup": "popup.html",
"default_title": "My Extension",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"permissions": [
"storage",
"tabs"
]
}
6.2 HTML Structure Design
6.2.1 Basic HTML Template
<!-- popup.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Extension</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<!-- Header Area -->
<header class="header">
<div class="logo">
<img src="icons/icon48.png" alt="Logo">
<h1>My Extension</h1>
</div>
<div class="status-indicator" id="statusIndicator">
<span class="status-dot"></span>
<span class="status-text">Connected</span>
</div>
</header>
<!-- Main Content Area -->
<main class="main-content">
<!-- Action Buttons Group -->
<section class="action-buttons">
<button class="btn btn-primary" id="toggleFeature">
<span class="btn-icon">🔧</span>
<span class="btn-text">Enable Feature</span>
</button>
<button class="btn btn-secondary" id="analyzeePage">
<span class="btn-icon">📊</span>
<span class="btn-text">Analyze Page</span>
</button>
</section>
<!-- Information Display Area -->
<section class="info-section">
<div class="info-card" id="pageInfo">
<h3>Page Information</h3>
<div class="info-content">
<p class="url-info" id="urlInfo">Waiting to fetch...</p>
<p class="title-info" id="titleInfo">Waiting to fetch...</p>
</div>
</div>
<div class="info-card" id="statsInfo">
<h3>Usage Statistics</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-value" id="clickCount">0</span>
<span class="stat-label">Click Count</span>
</div>
<div class="stat-item">
<span class="stat-value" id="activeTime">0</span>
<span class="stat-label">Active Time</span>
</div>
</div>
</div>
</section>
<!-- Settings Area -->
<section class="settings-section">
<div class="setting-item">
<label class="setting-label">
<input type="checkbox" id="autoMode" class="setting-checkbox">
<span class="checkmark"></span>
Auto Mode
</label>
</div>
<div class="setting-item">
<label class="setting-label">Theme</label>
<select id="themeSelect" class="setting-select">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="auto">Follow System</option>
</select>
</div>
</section>
</main>
<!-- Footer Area -->
<footer class="footer">
<div class="footer-buttons">
<button class="btn btn-small" id="settingsBtn">Settings</button>
<button class="btn btn-small" id="helpBtn">Help</button>
</div>
</footer>
</div>
<!-- Loading Indicator -->
<div class="loading-overlay" id="loadingOverlay" style="display: none;">
<div class="spinner"></div>
<p>Processing...</p>
</div>
<script src="popup.js"></script>
</body>
</html>
6.2.2 HTML Generator Configuration Example
// popup-generator.js - Chrome Extension Popup HTML Generator
class PopupGenerator {
constructor() {
this.title = "Chrome Extension";
this.width = 400;
this.height = 600;
this.theme = "light";
this.components = [];
}
setConfig(title, width = 400, height = 600, theme = "light") {
this.title = title;
this.width = width;
this.height = height;
this.theme = theme;
}
addHeader(logoPath, statusText = "Connected") {
this.components.push({
type: "header",
logo: logoPath,
title: this.title,
status: statusText
});
}
addButtonGroup(buttons) {
this.components.push({
type: "button_group",
buttons: buttons
});
}
addInfoCard(title, contentType = "text", contentId = "") {
this.components.push({
type: "info_card",
title: title,
contentType: contentType,
contentId: contentId
});
}
addSettingsSection(settings) {
this.components.push({
type: "settings",
settings: settings
});
}
generateHTML() {
const componentsHTML = this._generateComponents();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${this.title}</title>
<link rel="stylesheet" href="popup.css">
<style>
body { width: ${this.width}px; min-height: ${this.height}px; }
</style>
</head>
<body class="${this.theme}-theme">
<div class="container">
${componentsHTML}
</div>
<script src="popup.js"></script>
</body>
</html>`;
}
_generateComponents() {
return this.components.map(component => {
switch (component.type) {
case "header":
return this._generateHeader(component);
case "button_group":
return this._generateButtonGroup(component);
case "info_card":
return this._generateInfoCard(component);
case "settings":
return this._generateSettings(component);
default:
return '';
}
}).join('\n');
}
_generateHeader(header) {
return `
<header class="header">
<div class="logo">
<img src="${header.logo}" alt="Logo">
<h1>${header.title}</h1>
</div>
<div class="status-indicator">
<span class="status-dot"></span>
<span class="status-text">${header.status}</span>
</div>
</header>`;
}
_generateButtonGroup(buttonGroup) {
const buttonsHTML = buttonGroup.buttons.map(button => `
<button class="btn btn-${button.style || 'primary'}" id="${button.id}">
<span class="btn-icon">${button.icon || '🔧'}</span>
<span class="btn-text">${button.text}</span>
</button>`).join('');
return `
<section class="action-buttons">
${buttonsHTML}
</section>`;
}
_generateInfoCard(infoCard) {
return `
<section class="info-section">
<div class="info-card" id="${infoCard.contentId}">
<h3>${infoCard.title}</h3>
<div class="info-content">
<p class="info-text">Waiting to fetch...</p>
</div>
</div>
</section>`;
}
_generateSettings(settingsSection) {
const settingsHTML = settingsSection.settings.map(setting => {
if (setting.type === "checkbox") {
return `
<div class="setting-item">
<label class="setting-label">
<input type="checkbox" id="${setting.id}" class="setting-checkbox">
<span class="checkmark"></span>
${setting.label}
</label>
</div>`;
} else if (setting.type === "select") {
const optionsHTML = setting.options.map(option =>
`<option value="${option.value}">${option.text}</option>`
).join('');
return `
<div class="setting-item">
<label class="setting-label">${setting.label}</label>
<select id="${setting.id}" class="setting-select">
${optionsHTML}
</select>
</div>`;
}
return '';
}).join('');
return `
<section class="settings-section">
${settingsHTML}
</section>`;
}
}
// Usage example
const generator = new PopupGenerator();
generator.setConfig("My Chrome Extension", 400, 600, "light");
// Add components
generator.addHeader("icons/icon48.png", "Connected");
generator.addButtonGroup([
{ id: "toggleFeature", text: "Enable Feature", icon: "🔧", style: "primary" },
{ id: "analyzePage", text: "Analyze Page", icon: "📊", style: "secondary" }
]);
generator.addInfoCard("Page Information", "text", "pageInfo");
generator.addSettingsSection([
{ type: "checkbox", id: "autoMode", label: "Auto Mode" },
{
type: "select",
id: "themeSelect",
label: "Theme",
options: [
{ value: "light", text: "Light" },
{ value: "dark", text: "Dark" },
{ value: "auto", text: "Follow System" }
]
}
]);
// Generate HTML
const htmlContent = generator.generateHTML();
console.log("Generated Popup HTML:");
console.log(htmlContent.substring(0, 500) + "..."); // Display first 500 characters
6.3 CSS Styling Design
6.3.1 Modern Styling
/* popup.css */
:root {
--primary-color: #4285f4;
--secondary-color: #34a853;
--danger-color: #ea4335;
--warning-color: #fbbc05;
--text-primary: #202124;
--text-secondary: #5f6368;
--background-primary: #ffffff;
--background-secondary: #f8f9fa;
--border-color: #dadce0;
--shadow-light: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-medium: 0 4px 6px rgba(0, 0, 0, 0.1);
--border-radius: 8px;
--transition: all 0.2s ease;
}
/* Dark theme */
.dark-theme {
--text-primary: #e8eaed;
--text-secondary: #9aa0a6;
--background-primary: #202124;
--background-secondary: #303134;
--border-color: #5f6368;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 400px;
min-height: 500px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
background-color: var(--background-primary);
overflow-x: hidden;
}
.container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* Header styles */
.header {
padding: 16px;
border-bottom: 1px solid var(--border-color);
background-color: var(--background-primary);
}
.logo {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.logo img {
width: 24px;
height: 24px;
border-radius: 4px;
}
.logo h1 {
font-size: 18px;
font-weight: 500;
color: var(--text-primary);
}
.status-indicator {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text-secondary);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--secondary-color);
}
/* Main content area */
.main-content {
flex: 1;
padding: 16px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* Button styles */
.action-buttons {
display: flex;
flex-direction: column;
gap: 8px;
}
.btn {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border: none;
border-radius: var(--border-radius);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
text-align: left;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-medium);
}
.btn:active {
transform: translateY(0);
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: #3367d6;
}
.btn-secondary {
background-color: var(--background-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background-color: var(--border-color);
}
.btn-small {
padding: 8px 12px;
font-size: 12px;
}
.btn-icon {
font-size: 16px;
}
/* Info cards */
.info-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.info-card {
padding: 16px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background-color: var(--background-secondary);
}
.info-card h3 {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
color: var(--text-primary);
}
.info-content {
font-size: 12px;
color: var(--text-secondary);
}
.url-info, .title-info {
margin-bottom: 4px;
word-break: break-word;
}
/* Stats grid */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.stat-item {
text-align: center;
}
.stat-value {
display: block;
font-size: 20px;
font-weight: 600;
color: var(--primary-color);
}
.stat-label {
display: block;
font-size: 11px;
color: var(--text-secondary);
margin-top: 2px;
}
/* Settings area */
.settings-section {
display: flex;
flex-direction: column;
gap: 12px;
padding-top: 12px;
border-top: 1px solid var(--border-color);
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
}
.setting-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-primary);
cursor: pointer;
}
/* Custom checkbox */
.setting-checkbox {
display: none;
}
.checkmark {
width: 16px;
height: 16px;
border: 2px solid var(--border-color);
border-radius: 3px;
position: relative;
transition: var(--transition);
}
.setting-checkbox:checked + .checkmark {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.setting-checkbox:checked + .checkmark::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 10px;
font-weight: bold;
}
/* Select box */
.setting-select {
padding: 6px 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--background-primary);
color: var(--text-primary);
font-size: 12px;
cursor: pointer;
}
.setting-select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2);
}
/* Footer area */
.footer {
padding: 12px 16px;
border-top: 1px solid var(--border-color);
background-color: var(--background-secondary);
}
.footer-buttons {
display: flex;
gap: 8px;
justify-content: center;
}
/* Loading indicator */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid var(--border-color);
border-top: 3px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive design */
@media (max-width: 320px) {
body {
width: 300px;
}
.container {
padding: 0 8px;
}
.btn {
padding: 10px 12px;
font-size: 13px;
}
}
/* Animation effects */
.fade-in {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.slide-in {
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
/* Focus accessibility */
button:focus,
select:focus,
input:focus {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
/* Scrollbar styles */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: var(--background-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
}
6.4 JavaScript Interaction Logic
6.4.1 Main Script File
// popup.js
class PopupManager {
constructor() {
this.isInitialized = false;
this.currentTab = null;
this.settings = {
autoMode: false,
theme: 'light'
};
this.init();
}
async init() {
try {
// Show loading state
this.showLoading(true);
// Get current tab
await this.getCurrentTab();
// Load settings
await this.loadSettings();
// Initialize UI
this.initializeUI();
// Bind events
this.bindEvents();
// Update page information
await this.updatePageInfo();
// Update statistics
await this.updateStats();
this.isInitialized = true;
this.showLoading(false);
} catch (error) {
console.error('Initialization failed:', error);
this.showError('Initialization failed, please try again');
}
}
async getCurrentTab() {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
this.currentTab = tabs[0];
}
async loadSettings() {
const result = await chrome.storage.sync.get(['autoMode', 'theme']);
this.settings.autoMode = result.autoMode || false;
this.settings.theme = result.theme || 'light';
}
initializeUI() {
// Set theme
document.body.className = `${this.settings.theme}-theme`;
// Set checkbox state
const autoModeCheckbox = document.getElementById('autoMode');
if (autoModeCheckbox) {
autoModeCheckbox.checked = this.settings.autoMode;
}
// Set theme selection
const themeSelect = document.getElementById('themeSelect');
if (themeSelect) {
themeSelect.value = this.settings.theme;
}
// Add animation classes
document.querySelectorAll('.info-card, .action-buttons').forEach(el => {
el.classList.add('fade-in');
});
}
bindEvents() {
// Feature toggle button
const toggleFeatureBtn = document.getElementById('toggleFeature');
if (toggleFeatureBtn) {
toggleFeatureBtn.addEventListener('click', () => this.toggleFeature());
}
// Analyze page button
const analyzePageBtn = document.getElementById('analyzeePage');
if (analyzePageBtn) {
analyzePageBtn.addEventListener('click', () => this.analyzePage());
}
// Auto mode toggle
const autoModeCheckbox = document.getElementById('autoMode');
if (autoModeCheckbox) {
autoModeCheckbox.addEventListener('change', (e) => {
this.updateSetting('autoMode', e.target.checked);
});
}
// Theme selection
const themeSelect = document.getElementById('themeSelect');
if (themeSelect) {
themeSelect.addEventListener('change', (e) => {
this.updateTheme(e.target.value);
});
}
// Settings button
const settingsBtn = document.getElementById('settingsBtn');
if (settingsBtn) {
settingsBtn.addEventListener('click', () => this.openSettings());
}
// Help button
const helpBtn = document.getElementById('helpBtn');
if (helpBtn) {
helpBtn.addEventListener('click', () => this.openHelp());
}
}
async updatePageInfo() {
if (!this.currentTab) return;
const urlInfo = document.getElementById('urlInfo');
const titleInfo = document.getElementById('titleInfo');
if (urlInfo) {
urlInfo.textContent = `URL: ${this.currentTab.url}`;
}
if (titleInfo) {
titleInfo.textContent = `Title: ${this.currentTab.title}`;
}
}
async updateStats() {
try {
// Get statistics from background script
const response = await this.sendMessage({ action: 'getStats' });
if (response.success) {
const clickCount = document.getElementById('clickCount');
const activeTime = document.getElementById('activeTime');
if (clickCount) {
clickCount.textContent = response.data.clickCount || 0;
}
if (activeTime) {
activeTime.textContent = this.formatTime(response.data.activeTime || 0);
}
}
} catch (error) {
console.error('Failed to get statistics:', error);
}
}
async toggleFeature() {
try {
this.showLoading(true);
const response = await this.sendMessage({
action: 'toggleFeature',
tabId: this.currentTab.id
});
if (response.success) {
// Update button state
const btn = document.getElementById('toggleFeature');
const btnText = btn.querySelector('.btn-text');
if (response.enabled) {
btnText.textContent = 'Disable Feature';
btn.classList.add('btn-danger');
btn.classList.remove('btn-primary');
} else {
btnText.textContent = 'Enable Feature';
btn.classList.add('btn-primary');
btn.classList.remove('btn-danger');
}
this.showSuccess('Feature ' + (response.enabled ? 'enabled' : 'disabled'));
} else {
this.showError(response.error || 'Operation failed');
}
} catch (error) {
this.showError('Operation failed: ' + error.message);
} finally {
this.showLoading(false);
}
}
async analyzePage() {
try {
this.showLoading(true);
const response = await this.sendMessage({
action: 'analyzePage',
tabId: this.currentTab.id
});
if (response.success) {
this.displayAnalysisResults(response.data);
this.showSuccess('Page analysis completed');
} else {
this.showError(response.error || 'Analysis failed');
}
} catch (error) {
this.showError('Analysis failed: ' + error.message);
} finally {
this.showLoading(false);
}
}
displayAnalysisResults(data) {
// Create analysis results display
const resultsHtml = `
<div class="analysis-results">
<h4>Analysis Results</h4>
<ul>
<li>Page Elements: ${data.elementCount}</li>
<li>Image Count: ${data.imageCount}</li>
<li>Link Count: ${data.linkCount}</li>
<li>Load Time: ${data.loadTime}ms</li>
</ul>
</div>
`;
const pageInfo = document.getElementById('pageInfo');
if (pageInfo) {
pageInfo.querySelector('.info-content').innerHTML = resultsHtml;
}
}
async updateSetting(key, value) {
this.settings[key] = value;
try {
await chrome.storage.sync.set({ [key]: value });
// Notify background script of settings change
this.sendMessage({
action: 'settingChanged',
key: key,
value: value
});
} catch (error) {
console.error('Failed to save settings:', error);
}
}
async updateTheme(theme) {
this.settings.theme = theme;
document.body.className = `${theme}-theme`;
await this.updateSetting('theme', theme);
}
openSettings() {
chrome.runtime.openOptionsPage();
}
openHelp() {
chrome.tabs.create({
url: 'https://example.com/help'
});
}
// Utility methods
async sendMessage(message) {
return new Promise((resolve) => {
chrome.runtime.sendMessage(message, (response) => {
resolve(response || { success: false, error: 'No response' });
});
});
}
formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h ${minutes}m`;
} else {
return `${minutes}m`;
}
}
showLoading(show) {
const overlay = document.getElementById('loadingOverlay');
if (overlay) {
overlay.style.display = show ? 'flex' : 'none';
}
}
showSuccess(message) {
this.showToast(message, 'success');
}
showError(message) {
this.showToast(message, 'error');
}
showToast(message, type = 'info') {
// Create toast message
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
// Add styles
toast.style.cssText = `
position: fixed;
top: 16px;
right: 16px;
padding: 12px 16px;
border-radius: 4px;
color: white;
font-size: 12px;
z-index: 10000;
animation: slideIn 0.3s ease;
background-color: ${type === 'success' ? '#34a853' : type === 'error' ? '#ea4335' : '#4285f4'};
`;
document.body.appendChild(toast);
// Auto remove after 3 seconds
setTimeout(() => {
toast.style.animation = 'fadeOut 0.3s ease';
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 3000);
}
}
// Initialize after document is loaded
document.addEventListener('DOMContentLoaded', () => {
new PopupManager();
});
// Add keyboard shortcut support
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + K: Quick feature toggle
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
document.getElementById('toggleFeature')?.click();
}
// Escape: Close popup
if (e.key === 'Escape') {
window.close();
}
});
6.4.2 Interaction Logic Demo Example
// popup-demo.js - Chrome Extension Popup Interaction Manager Demo
class PopupManagerDemo {
constructor() {
this.isInitialized = false;
this.currentTab = null;
this.settings = {
autoMode: false,
theme: 'light'
};
this.stats = {
clickCount: 0,
activeTime: 0
};
}
async initialize() {
console.log("Initializing Popup Manager...");
await this.loadCurrentTab();
await this.loadSettings();
await this.updateStats();
this.isInitialized = true;
console.log("Popup Manager initialized");
}
async loadCurrentTab() {
// Simulate getting current tab
this.currentTab = {
id: 1,
url: 'https://www.example.com',
title: 'Example Page',
active: true
};
console.log(`Loaded tab: ${this.currentTab.title}`);
}
async loadSettings() {
// Simulate loading settings from storage
console.log("Loading settings:");
for (const [key, value] of Object.entries(this.settings)) {
console.log(` ${key}: ${value}`);
}
}
async updateStats() {
// Simulate updating statistics
this.stats.clickCount += 1;
this.stats.activeTime += 30; // Add 30 seconds active time
console.log(`Stats updated: clickCount=${this.stats.clickCount}, activeTime=${this.stats.activeTime}s`);
}
async toggleFeature() {
try {
// Simulate feature toggle
const currentState = this.settings.featureEnabled || false;
const newState = !currentState;
this.settings.featureEnabled = newState;
console.log(`Feature state toggled: ${currentState} -> ${newState}`);
return {
success: true,
enabled: newState,
message: `Feature ${newState ? 'enabled' : 'disabled'}`
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async analyzePage() {
try {
console.log("Starting page analysis...");
// Simulate page analysis
await new Promise(resolve => setTimeout(resolve, 1000));
const analysisData = {
elementCount: 245,
imageCount: 12,
linkCount: 38,
loadTime: 1250,
timestamp: new Date().toISOString()
};
console.log("Page analysis completed:");
for (const [key, value] of Object.entries(analysisData)) {
console.log(` ${key}: ${value}`);
}
return {
success: true,
data: analysisData
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
async updateSetting(key, value) {
const oldValue = this.settings[key];
this.settings[key] = value;
console.log(`Setting updated: ${key}: ${oldValue} -> ${value}`);
// Simulate saving to storage
await this.saveSettings();
}
async saveSettings() {
console.log("Saving settings to storage...");
// Simulate storage operation
}
getPageInfo() {
if (!this.currentTab) {
return { url: 'Unknown', title: 'Unknown' };
}
return {
url: this.currentTab.url,
title: this.currentTab.title
};
}
formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h ${minutes}m`;
} else {
return `${minutes}m`;
}
}
}
// Usage example
async function demoPopupInteraction() {
const popup = new PopupManagerDemo();
// Initialize
await popup.initialize();
// Get page info
const pageInfo = popup.getPageInfo();
console.log('\nPage Information:');
console.log(` URL: ${pageInfo.url}`);
console.log(` Title: ${pageInfo.title}`);
// Toggle feature
const result = await popup.toggleFeature();
console.log('\nFeature toggle result:', result);
// Analyze page
const analysisResult = await popup.analyzePage();
console.log('\nAnalysis result:', analysisResult);
// Update settings
await popup.updateSetting('autoMode', true);
await popup.updateSetting('theme', 'dark');
// Display formatted time
const formattedTime = popup.formatTime(popup.stats.activeTime);
console.log(`\nFormatted time: ${formattedTime}`);
}
// Run demo (commented out for actual execution)
// demoPopupInteraction();
6.5 Communication with Background Script
6.5.1 Message Passing Mechanism
// Message handling in background.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Background received message:', message);
switch (message.action) {
case 'getStats':
handleGetStats(sendResponse);
return true; // Async response
case 'toggleFeature':
handleToggleFeature(message.tabId, sendResponse);
return true;
case 'analyzePage':
handleAnalyzePage(message.tabId, sendResponse);
return true;
case 'settingChanged':
handleSettingChanged(message.key, message.value, sendResponse);
return true;
default:
sendResponse({ success: false, error: 'Unknown action' });
}
});
async function handleGetStats(sendResponse) {
try {
const stats = await chrome.storage.local.get(['clickCount', 'activeTime']);
sendResponse({
success: true,
data: {
clickCount: stats.clickCount || 0,
activeTime: stats.activeTime || 0
}
});
} catch (error) {
sendResponse({ success: false, error: error.message });
}
}
async function handleToggleFeature(tabId, sendResponse) {
try {
// Get current feature state
const result = await chrome.storage.sync.get(['featureEnabled']);
const currentState = result.featureEnabled || false;
const newState = !currentState;
// Save new state
await chrome.storage.sync.set({ featureEnabled: newState });
// Send message to content script
chrome.tabs.sendMessage(tabId, {
action: 'updateFeatureState',
enabled: newState
});
sendResponse({
success: true,
enabled: newState
});
// Update click statistics
const stats = await chrome.storage.local.get(['clickCount']);
await chrome.storage.local.set({
clickCount: (stats.clickCount || 0) + 1
});
} catch (error) {
sendResponse({ success: false, error: error.message });
}
}
async function handleAnalyzePage(tabId, sendResponse) {
try {
// Execute page analysis script
const results = await chrome.scripting.executeScript({
target: { tabId: tabId },
func: analyzePageContent
});
if (results && results[0]) {
sendResponse({
success: true,
data: results[0].result
});
} else {
sendResponse({ success: false, error: 'Analysis failed' });
}
} catch (error) {
sendResponse({ success: false, error: error.message });
}
}
// Page analysis function (injected into the page)
function analyzePageContent() {
const elements = document.querySelectorAll('*');
const images = document.querySelectorAll('img');
const links = document.querySelectorAll('a');
const performanceEntries = performance.getEntriesByType('navigation');
const loadTime = performanceEntries.length > 0
? Math.round(performanceEntries[0].loadEventEnd - performanceEntries[0].fetchStart)
: 0;
return {
elementCount: elements.length,
imageCount: images.length,
linkCount: links.length,
loadTime: loadTime
};
}
warning Common Issues
- Message Timeout: Use
return truebefore sendResponse to keep message channel open - Context Loss: All state is lost after Popup closes, important data needs to be stored
- Permission Restrictions: Some APIs require corresponding permission declarations
- Cross-Origin Issues: Access to external resources needs to be declared in manifest
6.6 Responsive Design and Optimization
6.6.1 Adaptive Layout
/* Responsive breakpoints */
@media (max-width: 400px) {
body {
width: 320px;
}
.stats-grid {
grid-template-columns: 1fr;
gap: 8px;
}
.action-buttons {
flex-direction: column;
}
}
@media (max-height: 500px) {
.main-content {
gap: 12px;
}
.info-card {
padding: 12px;
}
}
/* High DPI screen optimization */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
.logo img {
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
}
}
6.6.2 Performance Optimization
// Debounce function
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Optimized settings update
const updateSettingDebounced = debounce(async (key, value) => {
await chrome.storage.sync.set({ [key]: value });
}, 300);
// Virtual scrolling (for large data lists)
class VirtualList {
constructor(container, itemHeight, renderItem) {
this.container = container;
this.itemHeight = itemHeight;
this.renderItem = renderItem;
this.items = [];
this.visibleStart = 0;
this.visibleEnd = 0;
}
setItems(items) {
this.items = items;
this.update();
}
update() {
const containerHeight = this.container.clientHeight;
const visibleCount = Math.ceil(containerHeight / this.itemHeight);
const startIndex = Math.max(0, this.visibleStart);
const endIndex = Math.min(this.items.length, startIndex + visibleCount + 1);
// Clear container
this.container.innerHTML = '';
// Render visible items
for (let i = startIndex; i < endIndex; i++) {
const element = this.renderItem(this.items[i], i);
element.style.position = 'absolute';
element.style.top = `${i * this.itemHeight}px`;
element.style.height = `${this.itemHeight}px`;
this.container.appendChild(element);
}
// Set container total height
this.container.style.height = `${this.items.length * this.itemHeight}px`;
}
}
tip Optimization Recommendations
- Reduce Package Size: Compress CSS/JS files, remove unused code
- Lazy Loading: Delay loading of non-critical content
- Caching Strategy: Properly use localStorage caching
- Animation Optimization: Use CSS transform instead of position changes
- Image Optimization: Use WebP format, set appropriate sizes
note Summary This chapter provided detailed coverage of Chrome extension Popup interface development, including HTML structure design, CSS styling, JavaScript interaction logic implementation, and communication mechanisms with Background Script. Mastering these skills will help you create user-friendly extension interfaces. In the next chapter, we’ll learn how to develop Options pages to provide richer configuration features.