第06章 Popup 弹窗界面开发
12/3/25About 15 min
第6章:Popup 弹窗界面开发
学习目标
- 理解Chrome扩展Popup的作用和特点
- 掌握Popup页面的HTML、CSS、JavaScript开发
- 学会Popup与Background Script的通信
- 实现响应式设计和用户体验优化
6.1 Popup概述
Popup是用户点击扩展图标时显示的小窗口界面,是用户与扩展交互的主要入口。
6.1.1 Popup特点
核心特性
- 小窗口界面,通常宽度320-800px
- 点击扩展图标外区域会自动关闭
- 每次打开都会重新加载
- 可以与Background Script通信
- 支持完整的Web技术栈
6.1.2 Manifest配置
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0",
"action": {
"default_popup": "popup.html",
"default_title": "我的扩展",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"permissions": [
"storage",
"tabs"
]
}6.2 HTML结构设计
6.2.1 基础HTML模板
<!-- popup.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的扩展</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<!-- 头部区域 -->
<header class="header">
<div class="logo">
<img src="icons/icon48.png" alt="Logo">
<h1>我的扩展</h1>
</div>
<div class="status-indicator" id="statusIndicator">
<span class="status-dot"></span>
<span class="status-text">已连接</span>
</div>
</header>
<!-- 主要内容区域 -->
<main class="main-content">
<!-- 功能按钮组 -->
<section class="action-buttons">
<button class="btn btn-primary" id="toggleFeature">
<span class="btn-icon">🔧</span>
<span class="btn-text">启用功能</span>
</button>
<button class="btn btn-secondary" id="analyzeePage">
<span class="btn-icon">📊</span>
<span class="btn-text">分析页面</span>
</button>
</section>
<!-- 信息展示区域 -->
<section class="info-section">
<div class="info-card" id="pageInfo">
<h3>页面信息</h3>
<div class="info-content">
<p class="url-info" id="urlInfo">等待获取...</p>
<p class="title-info" id="titleInfo">等待获取...</p>
</div>
</div>
<div class="info-card" id="statsInfo">
<h3>使用统计</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-value" id="clickCount">0</span>
<span class="stat-label">点击次数</span>
</div>
<div class="stat-item">
<span class="stat-value" id="activeTime">0</span>
<span class="stat-label">活跃时间</span>
</div>
</div>
</div>
</section>
<!-- 设置区域 -->
<section class="settings-section">
<div class="setting-item">
<label class="setting-label">
<input type="checkbox" id="autoMode" class="setting-checkbox">
<span class="checkmark"></span>
自动模式
</label>
</div>
<div class="setting-item">
<label class="setting-label">主题</label>
<select id="themeSelect" class="setting-select">
<option value="light">浅色</option>
<option value="dark">深色</option>
<option value="auto">跟随系统</option>
</select>
</div>
</section>
</main>
<!-- 底部区域 -->
<footer class="footer">
<div class="footer-buttons">
<button class="btn btn-small" id="settingsBtn">设置</button>
<button class="btn btn-small" id="helpBtn">帮助</button>
</div>
</footer>
</div>
<!-- 加载指示器 -->
<div class="loading-overlay" id="loadingOverlay" style="display: none;">
<div class="spinner"></div>
<p>处理中...</p>
</div>
<script src="popup.js"></script>
</body>
</html>6.2.2 Python模拟HTML生成器
# popup_generator.py - 模拟Chrome扩展Popup HTML生成
from typing import Dict, List, Optional
import json
class PopupGenerator:
"""Chrome扩展Popup界面生成器"""
def __init__(self):
self.title = "Chrome扩展"
self.width = 400
self.height = 600
self.theme = "light"
self.components = []
def set_config(self, title: str, width: int = 400, height: int = 600, theme: str = "light"):
"""设置弹窗配置"""
self.title = title
self.width = width
self.height = height
self.theme = theme
def add_header(self, logo_path: str, status_text: str = "已连接"):
"""添加头部区域"""
header = {
"type": "header",
"logo": logo_path,
"title": self.title,
"status": status_text
}
self.components.append(header)
def add_button_group(self, buttons: List[Dict]):
"""添加按钮组"""
button_group = {
"type": "button_group",
"buttons": buttons
}
self.components.append(button_group)
def add_info_card(self, title: str, content_type: str = "text", content_id: str = ""):
"""添加信息卡片"""
info_card = {
"type": "info_card",
"title": title,
"content_type": content_type,
"content_id": content_id
}
self.components.append(info_card)
def add_settings_section(self, settings: List[Dict]):
"""添加设置区域"""
settings_section = {
"type": "settings",
"settings": settings
}
self.components.append(settings_section)
def generate_html(self) -> str:
"""生成完整的HTML代码"""
html_template = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{self.title}</title>
<link rel="stylesheet" href="popup.css">
<style>
body {{ width: {self.width}px; min-height: {self.height}px; }}
</style>
</head>
<body class="{self.theme}-theme">
<div class="container">
{self._generate_components()}
</div>
<script src="popup.js"></script>
</body>
</html>'''
return html_template
def _generate_components(self) -> str:
"""生成组件HTML"""
html_parts = []
for component in self.components:
if component["type"] == "header":
html_parts.append(self._generate_header(component))
elif component["type"] == "button_group":
html_parts.append(self._generate_button_group(component))
elif component["type"] == "info_card":
html_parts.append(self._generate_info_card(component))
elif component["type"] == "settings":
html_parts.append(self._generate_settings(component))
return "\n".join(html_parts)
def _generate_header(self, header: Dict) -> str:
return f'''
<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>'''
def _generate_button_group(self, button_group: Dict) -> str:
buttons_html = []
for button in button_group["buttons"]:
buttons_html.append(f'''
<button class="btn btn-{button.get('style', 'primary')}" id="{button['id']}">
<span class="btn-icon">{button.get('icon', '🔧')}</span>
<span class="btn-text">{button['text']}</span>
</button>''')
return f'''
<section class="action-buttons">
{"".join(buttons_html)}
</section>'''
def _generate_info_card(self, info_card: Dict) -> str:
return f'''
<section class="info-section">
<div class="info-card" id="{info_card['content_id']}">
<h3>{info_card['title']}</h3>
<div class="info-content">
<p class="info-text">等待获取...</p>
</div>
</div>
</section>'''
def _generate_settings(self, settings_section: Dict) -> str:
settings_html = []
for setting in settings_section["settings"]:
if setting["type"] == "checkbox":
settings_html.append(f'''
<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>''')
elif setting["type"] == "select":
options_html = []
for option in setting["options"]:
options_html.append(f'<option value="{option["value"]}">{option["text"]}</option>')
settings_html.append(f'''
<div class="setting-item">
<label class="setting-label">{setting['label']}</label>
<select id="{setting['id']}" class="setting-select">
{"".join(options_html)}
</select>
</div>''')
return f'''
<section class="settings-section">
{"".join(settings_html)}
</section>'''
# 使用示例
generator = PopupGenerator()
generator.set_config("我的Chrome扩展", 400, 600, "light")
# 添加组件
generator.add_header("icons/icon48.png", "已连接")
generator.add_button_group([
{"id": "toggleFeature", "text": "启用功能", "icon": "🔧", "style": "primary"},
{"id": "analyzePage", "text": "分析页面", "icon": "📊", "style": "secondary"}
])
generator.add_info_card("页面信息", "text", "pageInfo")
generator.add_settings_section([
{"type": "checkbox", "id": "autoMode", "label": "自动模式"},
{"type": "select", "id": "themeSelect", "label": "主题",
"options": [
{"value": "light", "text": "浅色"},
{"value": "dark", "text": "深色"},
{"value": "auto", "text": "跟随系统"}
]}
])
# 生成HTML
html_content = generator.generate_html()
print("生成的Popup HTML:")
print(html_content[:500] + "...") # 显示前500字符6.3 CSS样式设计
6.3.1 现代化样式
/* 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 {
--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 {
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 {
flex: 1;
padding: 16px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* 按钮样式 */
.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-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 {
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-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;
}
/* 自定义复选框 */
.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;
}
/* 选择框 */
.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 {
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-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); }
}
/* 响应式设计 */
@media (max-width: 320px) {
body {
width: 300px;
}
.container {
padding: 0 8px;
}
.btn {
padding: 10px 12px;
font-size: 13px;
}
}
/* 动画效果 */
.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); }
}
/* 焦点可访问性 */
button:focus,
select:focus,
input:focus {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
/* 滚动条样式 */
::-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交互逻辑
6.4.1 主要脚本文件
// popup.js
class PopupManager {
constructor() {
this.isInitialized = false;
this.currentTab = null;
this.settings = {
autoMode: false,
theme: 'light'
};
this.init();
}
async init() {
try {
// 显示加载状态
this.showLoading(true);
// 获取当前标签页
await this.getCurrentTab();
// 加载设置
await this.loadSettings();
// 初始化UI
this.initializeUI();
// 绑定事件
this.bindEvents();
// 更新页面信息
await this.updatePageInfo();
// 更新统计信息
await this.updateStats();
this.isInitialized = true;
this.showLoading(false);
} catch (error) {
console.error('初始化失败:', error);
this.showError('初始化失败,请重试');
}
}
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() {
// 设置主题
document.body.className = `${this.settings.theme}-theme`;
// 设置复选框状态
const autoModeCheckbox = document.getElementById('autoMode');
if (autoModeCheckbox) {
autoModeCheckbox.checked = this.settings.autoMode;
}
// 设置主题选择
const themeSelect = document.getElementById('themeSelect');
if (themeSelect) {
themeSelect.value = this.settings.theme;
}
// 添加动画类
document.querySelectorAll('.info-card, .action-buttons').forEach(el => {
el.classList.add('fade-in');
});
}
bindEvents() {
// 功能开关按钮
const toggleFeatureBtn = document.getElementById('toggleFeature');
if (toggleFeatureBtn) {
toggleFeatureBtn.addEventListener('click', () => this.toggleFeature());
}
// 分析页面按钮
const analyzePageBtn = document.getElementById('analyzeePage');
if (analyzePageBtn) {
analyzePageBtn.addEventListener('click', () => this.analyzePage());
}
// 自动模式开关
const autoModeCheckbox = document.getElementById('autoMode');
if (autoModeCheckbox) {
autoModeCheckbox.addEventListener('change', (e) => {
this.updateSetting('autoMode', e.target.checked);
});
}
// 主题选择
const themeSelect = document.getElementById('themeSelect');
if (themeSelect) {
themeSelect.addEventListener('change', (e) => {
this.updateTheme(e.target.value);
});
}
// 设置按钮
const settingsBtn = document.getElementById('settingsBtn');
if (settingsBtn) {
settingsBtn.addEventListener('click', () => this.openSettings());
}
// 帮助按钮
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 = `标题: ${this.currentTab.title}`;
}
}
async updateStats() {
try {
// 从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('获取统计数据失败:', error);
}
}
async toggleFeature() {
try {
this.showLoading(true);
const response = await this.sendMessage({
action: 'toggleFeature',
tabId: this.currentTab.id
});
if (response.success) {
// 更新按钮状态
const btn = document.getElementById('toggleFeature');
const btnText = btn.querySelector('.btn-text');
if (response.enabled) {
btnText.textContent = '禁用功能';
btn.classList.add('btn-danger');
btn.classList.remove('btn-primary');
} else {
btnText.textContent = '启用功能';
btn.classList.add('btn-primary');
btn.classList.remove('btn-danger');
}
this.showSuccess('功能已' + (response.enabled ? '启用' : '禁用'));
} else {
this.showError(response.error || '操作失败');
}
} catch (error) {
this.showError('操作失败: ' + 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('页面分析完成');
} else {
this.showError(response.error || '分析失败');
}
} catch (error) {
this.showError('分析失败: ' + error.message);
} finally {
this.showLoading(false);
}
}
displayAnalysisResults(data) {
// 创建分析结果显示
const resultsHtml = `
<div class="analysis-results">
<h4>分析结果</h4>
<ul>
<li>页面元素: ${data.elementCount} 个</li>
<li>图片数量: ${data.imageCount} 个</li>
<li>链接数量: ${data.linkCount} 个</li>
<li>加载时间: ${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 });
// 通知background script设置变更
this.sendMessage({
action: 'settingChanged',
key: key,
value: value
});
} catch (error) {
console.error('保存设置失败:', 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'
});
}
// 工具方法
async sendMessage(message) {
return new Promise((resolve) => {
chrome.runtime.sendMessage(message, (response) => {
resolve(response || { success: false, error: '无响应' });
});
});
}
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') {
// 创建提示消息
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
// 添加样式
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);
// 3秒后自动移除
setTimeout(() => {
toast.style.animation = 'fadeOut 0.3s ease';
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 3000);
}
}
// 文档加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
new PopupManager();
});
// 添加键盘快捷键支持
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + K: 快速功能切换
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
document.getElementById('toggleFeature')?.click();
}
// Escape: 关闭弹窗
if (e.key === 'Escape') {
window.close();
}
});6.4.2 Python模拟交互逻辑
# popup_manager.py - 模拟Chrome扩展Popup交互管理
import asyncio
import json
from datetime import datetime
from typing import Dict, Any, Optional
class PopupManager:
"""模拟Chrome扩展Popup管理器"""
def __init__(self):
self.is_initialized = False
self.current_tab = None
self.settings = {
'autoMode': False,
'theme': 'light'
}
self.stats = {
'clickCount': 0,
'activeTime': 0
}
async def initialize(self):
"""初始化Popup管理器"""
print("初始化Popup管理器...")
await self.load_current_tab()
await self.load_settings()
await self.update_stats()
self.is_initialized = True
print("Popup管理器初始化完成")
async def load_current_tab(self):
"""加载当前标签页信息"""
# 模拟获取当前标签页
self.current_tab = {
'id': 1,
'url': 'https://www.example.com',
'title': '示例页面',
'active': True
}
print(f"加载标签页: {self.current_tab['title']}")
async def load_settings(self):
"""加载设置"""
# 模拟从存储加载设置
print("加载设置:")
for key, value in self.settings.items():
print(f" {key}: {value}")
async def update_stats(self):
"""更新统计信息"""
# 模拟更新统计
self.stats['clickCount'] += 1
self.stats['activeTime'] += 30 # 增加30秒活跃时间
print(f"统计更新: 点击次数={self.stats['clickCount']}, 活跃时间={self.stats['activeTime']}s")
async def toggle_feature(self) -> Dict[str, Any]:
"""切换功能开关"""
try:
# 模拟功能切换
current_state = self.settings.get('featureEnabled', False)
new_state = not current_state
self.settings['featureEnabled'] = new_state
print(f"功能状态切换: {current_state} -> {new_state}")
return {
'success': True,
'enabled': new_state,
'message': f"功能已{'启用' if new_state else '禁用'}"
}
except Exception as e:
return {
'success': False,
'error': str(e)
}
async def analyze_page(self) -> Dict[str, Any]:
"""分析页面"""
try:
print("开始分析页面...")
# 模拟页面分析
await asyncio.sleep(1) # 模拟分析时间
analysis_data = {
'elementCount': 245,
'imageCount': 12,
'linkCount': 38,
'loadTime': 1250,
'timestamp': datetime.now().isoformat()
}
print("页面分析完成:")
for key, value in analysis_data.items():
print(f" {key}: {value}")
return {
'success': True,
'data': analysis_data
}
except Exception as e:
return {
'success': False,
'error': str(e)
}
async def update_setting(self, key: str, value: Any):
"""更新设置"""
old_value = self.settings.get(key)
self.settings[key] = value
print(f"设置更新: {key}: {old_value} -> {value}")
# 模拟保存到存储
await self.save_settings()
async def save_settings(self):
"""保存设置"""
print("保存设置到存储...")
# 模拟存储操作
def get_page_info(self) -> Dict[str, str]:
"""获取页面信息"""
if not self.current_tab:
return {'url': '未知', 'title': '未知'}
return {
'url': self.current_tab['url'],
'title': self.current_tab['title']
}
def format_time(self, seconds: int) -> str:
"""格式化时间显示"""
hours = seconds // 3600
minutes = (seconds % 3600) // 60
if hours > 0:
return f"{hours}h {minutes}m"
else:
return f"{minutes}m"
# 使用示例
async def demo_popup_interaction():
"""演示Popup交互"""
popup = PopupManager()
# 初始化
await popup.initialize()
# 获取页面信息
page_info = popup.get_page_info()
print(f"\n页面信息:")
print(f" URL: {page_info['url']}")
print(f" 标题: {page_info['title']}")
# 切换功能
result = await popup.toggle_feature()
print(f"\n功能切换结果: {result}")
# 分析页面
analysis_result = await popup.analyze_page()
print(f"\n分析结果: {analysis_result}")
# 更新设置
await popup.update_setting('autoMode', True)
await popup.update_setting('theme', 'dark')
# 显示格式化时间
formatted_time = popup.format_time(popup.stats['activeTime'])
print(f"\n格式化时间: {formatted_time}")
# 运行演示(注释掉实际执行)
# asyncio.run(demo_popup_interaction())6.5 与Background Script通信
6.5.1 消息传递机制
// background.js 中的消息处理
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Background收到消息:', message);
switch (message.action) {
case 'getStats':
handleGetStats(sendResponse);
return true; // 异步响应
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: '未知操作' });
}
});
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 {
// 获取当前功能状态
const result = await chrome.storage.sync.get(['featureEnabled']);
const currentState = result.featureEnabled || false;
const newState = !currentState;
// 保存新状态
await chrome.storage.sync.set({ featureEnabled: newState });
// 向content script发送消息
chrome.tabs.sendMessage(tabId, {
action: 'updateFeatureState',
enabled: newState
});
sendResponse({
success: true,
enabled: newState
});
// 更新点击统计
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 {
// 执行页面分析脚本
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: '分析失败' });
}
} catch (error) {
sendResponse({ success: false, error: error.message });
}
}
// 页面分析函数(注入到页面中执行)
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
};
}常见问题
- 消息超时: 在sendResponse前使用
return true保持消息通道开启 - 上下文丢失: Popup关闭后所有状态都会丢失,需要存储重要数据
- 权限限制: 某些API需要相应的权限声明
- 跨域问题: 访问外部资源需要在manifest中声明
6.6 响应式设计和优化
6.6.1 自适应布局
/* 响应式断点 */
@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;
}
}
/* 高DPI屏幕优化 */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
.logo img {
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
}
}6.6.2 性能优化
// 防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 优化的设置更新
const updateSettingDebounced = debounce(async (key, value) => {
await chrome.storage.sync.set({ [key]: value });
}, 300);
// 虚拟滚动(对大列表数据)
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);
// 清空容器
this.container.innerHTML = '';
// 渲染可见项目
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);
}
// 设置容器总高度
this.container.style.height = `${this.items.length * this.itemHeight}px`;
}
}优化建议
- 减小包体积: 压缩CSS/JS文件,移除未使用的代码
- 懒加载: 延迟加载非关键内容
- 缓存策略: 合理使用localStorage缓存
- 动画优化: 使用CSS transform替代位置变化
- 图片优化: 使用WebP格式,设置合适的尺寸
总结
本章详细介绍了Chrome扩展Popup界面的开发,包括HTML结构设计、CSS样式编写、JavaScript交互逻辑实现,以及与Background Script的通信机制。掌握这些技能将帮助你创建用户友好的扩展界面。下一章我们将学习如何开发Options选项页面,提供更丰富的配置功能。
