第10章 Chrome APIs 深入应用
12/3/25About 20 min
第10章:Chrome APIs 深入应用
学习目标
- 掌握Chrome扩展常用API的高级用法
- 学会标签页、书签、历史记录管理
- 实现网络请求和cookie操作
- 应用通知、菜单和快捷键功能
10.1 Tabs API 标签页管理
10.1.1 基础标签页操作
// tabs-manager.js - 标签页管理器
class TabsManager {
constructor() {
this.activeTabsCache = new Map();
this.tabGroups = new Map();
this.setupEventListeners();
}
setupEventListeners() {
// 监听标签页变化
chrome.tabs.onCreated.addListener((tab) => {
this.onTabCreated(tab);
});
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
this.onTabUpdated(tabId, changeInfo, tab);
});
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
this.onTabRemoved(tabId, removeInfo);
});
chrome.tabs.onActivated.addListener((activeInfo) => {
this.onTabActivated(activeInfo);
});
chrome.tabs.onMoved.addListener((tabId, moveInfo) => {
this.onTabMoved(tabId, moveInfo);
});
}
// 获取当前活动标签页
async getCurrentTab() {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
return tabs[0];
}
// 获取所有标签页
async getAllTabs() {
return await chrome.tabs.query({});
}
// 根据URL模式查找标签页
async findTabsByUrl(urlPattern) {
const allTabs = await this.getAllTabs();
return allTabs.filter(tab => {
if (typeof urlPattern === 'string') {
return tab.url.includes(urlPattern);
} else if (urlPattern instanceof RegExp) {
return urlPattern.test(tab.url);
}
return false;
});
}
// 创建新标签页
async createTab(options = {}) {
const defaultOptions = {
active: true,
pinned: false
};
const tabOptions = { ...defaultOptions, ...options };
try {
const tab = await chrome.tabs.create(tabOptions);
console.log('标签页已创建:', tab.id);
return tab;
} catch (error) {
console.error('创建标签页失败:', error);
throw error;
}
}
// 复制标签页
async duplicateTab(tabId) {
try {
const tab = await chrome.tabs.duplicate(tabId);
console.log('标签页已复制:', tab.id);
return tab;
} catch (error) {
console.error('复制标签页失败:', error);
throw error;
}
}
// 更新标签页
async updateTab(tabId, updateProperties) {
try {
const tab = await chrome.tabs.update(tabId, updateProperties);
console.log('标签页已更新:', tabId);
return tab;
} catch (error) {
console.error('更新标签页失败:', error);
throw error;
}
}
// 关闭标签页
async closeTab(tabId) {
try {
await chrome.tabs.remove(tabId);
console.log('标签页已关闭:', tabId);
return true;
} catch (error) {
console.error('关闭标签页失败:', error);
return false;
}
}
// 批量关闭标签页
async closeTabs(tabIds) {
try {
await chrome.tabs.remove(tabIds);
console.log('批量关闭标签页:', tabIds.length);
return true;
} catch (error) {
console.error('批量关闭失败:', error);
return false;
}
}
// 移动标签页
async moveTab(tabId, moveProperties) {
try {
const tabs = await chrome.tabs.move(tabId, moveProperties);
console.log('标签页已移动:', tabId);
return tabs;
} catch (error) {
console.error('移动标签页失败:', error);
throw error;
}
}
// 固定/取消固定标签页
async togglePinTab(tabId) {
try {
const tab = await chrome.tabs.get(tabId);
const updatedTab = await chrome.tabs.update(tabId, {
pinned: !tab.pinned
});
console.log(`标签页 ${tabId} ${updatedTab.pinned ? '已固定' : '已取消固定'}`);
return updatedTab;
} catch (error) {
console.error('切换固定状态失败:', error);
throw error;
}
}
// 静音/取消静音
async toggleMuteTab(tabId) {
try {
const tab = await chrome.tabs.get(tabId);
const updatedTab = await chrome.tabs.update(tabId, {
muted: !tab.mutedInfo.muted
});
console.log(`标签页 ${tabId} ${updatedTab.mutedInfo.muted ? '已静音' : '已取消静音'}`);
return updatedTab;
} catch (error) {
console.error('切换静音状态失败:', error);
throw error;
}
}
// 高级功能:标签页分组管理
async createTabGroup(tabIds, options = {}) {
try {
const groupId = await chrome.tabs.group({ tabIds });
if (options.title) {
await chrome.tabGroups.update(groupId, {
title: options.title,
color: options.color || 'blue'
});
}
this.tabGroups.set(groupId, {
id: groupId,
title: options.title || `Group ${groupId}`,
color: options.color || 'blue',
tabIds: tabIds,
createdAt: Date.now()
});
console.log('标签页组已创建:', groupId);
return groupId;
} catch (error) {
console.error('创建标签页组失败:', error);
throw error;
}
}
// 事件处理器
onTabCreated(tab) {
console.log('标签页已创建:', tab.id, tab.url);
this.activeTabsCache.set(tab.id, tab);
// 发送创建事件通知
chrome.runtime.sendMessage({
action: 'tabCreated',
tab: tab
});
}
onTabUpdated(tabId, changeInfo, tab) {
console.log('标签页已更新:', tabId, changeInfo);
// 更新缓存
this.activeTabsCache.set(tabId, tab);
// 检查URL变化
if (changeInfo.url) {
this.handleUrlChange(tab, changeInfo.url);
}
// 检查状态变化
if (changeInfo.status === 'complete') {
this.handlePageLoadComplete(tab);
}
// 发送更新事件通知
chrome.runtime.sendMessage({
action: 'tabUpdated',
tabId: tabId,
changeInfo: changeInfo,
tab: tab
});
}
onTabRemoved(tabId, removeInfo) {
console.log('标签页已删除:', tabId);
// 清理缓存
this.activeTabsCache.delete(tabId);
// 发送删除事件通知
chrome.runtime.sendMessage({
action: 'tabRemoved',
tabId: tabId,
removeInfo: removeInfo
});
}
onTabActivated(activeInfo) {
console.log('标签页已激活:', activeInfo.tabId);
// 发送激活事件通知
chrome.runtime.sendMessage({
action: 'tabActivated',
activeInfo: activeInfo
});
}
onTabMoved(tabId, moveInfo) {
console.log('标签页已移动:', tabId, moveInfo);
}
handleUrlChange(tab, newUrl) {
// 处理URL变化的逻辑
console.log(`标签页 ${tab.id} URL变化: ${newUrl}`);
// 可以在这里添加URL过滤、重定向等功能
if (this.shouldBlockUrl(newUrl)) {
this.redirectTab(tab.id, 'chrome://newtab/');
}
}
handlePageLoadComplete(tab) {
// 页面加载完成的处理
console.log(`标签页 ${tab.id} 加载完成: ${tab.title}`);
// 可以在这里执行content script注入等操作
this.injectContentScript(tab.id);
}
shouldBlockUrl(url) {
// URL过滤逻辑
const blockedPatterns = [
/.*\.malicious-site\.com/,
/.*dangerous-content.*/
];
return blockedPatterns.some(pattern => pattern.test(url));
}
async redirectTab(tabId, newUrl) {
try {
await chrome.tabs.update(tabId, { url: newUrl });
console.log(`标签页 ${tabId} 已重定向到: ${newUrl}`);
} catch (error) {
console.error('重定向失败:', error);
}
}
async injectContentScript(tabId) {
try {
await chrome.scripting.executeScript({
target: { tabId: tabId },
files: ['content-script.js']
});
console.log(`Content script已注入到标签页 ${tabId}`);
} catch (error) {
console.error('注入content script失败:', error);
}
}
// 获取标签页统计信息
async getTabStats() {
const allTabs = await this.getAllTabs();
const stats = {
total: allTabs.length,
active: 0,
pinned: 0,
muted: 0,
loading: 0,
byDomain: new Map(),
byWindow: new Map()
};
allTabs.forEach(tab => {
if (tab.active) stats.active++;
if (tab.pinned) stats.pinned++;
if (tab.mutedInfo && tab.mutedInfo.muted) stats.muted++;
if (tab.status === 'loading') stats.loading++;
// 按域名统计
try {
const domain = new URL(tab.url).hostname;
stats.byDomain.set(domain, (stats.byDomain.get(domain) || 0) + 1);
} catch (e) {
// 忽略无效URL
}
// 按窗口统计
const windowId = tab.windowId;
stats.byWindow.set(windowId, (stats.byWindow.get(windowId) || 0) + 1);
});
return {
...stats,
byDomain: Object.fromEntries(stats.byDomain),
byWindow: Object.fromEntries(stats.byWindow)
};
}
// 清理重复标签页
async removeDuplicateTabs() {
const allTabs = await this.getAllTabs();
const urlMap = new Map();
const duplicateTabs = [];
// 找出重复的标签页
allTabs.forEach(tab => {
if (urlMap.has(tab.url)) {
duplicateTabs.push(tab.id);
} else {
urlMap.set(tab.url, tab.id);
}
});
if (duplicateTabs.length > 0) {
await this.closeTabs(duplicateTabs);
console.log(`已关闭 ${duplicateTabs.length} 个重复标签页`);
}
return duplicateTabs.length;
}
}
// 全局标签页管理器实例
const tabsManager = new TabsManager();
// 导出供其他模块使用
window.tabsManager = tabsManager;10.1.2 Python模拟标签页管理
# tabs_simulator.py - 模拟Chrome标签页管理
import time
import uuid
from typing import Dict, List, Optional, Callable, Any
from dataclasses import dataclass, field
from enum import Enum
class TabStatus(Enum):
LOADING = "loading"
COMPLETE = "complete"
UNLOADED = "unloaded"
@dataclass
class Tab:
id: int
url: str
title: str = ""
active: bool = False
pinned: bool = False
muted: bool = False
status: TabStatus = TabStatus.LOADING
window_id: int = 1
index: int = 0
favicon_url: str = ""
width: Optional[int] = None
height: Optional[int] = None
incognito: bool = False
highlighted: bool = False
selected: bool = False # deprecated but kept for compatibility
audible: bool = False
discarded: bool = False
auto_discardable: bool = True
group_id: int = -1
opener_tab_id: Optional[int] = None
pending_url: Optional[str] = None
session_id: Optional[str] = None
class TabsManagerSimulator:
"""模拟Chrome标签页管理器"""
def __init__(self):
self.tabs: Dict[int, Tab] = {}
self.next_tab_id = 1
self.next_group_id = 1
self.tab_groups: Dict[int, Dict[str, Any]] = {}
self.event_listeners: Dict[str, List[Callable]] = {
'created': [],
'updated': [],
'removed': [],
'activated': [],
'moved': []
}
self.active_tab_id: Optional[int] = None
def add_listener(self, event_type: str, callback: Callable):
"""添加事件监听器"""
if event_type in self.event_listeners:
self.event_listeners[event_type].append(callback)
def emit_event(self, event_type: str, *args):
"""触发事件"""
for callback in self.event_listeners.get(event_type, []):
try:
callback(*args)
except Exception as e:
print(f"事件处理器出错: {e}")
def create_tab(self, url: str, **kwargs) -> Tab:
"""创建新标签页"""
tab_id = self.next_tab_id
self.next_tab_id += 1
# 设置默认值
defaults = {
'title': f'New Tab {tab_id}',
'active': True,
'index': len(self.tabs)
}
defaults.update(kwargs)
tab = Tab(
id=tab_id,
url=url,
**defaults
)
# 如果是活动标签页,取消其他标签页的活动状态
if tab.active:
self.deactivate_all_tabs()
self.active_tab_id = tab_id
self.tabs[tab_id] = tab
print(f"标签页已创建: {tab_id} - {url}")
self.emit_event('created', tab)
# 模拟页面加载
self.simulate_page_load(tab_id)
return tab
def get_tab(self, tab_id: int) -> Optional[Tab]:
"""获取指定标签页"""
return self.tabs.get(tab_id)
def get_all_tabs(self) -> List[Tab]:
"""获取所有标签页"""
return list(self.tabs.values())
def get_active_tab(self) -> Optional[Tab]:
"""获取当前活动标签页"""
if self.active_tab_id:
return self.tabs.get(self.active_tab_id)
return None
def update_tab(self, tab_id: int, **updates) -> bool:
"""更新标签页属性"""
tab = self.tabs.get(tab_id)
if not tab:
return False
old_values = {}
change_info = {}
for key, value in updates.items():
if hasattr(tab, key):
old_value = getattr(tab, key)
old_values[key] = old_value
if old_value != value:
setattr(tab, key, value)
change_info[key] = value
# 处理特殊属性
if key == 'active' and value:
self.deactivate_all_tabs()
self.active_tab_id = tab_id
tab.active = True
if change_info:
print(f"标签页已更新: {tab_id} - {change_info}")
self.emit_event('updated', tab_id, change_info, tab)
return True
def remove_tab(self, tab_id: int) -> bool:
"""删除标签页"""
tab = self.tabs.get(tab_id)
if not tab:
return False
# 如果是活动标签页,激活其他标签页
if tab.active and len(self.tabs) > 1:
other_tabs = [t for t in self.tabs.values() if t.id != tab_id]
if other_tabs:
self.activate_tab(other_tabs[0].id)
del self.tabs[tab_id]
if self.active_tab_id == tab_id:
self.active_tab_id = None
remove_info = {
'windowId': tab.window_id,
'isWindowClosing': len(self.tabs) == 0
}
print(f"标签页已删除: {tab_id}")
self.emit_event('removed', tab_id, remove_info)
return True
def activate_tab(self, tab_id: int) -> bool:
"""激活指定标签页"""
tab = self.tabs.get(tab_id)
if not tab:
return False
self.deactivate_all_tabs()
tab.active = True
self.active_tab_id = tab_id
active_info = {
'tabId': tab_id,
'windowId': tab.window_id,
'previousTabId': self.active_tab_id
}
print(f"标签页已激活: {tab_id}")
self.emit_event('activated', active_info)
return True
def deactivate_all_tabs(self):
"""取消所有标签页的活动状态"""
for tab in self.tabs.values():
tab.active = False
def move_tab(self, tab_id: int, new_index: int) -> bool:
"""移动标签页到新位置"""
tab = self.tabs.get(tab_id)
if not tab:
return False
old_index = tab.index
tab.index = new_index
move_info = {
'windowId': tab.window_id,
'fromIndex': old_index,
'toIndex': new_index
}
print(f"标签页已移动: {tab_id} 从 {old_index} 到 {new_index}")
self.emit_event('moved', tab_id, move_info)
return True
def duplicate_tab(self, tab_id: int) -> Optional[Tab]:
"""复制标签页"""
original_tab = self.tabs.get(tab_id)
if not original_tab:
return None
new_tab = self.create_tab(
url=original_tab.url,
title=original_tab.title,
pinned=original_tab.pinned,
active=False,
index=original_tab.index + 1
)
print(f"标签页已复制: {tab_id} -> {new_tab.id}")
return new_tab
def pin_tab(self, tab_id: int, pinned: bool = True) -> bool:
"""固定/取消固定标签页"""
return self.update_tab(tab_id, pinned=pinned)
def mute_tab(self, tab_id: int, muted: bool = True) -> bool:
"""静音/取消静音标签页"""
return self.update_tab(tab_id, muted=muted)
def create_tab_group(self, tab_ids: List[int], title: str = "", color: str = "blue") -> int:
"""创建标签页组"""
group_id = self.next_group_id
self.next_group_id += 1
# 更新标签页的组ID
for tab_id in tab_ids:
tab = self.tabs.get(tab_id)
if tab:
tab.group_id = group_id
self.tab_groups[group_id] = {
'id': group_id,
'title': title,
'color': color,
'tabIds': tab_ids,
'collapsed': False
}
print(f"标签页组已创建: {group_id} - {title}")
return group_id
def simulate_page_load(self, tab_id: int):
"""模拟页面加载过程"""
def complete_loading():
tab = self.tabs.get(tab_id)
if tab:
tab.status = TabStatus.COMPLETE
tab.title = f"Loaded: {tab.url}"
change_info = {'status': 'complete', 'title': tab.title}
self.emit_event('updated', tab_id, change_info, tab)
# 模拟1-3秒的加载时间
import threading
timer = threading.Timer(2.0, complete_loading)
timer.start()
def find_tabs_by_url(self, url_pattern: str) -> List[Tab]:
"""根据URL模式查找标签页"""
matching_tabs = []
for tab in self.tabs.values():
if url_pattern in tab.url:
matching_tabs.append(tab)
return matching_tabs
def close_duplicate_tabs(self) -> int:
"""关闭重复的标签页"""
url_map = {}
duplicates = []
for tab in self.tabs.values():
if tab.url in url_map:
duplicates.append(tab.id)
else:
url_map[tab.url] = tab.id
for tab_id in duplicates:
self.remove_tab(tab_id)
print(f"已关闭 {len(duplicates)} 个重复标签页")
return len(duplicates)
def get_tab_stats(self) -> Dict[str, Any]:
"""获取标签页统计信息"""
stats = {
'total': len(self.tabs),
'active': 0,
'pinned': 0,
'muted': 0,
'loading': 0,
'audible': 0,
'by_domain': {},
'by_status': {}
}
for tab in self.tabs.values():
if tab.active:
stats['active'] += 1
if tab.pinned:
stats['pinned'] += 1
if tab.muted:
stats['muted'] += 1
if tab.status == TabStatus.LOADING:
stats['loading'] += 1
if tab.audible:
stats['audible'] += 1
# 按域名统计
try:
from urllib.parse import urlparse
domain = urlparse(tab.url).netloc
stats['by_domain'][domain] = stats['by_domain'].get(domain, 0) + 1
except:
pass
# 按状态统计
status = tab.status.value
stats['by_status'][status] = stats['by_status'].get(status, 0) + 1
return stats
def export_tabs(self) -> List[Dict[str, Any]]:
"""导出所有标签页信息"""
return [
{
'id': tab.id,
'url': tab.url,
'title': tab.title,
'active': tab.active,
'pinned': tab.pinned,
'index': tab.index,
'group_id': tab.group_id if tab.group_id != -1 else None
}
for tab in self.tabs.values()
]
# 使用示例
def demo_tabs_management():
"""演示标签页管理功能"""
print("=== Chrome标签页管理演示 ===\n")
manager = TabsManagerSimulator()
# 添加事件监听器
def on_tab_created(tab):
print(f"[事件] 标签页创建: {tab.id} - {tab.url}")
def on_tab_updated(tab_id, change_info, tab):
print(f"[事件] 标签页更新: {tab_id} - {change_info}")
def on_tab_removed(tab_id, remove_info):
print(f"[事件] 标签页删除: {tab_id}")
manager.add_listener('created', on_tab_created)
manager.add_listener('updated', on_tab_updated)
manager.add_listener('removed', on_tab_removed)
# 1. 创建标签页
print("1. 创建标签页:")
tab1 = manager.create_tab("https://www.google.com", title="Google")
tab2 = manager.create_tab("https://www.github.com", title="GitHub", active=False)
tab3 = manager.create_tab("https://www.stackoverflow.com", title="Stack Overflow", active=False)
time.sleep(0.5)
# 2. 查看标签页状态
print("\n2. 标签页状态:")
stats = manager.get_tab_stats()
print(f" 总数: {stats['total']}")
print(f" 活动: {stats['active']}")
print(f" 加载中: {stats['loading']}")
# 3. 标签页操作
print("\n3. 标签页操作:")
manager.pin_tab(tab1.id, True)
manager.mute_tab(tab2.id, True)
manager.activate_tab(tab2.id)
# 4. 创建重复标签页并清理
print("\n4. 重复标签页处理:")
manager.create_tab("https://www.google.com", title="Google (duplicate)", active=False)
duplicates_removed = manager.close_duplicate_tabs()
# 5. 标签页分组
print("\n5. 标签页分组:")
remaining_tabs = manager.get_all_tabs()
tab_ids = [tab.id for tab in remaining_tabs[:2]]
group_id = manager.create_tab_group(tab_ids, "开发工具", "blue")
# 6. 最终统计
print("\n6. 最终状态:")
final_stats = manager.get_tab_stats()
exported_tabs = manager.export_tabs()
print(f" 最终标签页数: {final_stats['total']}")
print(f" 导出数据: {len(exported_tabs)} 项")
print("\n=== 演示完成 ===")
# 运行演示(注释掉实际执行)
# demo_tabs_management()10.2 Bookmarks API 书签管理
10.2.1 书签操作实现
// bookmarks-manager.js - 书签管理器
class BookmarksManager {
constructor() {
this.bookmarksCache = new Map();
this.bookmarkFolders = new Map();
this.setupEventListeners();
}
setupEventListeners() {
chrome.bookmarks.onCreated.addListener((id, bookmark) => {
this.onBookmarkCreated(id, bookmark);
});
chrome.bookmarks.onRemoved.addListener((id, removeInfo) => {
this.onBookmarkRemoved(id, removeInfo);
});
chrome.bookmarks.onChanged.addListener((id, changeInfo) => {
this.onBookmarkChanged(id, changeInfo);
});
chrome.bookmarks.onMoved.addListener((id, moveInfo) => {
this.onBookmarkMoved(id, moveInfo);
});
chrome.bookmarks.onChildrenReordered.addListener((id, reorderInfo) => {
this.onChildrenReordered(id, reorderInfo);
});
}
// 获取书签树
async getBookmarkTree() {
try {
const tree = await chrome.bookmarks.getTree();
return tree[0]; // 返回根节点
} catch (error) {
console.error('获取书签树失败:', error);
throw error;
}
}
// 搜索书签
async searchBookmarks(query) {
try {
const results = await chrome.bookmarks.search(query);
return results.filter(bookmark => bookmark.url); // 只返回实际的书签
} catch (error) {
console.error('搜索书签失败:', error);
throw error;
}
}
// 创建书签
async createBookmark(bookmarkInfo) {
const { title, url, parentId } = bookmarkInfo;
try {
// 检查是否已存在相同URL的书签
const existing = await this.searchBookmarks(url);
if (existing.length > 0) {
console.warn('书签已存在:', url);
return existing[0];
}
const bookmark = await chrome.bookmarks.create({
title: title || '未命名书签',
url: url,
parentId: parentId
});
console.log('书签已创建:', bookmark.id);
return bookmark;
} catch (error) {
console.error('创建书签失败:', error);
throw error;
}
}
// 创建书签文件夹
async createBookmarkFolder(name, parentId) {
try {
const folder = await chrome.bookmarks.create({
title: name,
parentId: parentId
});
console.log('书签文件夹已创建:', folder.id);
return folder;
} catch (error) {
console.error('创建书签文件夹失败:', error);
throw error;
}
}
// 更新书签
async updateBookmark(id, changes) {
try {
const updatedBookmark = await chrome.bookmarks.update(id, changes);
console.log('书签已更新:', id);
return updatedBookmark;
} catch (error) {
console.error('更新书签失败:', error);
throw error;
}
}
// 删除书签
async deleteBookmark(id) {
try {
await chrome.bookmarks.remove(id);
console.log('书签已删除:', id);
return true;
} catch (error) {
console.error('删除书签失败:', error);
return false;
}
}
// 移动书签
async moveBookmark(id, destination) {
try {
const movedBookmark = await chrome.bookmarks.move(id, destination);
console.log('书签已移动:', id);
return movedBookmark;
} catch (error) {
console.error('移动书签失败:', error);
throw error;
}
}
// 获取书签的完整路径
async getBookmarkPath(bookmarkId) {
try {
const path = [];
let currentId = bookmarkId;
while (currentId) {
const bookmarks = await chrome.bookmarks.get(currentId);
const bookmark = bookmarks[0];
if (!bookmark) break;
path.unshift(bookmark);
currentId = bookmark.parentId;
// 避免无限循环
if (path.length > 10) break;
}
return path;
} catch (error) {
console.error('获取书签路径失败:', error);
return [];
}
}
// 获取文件夹中的所有书签
async getBookmarksInFolder(folderId, includeSubfolders = false) {
try {
const children = await chrome.bookmarks.getChildren(folderId);
const bookmarks = [];
for (const child of children) {
if (child.url) {
// 这是一个书签
bookmarks.push(child);
} else if (includeSubfolders) {
// 这是一个文件夹,递归获取
const subBookmarks = await this.getBookmarksInFolder(child.id, true);
bookmarks.push(...subBookmarks);
}
}
return bookmarks;
} catch (error) {
console.error('获取文件夹书签失败:', error);
return [];
}
}
// 查找重复书签
async findDuplicateBookmarks() {
try {
const allBookmarks = await this.searchBookmarks('');
const urlMap = new Map();
const duplicates = [];
allBookmarks.forEach(bookmark => {
if (bookmark.url) {
if (urlMap.has(bookmark.url)) {
duplicates.push({
original: urlMap.get(bookmark.url),
duplicate: bookmark
});
} else {
urlMap.set(bookmark.url, bookmark);
}
}
});
return duplicates;
} catch (error) {
console.error('查找重复书签失败:', error);
return [];
}
}
// 清理重复书签
async cleanDuplicateBookmarks() {
const duplicates = await this.findDuplicateBookmarks();
const removedCount = duplicates.length;
for (const { duplicate } of duplicates) {
await this.deleteBookmark(duplicate.id);
}
console.log(`已清理 ${removedCount} 个重复书签`);
return removedCount;
}
// 导出书签
async exportBookmarks() {
try {
const tree = await this.getBookmarkTree();
const exportData = {
version: '1.0',
exportDate: new Date().toISOString(),
bookmarks: this.flattenBookmarkTree(tree)
};
return exportData;
} catch (error) {
console.error('导出书签失败:', error);
throw error;
}
}
// 扁平化书签树
flattenBookmarkTree(node, path = []) {
const bookmarks = [];
if (node.children) {
for (const child of node.children) {
if (child.url) {
bookmarks.push({
title: child.title,
url: child.url,
path: [...path, node.title].filter(p => p),
id: child.id,
dateAdded: child.dateAdded,
dateGroupModified: child.dateGroupModified
});
} else {
bookmarks.push(...this.flattenBookmarkTree(child, [...path, node.title]));
}
}
}
return bookmarks;
}
// 按域名分组书签
async groupBookmarksByDomain() {
try {
const allBookmarks = await this.searchBookmarks('');
const domainGroups = new Map();
allBookmarks.forEach(bookmark => {
try {
const url = new URL(bookmark.url);
const domain = url.hostname;
if (!domainGroups.has(domain)) {
domainGroups.set(domain, []);
}
domainGroups.get(domain).push(bookmark);
} catch (error) {
// 忽略无效URL
}
});
return Object.fromEntries(domainGroups);
} catch (error) {
console.error('按域名分组失败:', error);
return {};
}
}
// 书签统计信息
async getBookmarkStats() {
try {
const tree = await this.getBookmarkTree();
const stats = {
totalBookmarks: 0,
totalFolders: 0,
byDomain: new Map(),
orphanedBookmarks: 0,
recentBookmarks: 0
};
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
const countBookmarks = (node) => {
if (node.url) {
stats.totalBookmarks++;
// 统计最近添加的书签
if (node.dateAdded && node.dateAdded > oneWeekAgo) {
stats.recentBookmarks++;
}
// 按域名统计
try {
const domain = new URL(node.url).hostname;
stats.byDomain.set(domain, (stats.byDomain.get(domain) || 0) + 1);
} catch (error) {
stats.orphanedBookmarks++;
}
} else {
stats.totalFolders++;
}
if (node.children) {
node.children.forEach(countBookmarks);
}
};
countBookmarks(tree);
return {
...stats,
byDomain: Object.fromEntries(stats.byDomain)
};
} catch (error) {
console.error('获取书签统计失败:', error);
return null;
}
}
// 事件处理器
onBookmarkCreated(id, bookmark) {
console.log('书签已创建:', id, bookmark.title);
this.bookmarksCache.set(id, bookmark);
// 发送创建事件通知
chrome.runtime.sendMessage({
action: 'bookmarkCreated',
bookmark: bookmark
});
}
onBookmarkRemoved(id, removeInfo) {
console.log('书签已删除:', id);
this.bookmarksCache.delete(id);
// 发送删除事件通知
chrome.runtime.sendMessage({
action: 'bookmarkRemoved',
bookmarkId: id,
removeInfo: removeInfo
});
}
onBookmarkChanged(id, changeInfo) {
console.log('书签已更改:', id, changeInfo);
// 更新缓存
const cached = this.bookmarksCache.get(id);
if (cached) {
Object.assign(cached, changeInfo);
}
// 发送更改事件通知
chrome.runtime.sendMessage({
action: 'bookmarkChanged',
bookmarkId: id,
changeInfo: changeInfo
});
}
onBookmarkMoved(id, moveInfo) {
console.log('书签已移动:', id, moveInfo);
// 发送移动事件通知
chrome.runtime.sendMessage({
action: 'bookmarkMoved',
bookmarkId: id,
moveInfo: moveInfo
});
}
onChildrenReordered(id, reorderInfo) {
console.log('书签子项已重排:', id);
// 发送重排事件通知
chrome.runtime.sendMessage({
action: 'bookmarkChildrenReordered',
parentId: id,
reorderInfo: reorderInfo
});
}
}
// 全局书签管理器实例
const bookmarksManager = new BookmarksManager();
// 导出供其他模块使用
window.bookmarksManager = bookmarksManager;10.3 History API 历史记录
10.3.1 历史记录管理
// history-manager.js - 历史记录管理器
class HistoryManager {
constructor() {
this.historyCache = new Map();
this.setupEventListeners();
}
setupEventListeners() {
chrome.history.onVisited.addListener((historyItem) => {
this.onHistoryVisited(historyItem);
});
chrome.history.onVisitRemoved.addListener((removed) => {
this.onHistoryRemoved(removed);
});
}
// 搜索历史记录
async searchHistory(query, options = {}) {
const defaultOptions = {
text: query,
startTime: 0,
endTime: Date.now(),
maxResults: 100
};
const searchOptions = { ...defaultOptions, ...options };
try {
const results = await chrome.history.search(searchOptions);
console.log(`历史记录搜索完成: ${results.length} 条结果`);
return results;
} catch (error) {
console.error('搜索历史记录失败:', error);
throw error;
}
}
// 获取访问记录
async getVisits(url) {
try {
const visits = await chrome.history.getVisits({ url });
return visits;
} catch (error) {
console.error('获取访问记录失败:', error);
throw error;
}
}
// 添加历史记录
async addHistoryItem(url, title) {
try {
await chrome.history.addUrl({
url: url,
title: title || ''
});
console.log('历史记录已添加:', url);
return true;
} catch (error) {
console.error('添加历史记录失败:', error);
return false;
}
}
// 删除历史记录
async deleteHistoryItem(url) {
try {
await chrome.history.deleteUrl({ url });
console.log('历史记录已删除:', url);
return true;
} catch (error) {
console.error('删除历史记录失败:', error);
return false;
}
}
// 删除时间范围内的历史记录
async deleteHistoryRange(startTime, endTime) {
try {
await chrome.history.deleteRange({
startTime,
endTime
});
console.log(`历史记录已删除: ${new Date(startTime)} - ${new Date(endTime)}`);
return true;
} catch (error) {
console.error('删除历史记录范围失败:', error);
return false;
}
}
// 清除所有历史记录
async clearAllHistory() {
try {
await chrome.history.deleteAll();
console.log('所有历史记录已清除');
return true;
} catch (error) {
console.error('清除历史记录失败:', error);
return false;
}
}
// 获取最常访问的网站
async getTopSites(limit = 10) {
try {
const oneMonthAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
const allHistory = await this.searchHistory('', {
startTime: oneMonthAgo,
maxResults: 1000
});
// 按域名统计访问次数
const domainCounts = new Map();
allHistory.forEach(item => {
try {
const url = new URL(item.url);
const domain = url.hostname;
const count = domainCounts.get(domain) || 0;
domainCounts.set(domain, count + item.visitCount);
} catch (error) {
// 忽略无效URL
}
});
// 排序并返回前N个
const sortedDomains = Array.from(domainCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, limit)
.map(([domain, count]) => ({ domain, count }));
return sortedDomains;
} catch (error) {
console.error('获取热门网站失败:', error);
return [];
}
}
// 获取历史记录统计
async getHistoryStats(days = 30) {
try {
const startTime = Date.now() - days * 24 * 60 * 60 * 1000;
const history = await this.searchHistory('', {
startTime,
maxResults: 10000
});
const stats = {
totalVisits: 0,
uniqueUrls: new Set(),
uniqueDomains: new Set(),
visitsByDay: new Map(),
visitsByHour: new Map(),
topDomains: new Map()
};
history.forEach(item => {
stats.totalVisits += item.visitCount;
stats.uniqueUrls.add(item.url);
try {
const url = new URL(item.url);
const domain = url.hostname;
stats.uniqueDomains.add(domain);
// 统计域名访问次数
const domainCount = stats.topDomains.get(domain) || 0;
stats.topDomains.set(domain, domainCount + item.visitCount);
// 按天统计
if (item.lastVisitTime) {
const date = new Date(item.lastVisitTime);
const day = date.toISOString().split('T')[0];
const dayCount = stats.visitsByDay.get(day) || 0;
stats.visitsByDay.set(day, dayCount + 1);
// 按小时统计
const hour = date.getHours();
const hourCount = stats.visitsByHour.get(hour) || 0;
stats.visitsByHour.set(hour, hourCount + 1);
}
} catch (error) {
// 忽略无效URL
}
});
return {
totalVisits: stats.totalVisits,
uniqueUrls: stats.uniqueUrls.size,
uniqueDomains: stats.uniqueDomains.size,
avgVisitsPerDay: stats.totalVisits / days,
visitsByDay: Object.fromEntries(stats.visitsByDay),
visitsByHour: Object.fromEntries(stats.visitsByHour),
topDomains: Object.fromEntries(
Array.from(stats.topDomains.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
)
};
} catch (error) {
console.error('获取历史统计失败:', error);
return null;
}
}
// 查找相似访问模式
async findSimilarVisitPatterns(targetUrl) {
try {
const targetVisits = await this.getVisits(targetUrl);
const targetTimes = targetVisits.map(v => new Date(v.visitTime).getHours());
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
const recentHistory = await this.searchHistory('', {
startTime: oneWeekAgo,
maxResults: 500
});
const similarUrls = [];
for (const item of recentHistory) {
if (item.url === targetUrl) continue;
const visits = await this.getVisits(item.url);
const visitTimes = visits.map(v => new Date(v.visitTime).getHours());
// 计算时间模式相似度
const similarity = this.calculateTimeSimilarity(targetTimes, visitTimes);
if (similarity > 0.5) {
similarUrls.push({
url: item.url,
title: item.title,
similarity: similarity,
visitCount: item.visitCount
});
}
}
return similarUrls.sort((a, b) => b.similarity - a.similarity);
} catch (error) {
console.error('查找相似访问模式失败:', error);
return [];
}
}
calculateTimeSimilarity(times1, times2) {
if (times1.length === 0 || times2.length === 0) return 0;
const set1 = new Set(times1);
const set2 = new Set(times2);
const intersection = new Set([...set1].filter(x => set2.has(x)));
const union = new Set([...set1, ...set2]);
return intersection.size / union.size;
}
// 导出历史记录
async exportHistory(days = 30) {
try {
const startTime = Date.now() - days * 24 * 60 * 60 * 1000;
const history = await this.searchHistory('', {
startTime,
maxResults: 10000
});
const exportData = {
exportDate: new Date().toISOString(),
dateRange: {
start: new Date(startTime).toISOString(),
end: new Date().toISOString()
},
totalItems: history.length,
history: history.map(item => ({
url: item.url,
title: item.title,
visitCount: item.visitCount,
lastVisitTime: new Date(item.lastVisitTime).toISOString()
}))
};
return exportData;
} catch (error) {
console.error('导出历史记录失败:', error);
throw error;
}
}
// 事件处理器
onHistoryVisited(historyItem) {
console.log('页面已访问:', historyItem.url);
// 更新缓存
this.historyCache.set(historyItem.id, historyItem);
// 发送访问事件通知
chrome.runtime.sendMessage({
action: 'historyVisited',
historyItem: historyItem
});
}
onHistoryRemoved(removed) {
console.log('历史记录已删除:', removed);
// 清理缓存
if (removed.urls) {
removed.urls.forEach(url => {
// 从缓存中移除对应的记录
for (const [id, item] of this.historyCache.entries()) {
if (item.url === url) {
this.historyCache.delete(id);
}
}
});
} else {
// 全部清除
this.historyCache.clear();
}
// 发送删除事件通知
chrome.runtime.sendMessage({
action: 'historyRemoved',
removed: removed
});
}
}
// 全局历史记录管理器实例
const historyManager = new HistoryManager();
// 导出供其他模块使用
window.historyManager = historyManager;10.4 Notifications API 通知
10.4.1 通知系统实现
// notifications-manager.js - 通知管理器
class NotificationsManager {
constructor() {
this.activeNotifications = new Map();
this.notificationQueue = [];
this.isProcessingQueue = false;
this.setupEventListeners();
}
setupEventListeners() {
chrome.notifications.onClicked.addListener((notificationId) => {
this.onNotificationClicked(notificationId);
});
chrome.notifications.onButtonClicked.addListener((notificationId, buttonIndex) => {
this.onButtonClicked(notificationId, buttonIndex);
});
chrome.notifications.onClosed.addListener((notificationId, byUser) => {
this.onNotificationClosed(notificationId, byUser);
});
chrome.notifications.onPermissionLevelChanged.addListener((level) => {
this.onPermissionLevelChanged(level);
});
}
// 创建基础通知
async createNotification(options) {
const notificationId = this.generateNotificationId();
const notificationOptions = {
type: 'basic',
iconUrl: chrome.runtime.getURL('icons/icon48.png'),
title: '通知',
message: '这是一条通知消息',
...options
};
try {
await chrome.notifications.create(notificationId, notificationOptions);
// 保存通知信息
this.activeNotifications.set(notificationId, {
id: notificationId,
options: notificationOptions,
createdAt: Date.now(),
actions: options.actions || []
});
console.log('通知已创建:', notificationId);
return notificationId;
} catch (error) {
console.error('创建通知失败:', error);
throw error;
}
}
// 创建富文本通知
async createRichNotification(options) {
const { title, message, items, progress, imageUrl, buttons } = options;
const notificationOptions = {
type: items ? 'list' : (progress !== undefined ? 'progress' : 'image'),
iconUrl: chrome.runtime.getURL('icons/icon48.png'),
title: title || '富文本通知',
message: message || '',
...options
};
// 添加列表项
if (items) {
notificationOptions.items = items.map(item => ({
title: item.title || '',
message: item.message || ''
}));
}
// 添加进度条
if (progress !== undefined) {
notificationOptions.progress = Math.max(0, Math.min(100, progress));
}
// 添加图片
if (imageUrl) {
notificationOptions.imageUrl = imageUrl;
}
// 添加按钮
if (buttons) {
notificationOptions.buttons = buttons.map(button => ({
title: button.title,
iconUrl: button.iconUrl || ''
}));
}
return await this.createNotification(notificationOptions);
}
// 更新通知
async updateNotification(notificationId, options) {
try {
const notification = this.activeNotifications.get(notificationId);
if (!notification) {
throw new Error('通知不存在');
}
await chrome.notifications.update(notificationId, options);
// 更新缓存
notification.options = { ...notification.options, ...options };
notification.updatedAt = Date.now();
console.log('通知已更新:', notificationId);
return true;
} catch (error) {
console.error('更新通知失败:', error);
return false;
}
}
// 清除通知
async clearNotification(notificationId) {
try {
await chrome.notifications.clear(notificationId);
this.activeNotifications.delete(notificationId);
console.log('通知已清除:', notificationId);
return true;
} catch (error) {
console.error('清除通知失败:', error);
return false;
}
}
// 清除所有通知
async clearAllNotifications() {
const notificationIds = Array.from(this.activeNotifications.keys());
for (const id of notificationIds) {
await this.clearNotification(id);
}
console.log('所有通知已清除');
return notificationIds.length;
}
// 检查权限级别
async getPermissionLevel() {
return new Promise((resolve) => {
chrome.notifications.getPermissionLevel((level) => {
resolve(level);
});
});
}
// 队列化通知系统
async queueNotification(options, priority = 'normal') {
const notificationItem = {
id: this.generateNotificationId(),
options: options,
priority: priority,
timestamp: Date.now()
};
this.notificationQueue.push(notificationItem);
this.notificationQueue.sort((a, b) => {
const priorityOrder = { 'high': 0, 'normal': 1, 'low': 2 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
});
if (!this.isProcessingQueue) {
this.processNotificationQueue();
}
return notificationItem.id;
}
async processNotificationQueue() {
this.isProcessingQueue = true;
while (this.notificationQueue.length > 0) {
// 检查当前活动通知数量
if (this.activeNotifications.size >= 5) {
// 等待一些通知被清除
await this.delay(2000);
continue;
}
const notificationItem = this.notificationQueue.shift();
try {
await this.createNotification(notificationItem.options);
await this.delay(500); // 防止通知过于频繁
} catch (error) {
console.error('队列通知处理失败:', error);
}
}
this.isProcessingQueue = false;
}
// 智能通知功能
async createSmartNotification(type, data) {
let notificationOptions;
switch (type) {
case 'download_complete':
notificationOptions = {
title: '下载完成',
message: `文件 "${data.filename}" 下载完成`,
buttons: [
{ title: '打开文件' },
{ title: '打开文件夹' }
],
actions: ['openFile', 'openFolder'],
data: data
};
break;
case 'new_email':
notificationOptions = {
type: 'list',
title: `新邮件 (${data.count})`,
message: data.count > 1 ? `您有 ${data.count} 封新邮件` : '您有一封新邮件',
items: data.emails.map(email => ({
title: email.sender,
message: email.subject
})),
buttons: [
{ title: '查看邮件' }
],
actions: ['openEmail'],
data: data
};
break;
case 'reminder':
notificationOptions = {
title: '提醒',
message: data.message,
buttons: [
{ title: '完成' },
{ title: '稍后提醒' }
],
actions: ['markDone', 'snooze'],
data: data
};
break;
case 'progress':
notificationOptions = {
type: 'progress',
title: data.title || '进度通知',
message: data.message,
progress: data.progress,
data: data
};
break;
default:
notificationOptions = {
title: '通知',
message: data.message || '默认通知消息',
data: data
};
}
return await this.createNotification(notificationOptions);
}
// 批量通知
async createBatchNotifications(notifications, options = {}) {
const { delay = 1000, maxConcurrent = 3 } = options;
const results = [];
for (let i = 0; i < notifications.length; i += maxConcurrent) {
const batch = notifications.slice(i, i + maxConcurrent);
const batchPromises = batch.map(notif => this.createNotification(notif));
try {
const batchResults = await Promise.allSettled(batchPromises);
results.push(...batchResults);
// 批次间延迟
if (i + maxConcurrent < notifications.length) {
await this.delay(delay);
}
} catch (error) {
console.error('批量通知处理失败:', error);
}
}
const successful = results.filter(r => r.status === 'fulfilled').length;
console.log(`批量通知完成: ${successful}/${notifications.length}`);
return results;
}
// 事件处理器
onNotificationClicked(notificationId) {
console.log('通知被点击:', notificationId);
const notification = this.activeNotifications.get(notificationId);
if (notification && notification.actions) {
// 执行默认动作
this.executeAction('click', notification);
}
// 自动清除通知
this.clearNotification(notificationId);
}
onButtonClicked(notificationId, buttonIndex) {
console.log('通知按钮被点击:', notificationId, buttonIndex);
const notification = this.activeNotifications.get(notificationId);
if (notification && notification.actions && notification.actions[buttonIndex]) {
const action = notification.actions[buttonIndex];
this.executeAction(action, notification);
}
// 清除通知
this.clearNotification(notificationId);
}
onNotificationClosed(notificationId, byUser) {
console.log('通知已关闭:', notificationId, byUser ? '用户关闭' : '自动关闭');
this.activeNotifications.delete(notificationId);
// 发送关闭事件通知
chrome.runtime.sendMessage({
action: 'notificationClosed',
notificationId: notificationId,
byUser: byUser
});
}
onPermissionLevelChanged(level) {
console.log('通知权限级别改变:', level);
// 根据权限级别调整通知策略
if (level === 'denied') {
this.notificationQueue = [];
this.clearAllNotifications();
}
}
executeAction(action, notification) {
const data = notification.data || {};
switch (action) {
case 'openFile':
// 打开文件的逻辑
console.log('打开文件:', data.filepath);
break;
case 'openFolder':
// 打开文件夹的逻辑
console.log('打开文件夹:', data.folder);
break;
case 'openEmail':
// 打开邮件的逻辑
chrome.tabs.create({ url: 'mailto:' });
break;
case 'markDone':
// 标记完成的逻辑
console.log('任务已完成:', data.taskId);
break;
case 'snooze':
// 稍后提醒的逻辑
setTimeout(() => {
this.createSmartNotification('reminder', data);
}, 15 * 60 * 1000); // 15分钟后重新提醒
break;
default:
console.log('执行动作:', action, data);
}
}
generateNotificationId() {
return `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 获取通知统计
getNotificationStats() {
const active = this.activeNotifications.size;
const queued = this.notificationQueue.length;
const typeStats = {};
for (const notification of this.activeNotifications.values()) {
const type = notification.options.type || 'basic';
typeStats[type] = (typeStats[type] || 0) + 1;
}
return {
active: active,
queued: queued,
total: active + queued,
byType: typeStats,
isProcessingQueue: this.isProcessingQueue
};
}
}
// 全局通知管理器实例
const notificationsManager = new NotificationsManager();
// 导出供其他模块使用
window.notificationsManager = notificationsManager;权限要求
使用这些Chrome APIs需要在manifest.json中声明相应权限:
tabs: 标签页操作bookmarks: 书签管理history: 历史记录notifications: 通知功能activeTab: 当前标签页访问
最佳实践
- 错误处理: 始终处理API调用可能的异常
- 权限检查: 在使用API前检查权限状态
- 性能优化: 避免频繁的API调用,使用缓存
- 用户体验: 提供清晰的反馈和进度指示
- 隐私保护: 谨慎处理敏感数据如历史记录
总结
本章深入介绍了Chrome扩展中最常用的API,包括标签页管理、书签操作、历史记录处理和通知系统。掌握这些API的高级用法能够让你开发出功能强大的浏览器扩展。下一章我们将学习用户界面设计与体验优化,创建更加用户友好的扩展界面。
