Chrome扩展截图功能实现

Chrome扩展截图主要逻辑实现

本文档详细描述了一个浏览器扩展程序中的截图功能实现,该功能允许用户通过鼠标拖拽选择网页上的特定区域,并将该区域截取为图片(base64格式)。

一、整体架构

1.1 系统组件

复制代码
┌─────────────────────────────────────────┐
│          用户界面 (Popup/Page)          │
│        (触发截图,接收结果)             │
└───────────────────┬─────────────────────┘
                    │ 调用 captureScreen()
┌───────────────────▼─────────────────────┐
│          后台脚本 (Background)          │
│  (核心协调逻辑,当前代码所在位置)       │
└───────────────────┬─────────────────────┘
                    │ 注入脚本 + 消息通信
┌───────────────────▼─────────────────────┐
│         内容脚本 (Content Script)       │
│  (在网页中实现选择界面和交互)           │
└─────────────────────────────────────────┘

1.2 文件结构

复制代码
screenshot-extension/
├── manifest.json          # 扩展配置文件
├── background.ts          # 后台脚本(本文件)
├── content.ts            # 内容脚本(注入部分)
├── popup.html            # 用户界面
├── popup.js              # 界面逻辑
└── styles.css            # 样式文件

二、核心功能模块

2.1 截图主函数 captureScreen()

2.1.1 函数签名
typescript 复制代码
export const captureScreen = (
  onSuccess: (dataUrl: string, message: string) => void,
  onError: (message: string) => void
) => {
  // 实现逻辑...
};

参数说明:

  • onSuccess: 成功回调函数,接收base64图片数据和成功消息
  • onError: 失败回调函数,接收错误消息
2.1.2 状态变量
typescript 复制代码
// 用于管理截图流程的状态
let isCompleted = false;           // 完成标志,防止重复调用
let currentTabId: number | null = null;      // 当前标签页ID
let globalKeyHandler: ((e: KeyboardEvent) => void) | null = null;  // 全局键盘事件处理器
let messageHandler: ((msg: any, sender: any, response: any) => boolean | void) | null = null; // 消息监听器
let timeoutId: number | null = null;         // 超时计时器ID

2.2 资源管理模块

2.2.1 统一清理函数 cleanup()
typescript 复制代码
const cleanup = () => {
  // 移除全局ESC键监听器
  if (globalKeyHandler) {
    window.removeEventListener('keydown', globalKeyHandler, true);
    globalKeyHandler = null;
  }
  
  // 移除消息监听器
  if (messageHandler) {
    chrome.runtime.onMessage.removeListener(messageHandler);
    messageHandler = null;
  }
  
  // 清除超时计时器
  if (timeoutId) {
    clearTimeout(timeoutId);
    timeoutId = null;
  }
};

清理时机:

  1. 用户按ESC取消
  2. 选择超时(30秒)
  3. 脚本注入失败
  4. 收到取消/完成消息
2.2.2 强制停止选择 forceStopSelection()
typescript 复制代码
const forceStopSelection = () => {
  if (currentTabId) {
    chrome.tabs.sendMessage(
      currentTabId,
      { action: "forceStopScreenshot" },
      () => {
        // 忽略可能出现的错误(如标签页关闭)
        if (chrome.runtime.lastError) {
          console.log('Force stop failed:', chrome.runtime.lastError.message);
        }
      }
    );
  }
};

2.3 事件监听模块

2.3.1 全局ESC键监听
typescript 复制代码
globalKeyHandler = (e: KeyboardEvent) => {
  if (e.key === 'Escape' && !isCompleted) {
    isCompleted = true;      // 标记为已完成
    cleanup();               // 清理资源
    forceStopSelection();    // 强制停止内容脚本中的选择
    onError("截图已取消");   // 调用错误回调
  }
};

window.addEventListener('keydown', globalKeyHandler, true);

三、截图流程实现

3.1 初始化阶段

