- 示例工程
【免费下载链接】chrome-extensions-samples
Chrome Extensions Samples
本文以chrome-extensions-samples仓库中的 api-samples/downloads/download_manager 示例为核心,系统讲解如何基于 Manifest V3(MV3)的chrome.downloadsAPI 家族实现一个功能完整的下载管理器扩展:包括下载项的查询与实时渲染、暂停/恢复/取消/删除等操作、危险文件处理、搜索过滤,以及 Service Worker 中基于 OffscreenCanvas 的动态工具栏图标与进度轮询。读完本文,你将掌握chrome.downloads核心 API 的调用模式,以及如何在一个 MV3 扩展中组织 popup 页面与后台 Service Worker 的分工协作。
示例概览:一个 "Download Manager Button" 扩展
该示例位于 api-samples/downloads/download_manager,是一个完整的 MV3 扩展,官方 README 用一句话概括了它的定位:
该示例使用了多个
chrome.downloadsAPI 来实现一个简单的下载管理器(Download Manager)。
实际代码远比"简单"丰富。它通过扩展的 action 按钮(popup)展示当前浏览器的下载列表,并支持对每一条下载记录执行一系列操作,同时把工具栏图标变成一枚"实时状态徽章"——正在下载时显示进度弧、存在危险文件时显示红色危险角标、暂停时显示暂停符号、有新完成项时显示绿色对勾。
扩展的文件结构如下:
api-samples/downloads/download_manager/ ├── _locales/en/messages.json # i18n 本地化文案 ├── manifest.json # MV3 清单 ├── popup.html / popup.css # 弹出层界面与样式 ├── popup.js # 弹出层核心逻辑(约 767 行) ├── service-worker.js # 后台 Service Worker(约 233 行) ├── icons.html / icons.js # 辅助工具页:重新生成 Manifest 图标 ├── icon128.png / icon19.png / icon38.png └── README.md其中 popup 与后台的分工非常清晰:popup.js 负责下载列表的展示与用户交互,service-worker.js 负责需要持续运行的状态监控与动态图标绘制。这种"UI 密集逻辑放 popup、后台只做轻量监控"的架构,正是 MV3 扩展的推荐实践,因为 popup 只在用户点击工具栏按钮时短暂存在,而 Service Worker 会因事件被唤醒。
运行与调试:三步加载一个未打包扩展
官方 README 给出的运行步骤非常简洁,共三步:
- 克隆本仓库(
chrome-extensions-samples)。 - 在 Chrome 中以"加载已解压的扩展程序"(Load unpacked)方式加载
api-samples/downloads/download_manager目录。 - 点击扩展的工具栏按钮,打开 popup。
补充几个实际运行中的细节:
- 加载入口:打开
chrome://extensions,开启"开发者模式",点击"加载已解压的扩展程序",选择api-samples/downloads/download_manager目录即可。加载后浏览器会读取 manifest.json 完成注册。 - 触发下载:由于
chrome.downloads的事件监听器(onCreated/onChanged/onErased)会唤醒 Service Worker,即使 popup 未打开,工具栏图标也会被动态更新;点击图标后 popup 打开,会立即执行一次全量查询并渲染列表。 - 辅助图标生成页:目录内的 icons.html 是一个独立的小工具页("Generate Manifest Icons"),点击按钮会通过
chrome.runtime.sendMessage('icons')通知 Service Worker,用 OffscreenCanvas 重新绘制icon16/19/38/128.png等图标并触发下载(见 service-worker.js),便于扩展作者重新生成自己的图标资源。
manifest.json 解析:downloads 权限族与可选权限
扩展的核心配置在 manifest.json,完整内容如下:
{ "name": "Download Manager Button", "version": "0.3", "manifest_version": 3, "description": "Uses multiple chrome.downloads APIs to implement a simple download manager.", "icons": { "128": "icon128.png" }, "action": { "default_icon": { "19": "icon19.png", "38": "icon38.png" }, "default_title": "__MSG_extName__", "default_popup": "popup.html" }, "background": { "service_worker": "service-worker.js" }, "default_locale": "en", "optional_permissions": ["management"], "permissions": ["downloads", "downloads.open", "downloads.ui", "storage"] }几个关键点值得逐一说明:
| 配置项 | 值 | 作用 |
|---|---|---|
manifest_version | 3 | 使用 Manifest V3,后台逻辑运行在 Service Worker 中(对应background.service_worker字段) |
permissions | downloads | chrome.downloadsAPI 的查询/管理权限,本示例几乎所有功能都依赖它 |
permissions | downloads.open | 允许调用chrome.downloads.open()打开已下载文件 |
permissions | downloads.ui | 允许调用chrome.downloads.setUiOptions()关闭浏览器原生的下载 UI(下载托盘),由本扩展自绘状态 |
permissions | storage | 使用chrome.storage.local与chrome.storage.session记录 popup 打开时间、图标缓存、权限拒绝状态等 |
optional_permissions | management | 可选权限,仅在用户点击"Show links to extensions that download files"并授权后,才通过chrome.permissions.request()申请,用于显示"由哪个扩展发起的下载"链接 |
action.default_popup | popup.html | 点击工具栏图标时打开的弹出层 |
default_locale | en | 声明默认语言为英文,配合_locales/en/messages.json使用 |
关于downloads.ui权限有一个非常重要的细节:获得该权限的扩展(通常是下载管理器类扩展)调用chrome.downloads.setUiOptions({ enabled: false })后,Chrome 会隐藏自带的下载托盘/下载条,将下载状态的呈现完全交给扩展。这正是 service-worker.js 的第一行代码所做的事情:
chrome.downloads.setUiOptions({ enabled: false });从源码结构看,本扩展正是借助这一能力"接管"了下载进度的可视化,用工具栏动态图标替代系统下载托盘。这也是为什么它需要同时申请downloads、downloads.open、downloads.ui三个权限的原因。
弹出层架构:DownloadItem 类与下载列表渲染
popup 的全部逻辑位于 popup.js,其核心是一个DownloadItem类(popup.js),它封装了"一条下载记录"的全部数据与操作:
- 数据绑定:构造函数遍历
chrome.downloads.search()返回的 DownloadItem 数据对象,把state、bytesReceived、totalBytes、filename、referrer、danger、paused、startTime、estimatedEndTime、endTime、byExtensionId等字段全部复制到实例上,并把startTime解析为Date对象(popup.js)。 - DOM 克隆:popup.html 中定义了一个隐藏的
.item模板节点,DownloadItem通过cloneNode(true)克隆模板生成列表项,并以item{id}作为 DOM id(popup.js)。 - 按开始时间排序插入:新项插入列表时,先用
binarySearch按startTime时间戳二分定位插入位置,再插入到对应 DOM 节点前后,保证列表始终按"开始时间从新到旧"排列([popup.js](https://link.gitcode.com/i/f6afd7332af812c00784f4b3ab11588f#L151-L168, L185-L216))。 - 事件绑定:每个操作按钮都绑定到对应 API 调用——
open调chrome.downloads.open(id)、show调chrome.downloads.show(id)、removeFile调chrome.downloads.removeFile(id)、erase调chrome.downloads.erase({id})、pause/resume/cancel分别调chrome.downloads.pause/resume/cancel(id)(popup.js)。
render()方法(popup.js)负责把数据状态映射为界面表现,是理解整个列表渲染的关键:
- 状态判定:
in_progress = state == 'in_progress';openable = state != 'interrupted' && exists && !deleted。文件仍可打开时显示"打开文件名"链接,否则显示灰化的"Removed"文本(popup.js)。 - 文件图标:若已有
filename且尚无icon_url,调用chrome.downloads.getFileIcon(id, { size: 32 })异步获取文件类型图标(popup.js)。 - 按钮显隐矩阵:暂停/恢复/取消按钮仅在
in_progress时出现;恢复按钮额外要求paused为真;remove-file仅当state == 'complete'且文件存在、未被删除、且chrome.downloads.removeFile可用时显示;erase在下载进行中隐藏(popup.js)。 - 进度条与剩余时间:当
in_progress || canResume时显示进度区;有totalBytes时显示bytesReceived/totalBytes及按百分比填充的 meter 进度条;estimatedEndTime存在且未暂停时,用formatTimeLeft()把剩余毫秒格式化为 "Xd Yh left / Xh Ym left / Xm Ys left / Xs left"([popup.js](https://link.gitcode.com/i/f6afd7332af812c00784f4b3ab11588f#L365-L447, L120-L139))。 - 危险文件提示:
maybeAccept()是渲染的收尾动作(popup.js)——只要某条记录处于in_progress且danger既不是'safe'也不是'accepted',就调用chrome.downloads.acceptDanger(id)触发 Chrome 原生的危险下载确认对话框,并用一个类级标志accepting_danger防止并发重复弹窗。
数据驱动:search 查询、事件监听与进度轮询
popup 里的数据不是静态快照,而是由"事件驱动 + 定时轮询"两条链路共同维持的:
初次加载:chrome.downloads.search分页取数
DownloadManager.loadItems()在脚本加载时立即执行(不等待window.onload,见 popup.js),用orderBy和limit做分页查询:
const kShowNewMax = 50; const kOldMs = 1000 * 60 * 60 * 24 * 7; // 7 天 const results = await chrome.downloads.search({ orderBy: ['-startTime'], limit: kShowNewMax + 1 });这里有个精巧的"探针"设计(popup.js):查询kShowNewMax + 1(51)条,但只展示 50 条——多出来的第 51 条用于探测"是否存在更早的下载"。如果确实存在更早记录,就显示"Show Older Downloads"按钮。默认展示策略是:优先展示 7 天(kOldMs)内、最多 50 条新记录;如果一条新的都没有,则退而展示任意时间的最多 50 条(见showNew()的兜底逻辑,popup.js)。
点击"Show Older"后,showOlder()会执行一次不带任何过滤条件的全量search({}),把隐藏的旧记录全部显示出来,同时显示"Loading Older Downloads..."占位(popup.js)。
事件监听:onCreated / onChanged / onErased
popup 注册了三个事件监听器(popup.js):
chrome.downloads.onCreated:新下载创建时,getOrCreate()拿到或新建DownloadItem,刷新列表并启动进度轮询。chrome.downloads.onChanged:任意字段变化时,把 delta 中的current值合并进实例并重新render();当状态变为in_progress且未暂停时,启动进度轮询(popup.js)。chrome.downloads.onErased:下载从历史中删除时,从 DOM 移除对应列表项并重新加载。
值得强调的是代码注释里明确提到的一个坑:bytesReceived的变化永远不会触发onChanged事件(见 service-worker.js 的注释),所以实时进度只能靠轮询。popup 的进度轮询间隔为200ms:
DownloadManager.startPollingProgress.MS = 200; DownloadManager.startPollingProgress.pollProgress = async function () { const results = await chrome.downloads.search({ state: 'in_progress', paused: false }); // 更新所有进行中记录并继续下一轮 };轮询用setTimeout自续期(而非setInterval),且只在"存在进行中且未暂停的下载"时才启动,避免空闲时反复唤醒(popup.js)。
搜索:支持引号的 token 化查询
顶部搜索框通过onsearch事件触发DownloadManager.onSearch()(popup.js)。查询串会按"空格分词、引号内整体保留"的规则拆成多个 term:
let query = document.getElementById('q').value.match(/(?:[^\s"]+|"[^"]*")+/g); // 逐个 strip 掉成对的引号,然后调用: const results = await chrome.downloads.search({ query: query });即搜索"foo bar" baz会被解析为foo bar(整体)与baz两个关键词,交给chrome.downloads.search的query参数做匹配;无结果时显示"Zero matches"占位。搜索期间会隐藏"Show Older"入口并显示"Teleporting lots of goats..."(本地化后的searching文案)。
打开文件夹:showDefaultFolder
popup 顶部的文件夹图标调用chrome.downloads.showDefaultFolder()直接打开系统的默认下载目录(popup.js)。
可选权限实战:按需申请 management 权限
这是示例中非常值得学习的一个模式:下载记录里可能带有byExtensionId/byExtensionName(由哪个扩展发起的下载),但展示"来源扩展"的链接需要management权限。示例没有在安装时强要该权限,而是:
- 在 manifest 里声明为
optional_permissions: ["management"]; - 渲染时用
chrome.permissions.contains({ permissions: ['management'] })检查是否已授权(popup.js); - 未授权时显示一条提示条与"Show links to extensions that download files"链接,点击后通过
chrome.permissions.request({ permissions: ['management'] })弹出系统授权对话框; - 若用户拒绝,则把
managementPermissionDenied写入chrome.storage.local,下次不再重复打扰用户(popup.js)。
授权成功后,每条由扩展发起的下载会显示来源扩展图标(通过chrome://extension-icon/{id}/48/1加载)和名称链接,指向chrome://extensions#{extensionId}(popup.js)。注意:show-folder、referrer(跳转到来源页面)、open等链接的目标 URL 均在render()中动态赋值。
Service Worker:setUiOptions、动态图标与 1 秒轮询
后台 service-worker.js 承担三类任务:
1. 关闭原生下载 UI
第一行即chrome.downloads.setUiOptions({ enabled: false })(需要downloads.ui权限),前文已述。
2. OffscreenCanvas 动态绘制工具栏图标
drawIcon(side, options)使用OffscreenCanvas在 Service Worker 中直接绘制图标(service-worker.js),绘制逻辑包括:
- 进度弧:存在进行中下载时,按
totalBytesReceived / totalTotalBytes的比例绘制绿色圆弧;若某些下载缺少totalBytes(服务器未给出总大小),则绘制 16 段的"未知进度"旋转条纹; - 下载箭头:中央固定的下载箭头;
- 状态角标(优先级从高到低):存在
danger非 safe/accepted 的记录 → 红色危险角标;否则存在暂停项 → 灰色暂停符号;否则存在自上次打开 popup 以来新完成的项 → 绿色完成对勾。
绘制完成后,通过chrome.action.setIcon({ imageData })一次性设置 19px 与 38px 两档图标(service-worker.js)。
3. 1 秒轮询驱动图标刷新
与 popup 的 200ms 轮询不同,后台的pollProgress()以1000ms间隔运行(pollProgress.MS = 1000,service-worker.js),同样只在存在进行中下载时自续期。它执行一次全量chrome.downloads.search({}),聚合出上述图标状态,并用chrome.storage.session缓存上一次的图标 JSON 快照——只有状态真正变化时才调用setIcon,避免无意义的重复绘制(service-worker.js)。
后台的唤醒路径覆盖了所有相关事件:onCreated、onChanged(状态变化会唤醒已卸载的 Worker)、以及 popup 通过chrome.runtime.sendMessage('poll')发来的主动请求(service-worker.js)。popup 每次打开时(setLastOpened())会记录popupLastOpened时间戳并发送poll消息,从而让"新完成下载"角标的判定以上一次打开 popup 为时间基准。
界面与国际化:popup.html / popup.css / messages.json
popup.html 定义了完整的界面骨架:顶部固定搜索栏(#q)、清除全部按钮(#clear-all,遍历可见项逐个erase())、打开下载文件夹按钮、下载项列表容器#items、隐藏的.item模板、以及危险文件授权提示条。所有操作按钮均使用内联 SVG 绘制图标(暂停/恢复/取消/删除/擦除/显示目录/引用页/来源扩展),无需额外图片资源。
popup.css 实现了紧凑的单行布局(white-space: nowrap)、悬浮操作菜单(.more绝对定位)、以及带条纹动画的进度条(.meter > span:after+@keyframes move,2 秒线性循环)。
国际化方面,messages.json 定义了全部可见文案:按钮 title、搜索占位符、空列表/零结果提示、12 个月份缩写、以及带占位符的剩余时间模板(如$days$d $hours$h left)。popup.js 的loadI18nMessages()在窗口加载时统一把chrome.i18n.getMessage()的结果写入 DOM,并会根据本地化文案长度动态调整 popup 的最小宽度(ratchetWidth/ratchetHeight,保证不同语言下 UI 不换行、不截断)。formatDateTime()还根据日期距离当前时间的远近,智能显示"HH:MMam/pm"、"日 月"缩写或年份(popup.js)。
本示例覆盖的 chrome.downloads API 全景
将全部源码整理后可得到本示例实际使用到的 API 清单,这也是一个下载管理器类扩展的"最低完整能力集":
| API | 用途 | 出现位置 |
|---|---|---|
chrome.downloads.search() | 按orderBy/limit/state/paused/query查询下载记录 | [popup.js](https://link.gitcode.com/i/f6afd7332af812c00784f4b3ab11588f#L573-L576, L691-L694, L636, L662) |
chrome.downloads.pause()/resume()/cancel() | 暂停 / 恢复 / 取消下载 | popup.js |
chrome.downloads.erase() | 从下载历史中移除记录 | popup.js |
chrome.downloads.removeFile() | 删除已下载的文件 | popup.js |
chrome.downloads.open() | 打开已完成的文件 | popup.js |
chrome.downloads.show() | 在系统文件管理器中定位文件 | popup.js |
chrome.downloads.showDefaultFolder() | 打开系统默认下载目录 | popup.js |
chrome.downloads.getFileIcon() | 获取文件类型图标 | popup.js |
chrome.downloads.acceptDanger() | 触发危险下载确认 | popup.js |
chrome.downloads.setUiOptions() | 接管/关闭浏览器原生下载 UI | service-worker.js |
chrome.downloads.download() | 发起新下载(生成图标场景) | service-worker.js |
chrome.downloads.onCreated/onChanged/onErased | 下载生命周期事件 | popup.js |
chrome.action.setIcon() | 动态更新工具栏图标 | service-worker.js |
chrome.permissions.request()/contains() | 按需申请可选权限 | popup.js |
chrome.storage.local / session | 持久化与图标快照缓存 | [popup.js](https://link.gitcode.com/i/f6afd7332af812c00784f4b3ab11588f#L11, L394-L407)、service-worker.js |
chrome.runtime.sendMessage / onMessage | popup 与 Worker 通信 | popup.js、service-worker.js |
两个值得借鉴的实现细节
最后提炼示例中两个可复用的工程技巧:
- "少查询一条"的探针分页:
loadItems查询 51 条只展示 50 条,用多出的 1 条判断"是否有更早记录",从而决定是否显示"Show Older"按钮——这是避免额外一次 API 调用的低成本做法(popup.js)。 - 两个层级的差异化轮询:popup 需要高刷新率的进度条,用 200ms 轮询;Service Worker 只关心工具栏徽章的整体状态,用 1000ms 轮询,且仅在存在进行中下载时自续期、仅在状态快照变化时重绘。这种"按需唤醒、按变化绘制"的策略能显著减少后台 Worker 的空转与绘制开销。
参考同仓库其他 downloads 示例(downloads_overwrite、download_links、download_filename_controller)可以进一步看到chrome.downloads在文件命名、批量下载、下载触发等场景的更多用法;而本示例聚焦的是"下载完成后的全生命周期管理",二者互补。若要在自己的项目中复刻,直接以本示例的 manifest.json 为起点,按上文权限表裁剪权限,即可快速落地一个拥有完整下载管理能力的 MV3 扩展。
- 示例工程
【免费下载链接】chrome-extensions-samples
Chrome Extensions Samples
相关推荐
如何快速上手 WrenAI:从安装到第一次跑通自然语言查询
如何快速上手 WrenAI:从安装到第一次跑通自然语言查询 WrenAI 是一个开源的生成式 BI(GenBI)引擎:它让 AI 智能体通过受治理的 text
示例工程Chrome 扩展 web_accessible_resources 完整指南:基于 chrome-extensions-samples 的实战示例解析
Chrome 扩展 web_accessible_resources 完整指南:基于 chrome extensions samples 的实战示例解析 本指南
示例工程Hindsight微服务架构:将记忆系统拆分为独立服务的完整指南
Hindsight微服务架构:将记忆系统拆分为独立服务的完整指南 Hindsight作为一款先进的AI代理记忆系统,其微服务架构设计为大规模生产部署提供了强大的
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考