Rokcso's Blog (柯枝蕤叶)

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 的功能非常简单:

这篇文章后半部分主要想记录 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 官方文档和社区反馈,这种方案存在以下限制:

  1. DOM 环境依赖:复制操作依赖目标页面的 document.body 元素;
  2. 时序竞争条件:新标签页 DOM 初始化与脚本注入存在竞争关系;
  3. 权限受限页面:系统页面(chrome://, about:)无法注入脚本;
  4. 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 环境的操作(如剪贴板、本地存储等)。

该方案的核心思路是:

根据 Chrome 官方文档,Offscreen Document 具有以下特性:

实现步骤

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 环境
时序问题 受新标签页加载状态影响 与页面加载状态无关
兼容性 无法在系统页面工作 支持所有页面类型
稳定性 存在竞争条件导致的失败 消除时序相关的失败因素

基于官方文档和实际测试,以下是关键的技术要点:


欢迎通过 GitHub Releases 下载 Arclet Copier 使用体验。

#dev #build