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 扩展内部通信、错误捕获
相关推荐
二狗哈5 小时前
Cesium快速入门17:与entity和primitive交互
开发语言·前端·javascript·3d·webgl·cesium·地图可视化
xingzhemengyou15 小时前
python datetime模块使用
前端·python
GISer_Jing6 小时前
AI驱动营销增长:7大核心场景与前端实现
前端·javascript·人工智能
T___T6 小时前
Vue 3 做 todos , ref 能看懂,computed 终于也懂了
前端·javascript·面试
bug总结6 小时前
vue+A*算法+canvas解决自动寻路方案
前端·vue.js·算法
cindershade6 小时前
JavaScript 事件循环机制详解及项目中的应用
前端·javascript
王霸天6 小时前
🚀 告别“变形”与“留白”:前端可视化大屏适配的终极方案(附源码)
前端·javascript
LYFlied6 小时前
Vue版本演进:Vue3、Vue2.7与Vue2全面对比
前端·javascript·vue.js
PieroPC6 小时前
Nicegui 组件放在页面中间
前端·后端