3.1.1 获取当前标签页
typescript 复制代码
chrome.tabs.query(
  { active: true, currentWindow: true },
  (tabs: chrome.tabs.Tab[]) => {
    // 检查标签页是否有效
    if (!tabs[0]?.id || !tabs[0].windowId) {
      cleanup();
      onError("无法获取当前标签页");
      return;
    }
    
    const tabId = tabs[0].id;           // 标签页ID
    const windowId = tabs[0].windowId;  // 窗口ID
    const tabUrl = tabs[0].url;         // 标签页URL
    currentTabId = tabId;               // 保存当前标签页ID
  }
);
3.1.2 检查特殊页面
typescript 复制代码
// 检查是否是浏览器内置页面
if (
  tabUrl &&
  (tabUrl.startsWith("chrome://") ||   // Chrome特殊页面
   tabUrl.startsWith("edge://") ||     // Edge特殊页面
   tabUrl.startsWith("about:"))        // Firefox特殊页面
) {
  cleanup();
  onError("无法在特殊页面上截屏");
  return;
}

3.2 消息处理器

typescript 复制代码
messageHandler = (
  message: any,
  _sender: chrome.runtime.MessageSender,
  _sendResponse: (response?: unknown) => void
) => {
  // 防重复检查
  if (isCompleted) {
    return false;
  }
  
  // 处理选择完成
  if (message.action === 'screenshotSelectionResult' && message.rect) {
    isCompleted = true;
    cleanup();
    
    // 截取整个标签页
    chrome.tabs.captureVisibleTab(
      windowId,
      { format: "png", quality: 100 },
      (dataUrl: string) => {
        if (chrome.runtime.lastError) {
          onError(chrome.runtime.lastError.message || "截屏失败");
          return;
        }
        
        if (dataUrl) {
          // 裁剪图片
          cropImage(dataUrl, message.rect, (croppedDataUrl) => {
            if (croppedDataUrl) {
              onSuccess(croppedDataUrl, "截屏成功!");
            } else {
              onError("图片裁剪失败");
            }
          });
        }
      }
    );
  }
  
  // 处理用户取消
  if (message.action === 'screenshotSelectionCancelled') {
    isCompleted = true;
    cleanup();
    onError("截图已取消");
  }
  
  return false;
};

3.3 超时控制

typescript 复制代码
// 设置30秒超时
timeoutId = window.setTimeout(() => {
  if (isCompleted) return;
  
  console.log('Screenshot timeout');
  isCompleted = true;
  cleanup();
  forceStopSelection();
  onError("截图选择超时,请重试");
}, 30000);

四、内容脚本注入

4.1 注入选择功能

typescript 复制代码
chrome.scripting.executeScript({
    target: { tabId },       // 目标标签页
    func: startSelectionFunction, // 要执行的函数
  }).then(() => {
    console.log("Selection script injected"); // 日志记录注入成功
    // 如果已经完成,强制停止选择
    if (isCompleted) {
      forceStopSelection();
    }
  }).catch((error) => {
    // 如果已经完成,直接返回
    if (isCompleted) return;

    console.error("Failed to inject script:", error); // 记录注入失败
    isCompleted = true;  // 标记为已完成
    cleanup();           // 清理资源
    onError("无法启动选择模式,请刷新页面后重试"); // 调用错误回调
});

4.2 选择功能实现 startSelectionFunction()

