Chrome 扩展程序 Arclet Copier 🌈 开发实践
上周从使用了近 3 年的 Arc 浏览器迁移回 Google Chrome,没有了垂直标签页、Space、Library 等等我非常喜爱的也是 Arc 极具创新的功能,起初有点难以适应,但是使用了一天左右也慢慢接受了。尤其是 Chrome 的同步真的比 Arc 稳定太多太多。
但是唯独我在 Arc 上使用最频繁的 cmd + shift + c
快捷键复制 URL 是我在迁移 Chrome 使用一周之后也无法适应的(肌肉记忆的强大),研究了一下解决方案,最终决定通过 Chrome 扩展程序解决。
试用过 Shortkeys、Copy URLs 等拓展程序,可能是因为功能太全面,用起来都不太顺手,于是自己写了一个:Arclet Copier 🌈。
Arclet Copier 的功能非常简单:
- 一键复制 URL,支持静默复制,可绑定快捷键
cmd + shift + c
实现与 Arc 一致的操作体验; - 支持 URL 参数清理和 Markdown 链接格式化。
这篇文章后半部分主要想记录 Arclet Copier 开发过程中实现静默复制时遇到的问题和解决方案。
问题(v1.0.0)
在 Arclet Copier v1.0.0 中,通过键盘快捷键触发的剪贴板操作面临特定场景下会出现:
- 用户刚打开新标签页时,键盘快捷键静默复制经常失败;
- 通过扩展程序弹窗点击复制按钮则正常工作。
这个问题的根本原因在于 Arclet Copier 使用的方案中 Service Worker 架构限制和新标签页的 DOM 环境初始化时序。
最初的实现方案是通过 chrome.scripting.executeScript
API 向当前标签页注入内容脚本来执行复制操作:
// background.js 中的原始实现
async function copyToClipboard(text) {
const tab = await getCurrentTab();
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: copyContentScript, // 注入目标页面执行
args: [text],
});
}
// 在目标页面 DOM 环境中执行复制
function copyContentScript(textToCopy) {
const textarea = document.createElement("textarea");
textarea.value = textToCopy;
document.body.appendChild(textarea); // 依赖目标页面的 document.body
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
}
根据 Chrome 官方文档和社区反馈,这种方案存在以下限制:
- DOM 环境依赖:复制操作依赖目标页面的
document.body
元素; - 时序竞争条件:新标签页 DOM 初始化与脚本注入存在竞争关系;
- 权限受限页面:系统页面(
chrome://
,about:
)无法注入脚本; - Service Worker 限制:Manifest V3 的 Service Worker 无法直接访问 DOM API。
解决方案(v1.1.0)
为了解决上述问题,最终 Arclet Copier 采用了 Offscreen Document 方案。Offscreen Document 是 Chrome 在 Manifest V3 中引入的新 API,专为解决 Service Worker 无法访问 DOM API 的限制而设计。
核心思路
Chrome 官方推荐在 Manifest V3 扩展程序中使用 Offscreen Document 处理需要 DOM 环境的操作(如剪贴板、本地存储等)。
该方案的核心思路是:
- 在扩展内部创建独立的、隐藏的文档环境;
- 通过消息传递机制与 Service Worker 通信;
- 避免对目标页面 DOM 状态的依赖。
根据 Chrome 官方文档,Offscreen Document 具有以下特性:
- 每个扩展同时只能创建一个 Offscreen Document;
- 具有完整的 DOM 环境,但对用户不可见;
- 支持
document.execCommand('copy')
等传统 DOM API。
实现步骤
1. 权限配置
// manifest.json - 添加 Offscreen Document 相关权限
{
"permissions": [
"offscreen", // 创建 Offscreen Document
"clipboardWrite" // 剪贴板写入权限
]
}
2. 创建隐藏文档
<!-- offscreen.html - 包含用于复制操作的文本域 -->
<!doctype html>
<html>
<body>
<textarea id="text" style="position: fixed; left: -9999px;"></textarea>
<script src="offscreen.js"></script>
</body>
</html>
3. 剪贴板操作处理
// offscreen.js - 处理来自 Service Worker 的复制请求
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "copy") {
handleClipboardWrite(message.text)
.then(() => sendResponse({ success: true }))
.catch((error) => sendResponse({ success: false, error: error.message }));
return true; // 保持消息通道开启以支持异步响应
}
});
async function handleClipboardWrite(data) {
const textEl = document.querySelector("#text");
textEl.value = data;
textEl.select();
// 在 offscreen 环境中,execCommand 是可靠的
// 但其实也可以考虑优先使用现代 Clipboard API
const success = document.execCommand("copy");
if (!success) {
throw new Error("execCommand copy failed");
}
}
4. Service Worker 集成
// background.js - 通过消息传递使用 offscreen document
async function copyToClipboard(text) {
await ensureOffscreenDocument();
const response = await chrome.runtime.sendMessage({
action: "copy",
text: text,
});
if (!response?.success) {
throw new Error(response?.error || "Copy failed");
}
}
// 确保 offscreen document 存在(避免重复创建)
async function ensureOffscreenDocument() {
const existingContexts = await chrome.runtime.getContexts({
contextTypes: ["OFFSCREEN_DOCUMENT"],
});
if (existingContexts.length === 0) {
await chrome.offscreen.createDocument({
url: chrome.runtime.getURL("offscreen.html"),
reasons: ["CLIPBOARD"],
justification: "Perform clipboard write operation",
});
}
}
结
采用 Offscreen Document 方案后,解决了原有方案的核心问题:
对比维度 | 原始方案 | Offscreen Document 方案 |
---|---|---|
环境依赖 | 依赖目标页面 DOM 状态 | 独立的扩展内部 DOM 环境 |
时序问题 | 受新标签页加载状态影响 | 与页面加载状态无关 |
兼容性 | 无法在系统页面工作 | 支持所有页面类型 |
稳定性 | 存在竞争条件导致的失败 | 消除时序相关的失败因素 |
基于官方文档和实际测试,以下是关键的技术要点:
- API 选择:Offscreen Document 是 Manifest V3 中处理需要 DOM 的操作的标准方案;
- 通信机制:使用
chrome.runtime.sendMessage
实现 Service Worker 与 Offscreen Document 的双向通信; - 复制实现:在 Offscreen 环境中,
document.execCommand('copy')
仍然是比较稳定的剪贴板写入方法,但也可以考虑优先使用现代 Clipboard API,用传统方法兜底,以提高兼容性和稳定性; - 生命周期管理:通过
chrome.runtime.getContexts
检查现有文档,避免重复创建。
欢迎通过 GitHub Releases 下载 Arclet Copier 使用体验。