4.2.1 变量定义
typescript 复制代码
let selectionOverlay: HTMLDivElement | null = null;   // 选择覆盖层
let isSelecting = false;                              // 是否正在选择
let startX = 0;                                       // 鼠标起始X坐标
let startY = 0;                                       // 鼠标起始Y坐标
let selectionBox: HTMLDivElement | null = null;       // 选择框元素
let keydownHandler: ((e: KeyboardEvent) => void) | null = null; // 键盘事件处理器
let mousedownHandler: ((e: MouseEvent) => void) | null = null; // 鼠标按下处理器
let messageListener: ((msg: any) => void) | null = null; // 消息监听器
4.2.2 清理函数
typescript 复制代码
const cleanupAll = () => {
  // 移除所有事件监听器
  if (keydownHandler) document.removeEventListener('keydown', keydownHandler);
  if (messageListener) chrome.runtime.onMessage.removeListener(messageListener);
  if (selectionOverlay) {
    if (mousedownHandler) selectionOverlay.removeEventListener('mousedown', mousedownHandler);
    selectionOverlay.remove();
  }
  
  // 重置变量
  selectionBox = null;
  isSelecting = false;
};
4.2.3 创建选择界面
typescript 复制代码
const startSelectionMode = () => {
  // 创建覆盖层
  selectionOverlay = document.createElement('div');
  selectionOverlay.id = 'rcc-selection-overlay';
  selectionOverlay.style.cssText = `
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.3);
    z-index: 2147483646;
    cursor: crosshair;
  `;
  
  // 创建选择框
  selectionBox = document.createElement('div');
  selectionBox.style.cssText = `
    position: absolute;
    border: 2px dashed #1890ff;
    background: rgba(24, 144, 255, 0.1);
    pointer-events: none;
    display: none;
  `;
  
  // 创建提示文字
  const hint = document.createElement('div');
  hint.textContent = '拖拽选择截图区域,按 ESC 取消';
  hint.style.cssText = `
    position: fixed;
    top: 20px;
    left: 50%;
    transform: translateX(-50%);
    background: rgba(0, 0, 0, 0.8);
    color: white;
    padding: 8px 16px;
    border-radius: 4px;
    font-size: 14px;
    z-index: 2147483647;
    pointer-events: none;
  `;
  
  selectionOverlay.appendChild(selectionBox);
  selectionOverlay.appendChild(hint);
  document.body.appendChild(selectionOverlay);
};

4.3 鼠标事件处理

4.3.1 鼠标按下事件
typescript 复制代码
mousedownHandler = (e: MouseEvent) => {
  e.preventDefault();
  e.stopPropagation();
  
  startX = e.clientX;
  startY = e.clientY;
  
  // 显示选择框
  if (selectionBox) {
    selectionBox.style.display = 'block';
    selectionBox.style.left = startX + 'px';
    selectionBox.style.top = startY + 'px';
    selectionBox.style.width = '0px';
    selectionBox.style.height = '0px';
  }
  
  // 动态更新选择框
  const handleMouseMove = (e: MouseEvent) => {
    const currentX = e.clientX;
    const currentY = e.clientY;
    
    const left = Math.min(startX, currentX);
    const top = Math.min(startY, currentY);
    const width = Math.abs(currentX - startX);
    const height = Math.abs(currentY - startY);
    
    if (selectionBox) {
      selectionBox.style.left = left + 'px';
      selectionBox.style.top = top + 'px';
      selectionBox.style.width = width + 'px';
      selectionBox.style.height = height + 'px';
    }
  };
  
  // 选择完成
  const handleMouseUp = (e: MouseEvent) => {
    e.preventDefault();
    e.stopPropagation();
    
    const currentX = e.clientX;
    const currentY = e.clientY;
    
    const left = Math.min(startX, currentX);
    const top = Math.min(startY, currentY);
    const width = Math.abs(currentX - startX);
    const height = Math.abs(currentY - startY);
    //起始点 (100, 100) → 结束点 (200, 150)
    //left = min(100, 200) = 100
    //top = min(100, 150) = 100
    //width = |200 - 100| = 100
    //height = |150 - 100| = 50
    
    // 最小选择区域检查
    if (width < 10 || height < 10) {
      stopSelectionMode();
      return;
    }
    
    // 收集选择区域信息
    const rect = {
      x: left,
      y: top,
      width,
      height,
      viewportWidth: window.innerWidth,//视口宽度
      viewportHeight: window.innerHeight,//视口高度
      devicePixelRatio: window.devicePixelRatio || 1 //设备像素比
    };
    
    cleanupAll();
    
    // 发送结果到后台
    chrome.runtime.sendMessage({
      action: 'screenshotSelectionResult',
      rect
    });
  };
  
  document.addEventListener('mousemove', handleMouseMove);
  document.addEventListener('mouseup', handleMouseUp, { once: true });
};

4.4 键盘事件处理

typescript 复制代码
keydownHandler = (e: KeyboardEvent) => {
  if (e.key === 'Escape') {
    stopSelectionMode();
  }
};

document.addEventListener('keydown', keydownHandler);

五、图片裁剪模块

5.1 裁剪函数 cropImage()

typescript 复制代码
function cropImage(
  dataUrl: string,
  rect: { 
    x: number;
    y: number;
    width: number;
    height: number;
    viewportWidth?: number;
    viewportHeight?: number;
    devicePixelRatio?: number;
  },
  callback: (croppedDataUrl: string | null) => void
) {
  const img = new Image();
  
  img.onload = () => {
    // 计算缩放比例
    const viewportWidth = rect.viewportWidth || window.innerWidth;
    const viewportHeight = rect.viewportHeight || window.innerHeight;
    const devicePixelRatio = rect.devicePixelRatio || (img.width / viewportWidth);
    const scale = devicePixelRatio;
    
    // 创建画布
    const canvas = document.createElement("canvas");
    canvas.width = rect.width;
    canvas.height = rect.height;
    
    const ctx = canvas.getContext("2d");
    if (!ctx) {
      callback(null);
      return;
    }
    
    // 设置高质量渲染
    ctx.imageSmoothingEnabled = true;
    ctx.imageSmoothingQuality = "high";
    
    // 裁剪并绘制
    ctx.drawImage(
      img,                        // 源图像
      rect.x * scale,             // 源图像裁剪起点X(考虑设备像素比)
      rect.y * scale,             // 源图像裁剪起点Y(考虑设备像素比)
      rect.width * scale,         // 源图像裁剪宽度(考虑设备像素比)
      rect.height * scale,        // 源图像裁剪高度(考虑设备像素比)
      0,                          // 画布绘制起点X
      0,                          // 画布绘制起点Y
      rect.width,                 // 画布绘制宽度(原始选择宽度)
      rect.height                 // 画布绘制高度(原始选择高度)
    );
    
    // 转换为base64
    const croppedDataUrl = canvas.toDataURL("image/png");
    callback(croppedDataUrl);
  };
  
  img.onerror = () => {
    callback(null);
  };
  
  img.src = dataUrl;
}

5.2 像素比处理说明

问题:

  • 网页中的坐标是逻辑像素
  • 截图得到的是物理像素
  • 高DPI屏幕需要缩放计算

解决方案:

typescript 复制代码
const devicePixelRatio = window.devicePixelRatio || 1;
const scale = devicePixelRatio;

// 实际裁剪时乘以缩放比例
rect.x * scale,      // 考虑高DPI屏幕
rect.y * scale,
rect.width * scale,
rect.height * scale

六、数据流分析

6.1 完整流程图

复制代码
用户操作 → 系统响应 → 数据流转 → 最终结果
   ↓          ↓          ↓          ↓
点击截图 → 获取标签页 → 检查权限 → 准备环境
   ↓          ↓          ↓          ↓
选择区域 → 注入脚本 → 创建界面 → 等待交互
   ↓          ↓          ↓          ↓
拖拽框选 → 事件处理 → 计算坐标 → 发送消息
   ↓          ↓          ↓          ↓
确认选择 → 截全屏 → 裁剪图片 → 返回数据
   ↓          ↓          ↓          ↓
显示结果 → 清理资源 → 状态重置 → 流程结束

6.2 消息通信流程

复制代码
┌──────────┐     ESC取消     ┌──────────┐
│  用户    ├────────────────►│ 全局监听 │
└────┬─────┘                 └────┬─────┘
     │                            │
     │ 点击截图                   │ 传递消息
     ▼                            ▼
┌──────────┐    注入脚本     ┌──────────┐
│  Popup   ├────────────────►│  后台    │
└────┬─────┘                 └────┬─────┘
     │                            │
     │ 返回结果                   │ 发送消息
     ▼                            ▼
┌──────────┐    选择完成     ┌──────────┐
│  结果    │◄────────────────┤ 内容脚本 │
└──────────┘                 └──────────┘

6.3 错误处理流程

复制代码
┌──────────────┐
│  开始截图    │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│ 检查标签页   │───失败───► 清理资源,返回错误
└──────┬───────┘
       │
       ▼
┌──────────────┐
│ 检查特殊页面 │───失败───► 清理资源,返回错误
└──────┬───────┘
       │
       ▼
┌──────────────┐
│ 注入内容脚本 │───失败───► 清理资源,返回错误
└──────┬───────┘
       │
       ▼
┌──────────────┐
│ 用户选择阶段 │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│ 可能的失败: │
│ 1. ESC取消   │
│ 2. 选择太小  │
│ 3. 超时      │
│ 4. 截图失败  │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│  清理资源    │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│ 返回错误信息 │
└──────────────┘

附录:完整代码索引

文件/函数 作用 位置
captureScreen() 主入口函数 background.ts
cleanup() 资源清理 background.ts: cleanup()
forceStopSelection() 强制停止 background.ts: forceStopSelection()
startSelectionFunction() 选择功能 注入的内容脚本
cropImage() 图片裁剪 background.ts: cropImage()
messageHandler 消息处理 background.ts: messageHandler
globalKeyHandler 全局ESC监听 background.ts: globalKeyHandler

本实现提供了一个完整、健壮的截图功能解决方案,具有良好的可扩展性和可维护性。

七、截图涉及到的ChromeAPI

1、Tabs API(标签页管理)

用于查询和操作浏览器标签页,是截图功能的核心依赖。

API 方法/属性 用途
chrome.tabs.query 查询符合条件的标签页(如当前活动标签页)。
chrome.tabs.sendMessage 向指定标签页的内容脚本发送消息(用于强制停止选择模式)。
chrome.tabs.captureVisibleTab 截取指定窗口的可见区域(生成完整页面的截图数据)。
tabs.Tab.id 标签页的唯一标识(用于后续操作的目标标识)。
tabs.Tab.windowId 标签页所属窗口的 ID(用于截取窗口级别的截图)。
tabs.Tab.url 标签页的 URL(用于检查是否为特殊页面,如 chrome://)。
2、Scripting API(脚本注入)

用于将自定义 JavaScript 代码注入到网页上下文中,实现页面内的交互逻辑(如选择覆盖层)。

API 方法/属性 用途
chrome.scripting.executeScript 向指定标签页注入并执行 JavaScript 函数(用于创建选择覆盖层)。
3、Runtime API(运行时通信)

用于扩展内部(后台脚本、内容脚本、弹出页)之间的消息传递和生命周期管理。

API 方法/属性 用途
chrome.runtime.onMessage 监听来自其他扩展组件(如内容脚本)的消息。
chrome.runtime.sendMessage 向其他扩展组件发送消息(如内容脚本通知后台选择完成或取消)。
chrome.runtime.lastError 获取最近一次 API 调用的错误信息(用于错误处理)。
4、总结表格
API 类别 核心方法/属性 关键作用
Tabs query, sendMessage, captureVisibleTab, Tab.id, Tab.windowId, Tab.url 管理标签页、获取上下文信息、执行截图
Scripting executeScript 注入页面脚本,实现前端交互(如选择覆盖层)
Runtime onMessage, sendMessage, lastError 扩展内部通信、错误捕获
相关推荐
刘发财4 小时前
弃用html2pdf.js,这个html转pdf方案能力是它的几十倍
前端·javascript·github
牛奶7 小时前
2026年大模型怎么选?前端人实用对比
前端·人工智能·ai编程
牛奶7 小时前
前端人为什么要学AI?
前端·人工智能·ai编程
Kagol9 小时前
🎉OpenTiny NEXT-SDK 重磅发布:四步把你的前端应用变成智能应用!
前端·开源·agent
GIS之路11 小时前
ArcGIS Pro 中的 notebook 初识
前端
JavaGuide11 小时前
7 道 RAG 基础概念知识点/面试题总结
前端·后端
ssshooter11 小时前
看完就懂 useSyncExternalStore
前端·javascript·react.js
格砸12 小时前
从入门到辞职|从ChatGPT到OpenClaw,跟上智能时代的进化
前端·人工智能·后端
Live0000013 小时前
在鸿蒙中使用 Repeat 渲染嵌套列表,修改内层列表的一个元素,页面不会更新
前端·javascript·react native
柳杉13 小时前
使用Ai从零开发智慧水利态势感知大屏(开源)
前端·javascript·数据可视化