注入之道:Content Script 与网页的对话艺术

Content Script 是浏览器插件与网页内容建立联系的核心桥梁。它运行在页面上下文中,具备访问和操作 DOM 的能力,是实现内容读取、界面干预和行为监听的关键手段。作为插件与网页之间的交互接口,Content Script 既保证了插件逻辑与页面环境的隔离性,又通过结构化通信机制,实现了安全、可控的数据交换。本章节,我们就来会一会这 Chrome 插件所不可或缺的一环。

初识内容脚本

内容脚本:网页与插件的对话

Content Script 是运行在网页上下文(Context)中的 JavaScript 脚本,由插件注入并受插件配置约束。

能力所及:DOM操作与功能扩展

Content Script 可以直接访问和操作网页的 DOM,具备信息提取、界面重构、交互增强等能力。其核心价值在于:在插件与网页之间建立一座可控、安全的通信桥梁,使二者在隔离中实现协作。

界限之内:隔离与限制

Content Script 虽能操作网页,却并非无所不能。它可访问部分 Chrome API(如chrome.runtime消息接口)和网页 DOM,但无法直接调用插件的后台 API(如chrome.tabs的部分方法),也不能访问网页中定义的全局变量 —— 这种 “能与不能” 的界限,正是理解其工作机制的关键。

隔离的世界

既然我们可以在网页注入脚本?那你有没有想过做一些有趣的事情呢?

比如拦截页面的API响应处理

const originalFetch = window.fetch;
window.fetch = async function(...args) {
  const response = await originalFetch(...args);
  // 尝试窃取敏感API响应
  if (args[0].includes('/api/user')) {
    const userData = await response.clone().json();
    sendToAttacker(userData);
  }
  return response;
};

或者重写网页的关键函数,改变网页的正常行为

// 篡改方法
const originalLogin = window.authenticateUser;
window.authenticateUser = function(credentials) {
  // 试图窃取登录凭证
  sendToAttacker(credentials); 
  return originalLogin(credentials);
};

再或者利用用户已登录的状态,模拟用户向其他网站发送恶意请求🤔

function stealMoney() {
  // 检查用户是否已登录银行网站(通过Cookie或其他方式)
  if (document.cookie.includes('bank_session=')) {
    // 创建一个隐藏的表单,用于向银行API发送转账请求
    const form = document.createElement('form');
    ...
    // 添加转账参数(将钱转到攻击者的账户)
    const amountInput = document.createElement('input');
    ...
    // 提交表单,浏览器会自动带上银行网站的Cookie
    form.submit();
    // 为了掩盖痕迹,提交后删除表单
    setTimeout(() => form.remove(), 100);
  }
}
// 在页面加载完成后执行攻击
window.addEventListener('load', stealMoney);

当然,且不说遵纪守法是每一位开发者的基本准则,现代浏览器的安全机制更不会为插件开发留下如此明显的漏洞。事实上,内容脚本虽运行于网页环境,却处于一个被严格隔离的执行上下文中,DOM共享但JavaScript隔离这样的设计不仅确保了插件与宿主页面之间的安全性和稳定性,更通过独立的执行上下文避免了命名空间的冲突。不同来源的插件得以在同一页面中和平共处,如同共享舞台却互不干扰的表演者 —— 每个插件都能专注于自己的功能实现,而不必担心与其他代码产生冲突。

当然,眼见为实,耳听为虚,我们可以来验证一下,在内容脚本里写上实例1的代码,可以看到,并不能改变网页里的fetch的方法

注入之道

重点来了哦,说了这么多,我们到底该怎么注入内容脚本呢?方法有三种,静态注入,动态注入以及以编程的方式注入。

以静态方式注入:按图索骥,提前布局

静态注入是最常见、最直观的内容脚本注入方式,它通过 manifest.json 文件预先声明脚本,并在页面加载时自动注入。这种方式适合那些需要在特定页面一打开就执行的脚本,例如页面初始化处理、样式替换、内容拦截等。

"content_scripts": [
  {
    "matches": ["<all_urls>"],  
    "js": ["content.js"],       
    "run_at": "document_end"    
  }
],
  • matches:定义脚本注入的页面匹配规则,支持通配符。
  • js / css:指定要注入的 JavaScript 或 CSS 文件。
  • run_at:控制注入时机,可选值包括 document_startdocument_enddocument_idle,影响脚本执行时机与页面加载节奏的配合。

以动态方式注入:随需而动,灵活出击

通过编程方式在运行时动态地将内容脚本注入到指定标签页中,通常由用户操作(如点击按钮)或某些特定事件触发。这种方式适合那些需要根据上下文判断是否执行的脚本,比如点击插件图标后高亮页面内容、抓取当前页面信息等。Chrome 提供了 chrome.scripting.executeScript()chrome.scripting.insertCSS() 两个核心方法,用于在运行时通过代码注入脚本或样式。

// 在后台脚本(service-worker.js)中
chrome.action.onClicked.addListener((tab) => {
  // 注入外部JS文件
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ['content-script.js']
  });
  
  // 注入CSS文件
  chrome.scripting.insertCSS({
    target: { tabId: tab.id },
    files: ['styles.css']
  });
  
  // 直接注入代码函数
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => {
      document.body.style.backgroundColor = '#f0f0f0';
      console.log('动态注入执行成功!');
    }
  });
});

以编程的方式注入

除了上述两种常见方式,我们还可以通过编程逻辑,根据页面状态、用户行为、甚至远程配置,决定何时、何地、注入哪些脚本。这就是以编程的方式进行注入。

注:动态注入和以编程的方式注入,虽然听起来有些相似,但它们其实是两个不同维度的概念,动态注入 是一种 注入时机上的“动态性” (脚本不是在页面加载时自动注入,而是在运行时由插件主动触发注入)。以编程的方式注入 则是一种 注入方式上的“灵活性” (开发者可以通过逻辑判断、条件控制、异步操作等,决定注入的内容、时机与目标)。

换句话来说,所有动态注入都是以编程方式实现的,但并非所有以编程方式注入的脚本都属于“动态”注入。

实践案例---护眼模式

当我们专注工作时,常常一盯屏幕就是好久,不自觉就忘了休息。这对眼睛可不太友好。这个时候,护眼模式就显得尤为重要。

{
    "manifest_version": 3,
    "name": "护眼模式切换器",
    "version": "1.0",
    "description": "静态注入内容脚本,为任意网页添加护眼模式切换按钮",
    "permissions": [
        "activeTab",
        "scripting"
    ],
    "host_permissions": [
        "<all_urls>"
    ],
    "content_scripts": [
        {
            "matches": [
                "<all_urls>"
            ],
            "js": [
                "content.js"
            ],
            "css": [
                "styles.css"
            ]
        }
    ]
}
// 创建护眼模式切换按钮
const toggleBtn = document.createElement("button");
toggleBtn.textContent = "🌿 开启护眼模式";
toggleBtn.id = "eye-protection-toggle";

// 添加到页面右下角
document.body.appendChild(toggleBtn);

// 切换护眼模式样式
toggleBtn.addEventListener("click", () => {
  const styleId = "eye-protection-style";

  if (document.getElementById(styleId)) {
    // 已开启,关闭护眼模式
    document.getElementById(styleId).remove();
    toggleBtn.textContent = "🌿 开启护眼模式";
  } else {
    // 未开启,注入护眼样式
    const style = document.createElement("style");
    style.id = styleId;
    style.textContent = `
      body, html {
        background-color: #f2f7f2 !important;
        color: #333333 !important;
      }
      a {
        color: #006600 !important;
      }
      img {
        filter: brightness(0.95);
      }
      input, textarea, select, button {
        background-color: #f9fbf9 !important;
        color: #333333 !important;
        border-color: #ccc !important;
      }
    `;
    document.head.appendChild(style);
    toggleBtn.textContent = "🚫 关闭护眼模式";
  }
});
#eye-protection-toggle {
    position: fixed;
    bottom: 20px;
    right: 20px;
    z-index: 999999;
    padding: 10px 15px;
    background-color: #88c988;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 14px;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}

#eye-protection-toggle:hover {
    background-color: #77b577;
}

成果展示

实践案例---页面标注工具

有时候,我们在网上查资料,看到重点想圈一下,可又不能拿红笔戳屏幕……截图吧,麻烦;记笔记吧,懒得动。这时候就特别希望:要是能直接在网页上画两笔就好了!

{
    "manifest_version": 3,
    "name": "页面标注工具",
    "version": "1.0",
    "description": "内容脚本为当前网页添加“画笔”功能",
    "permissions": [
        "activeTab",
        "scripting"
    ],
    "host_permissions": [
        "<all_urls>"
    ],
    "action": {
        "default_popup": "popup.html"
    }
}
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        body {
            width: 200px;
            padding: 15px;
            font-family: sans-serif;
        }

        button {
            width: 100%;
            padding: 10px;
            margin-bottom: 10px;
            background-color: #2196F3;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        button:hover {
            background-color: #1976D2;
        }
    </style>
</head>

<body>
    <button id="startBtn">启动标注工具</button>
    <button id="clearBtn">清除标注</button>

    <script src="popup.js"></script>
</body>

</html>
document.getElementById("startBtn").addEventListener("click", () => {
  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
    chrome.scripting.executeScript({
      target: { tabId: tabs[0].id },
      files: ["annotation.js"]
    });
  });
});

document.getElementById("clearBtn").addEventListener("click", () => {
  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
    chrome.scripting.executeScript({
      target: { tabId: tabs[0].id },
      func: clearAnnotations
    });
  });
});

function clearAnnotations() {
  const canvas = document.getElementById("annotation-canvas");
  if (canvas) {
    const ctx = canvas.getContext("2d");
    ctx.clearRect(0, 0, canvas.width, canvas.height);
  }
}

(function () {
    let isDrawing = false;
    let startX = 0;
    let startY = 0;
  
    // 创建 canvas 覆盖整个页面
    const canvas = document.createElement("canvas");
    canvas.id = "annotation-canvas";
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    canvas.style.position = "fixed";
    canvas.style.top = "0";
    canvas.style.left = "0";
    canvas.style.zIndex = "999999";
    canvas.style.pointerEvents = "none";
    document.body.appendChild(canvas);
  
    const ctx = canvas.getContext("2d");
  
    // 鼠标按下事件
    window.addEventListener("mousedown", (e) => {
      if (e.button === 0) {
        isDrawing = true;
        startX = e.clientX;
        startY = e.clientY;
      }
    });
  
    // 鼠标移动事件
    window.addEventListener("mousemove", (e) => {
      if (!isDrawing) return;
  
      const endX = e.clientX;
      const endY = e.clientY;
  
      // 每次重绘前清除
      ctx.clearRect(0, 0, canvas.width, canvas.height);
  
      // 绘制矩形
      ctx.strokeStyle = "#ff0000";
      ctx.lineWidth = 2;
      ctx.setLineDash([]);
      ctx.strokeRect(startX, startY, endX - startX, endY - startY);
    });
  
    // 鼠标释放事件
    window.addEventListener("mouseup", (e) => {
      if (!isDrawing || e.button !== 0) return;
  
      const endX = e.clientX;
      const endY = e.clientY;
  
      // 最终绘制实线矩形
      ctx.strokeStyle = "#ff0000";
      ctx.lineWidth = 2;
      ctx.setLineDash([]);
      ctx.strokeRect(startX, startY, endX - startX, endY - startY);
  
      isDrawing = false;
    });
  
    // 页面缩放时调整 canvas 大小
    window.addEventListener("resize", () => {
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
    });
  })();
  

成果展示

从零探索Chrome插件开发,手把手教你构建实用功能,开启浏览器扩展创作之旅。

全部评论
忍耐王
点赞 回复 分享
发布于 08-09 23:47 江苏
点赞 回复 分享
发布于 08-09 20:27 江苏
mark收藏了
点赞 回复 分享
发布于 08-09 20:27 江苏
有点东西😮
点赞 回复 分享
发布于 08-05 12:26 江苏

相关推荐

12-15 14:16
门头沟学院 Java
回家当保安:发offer的时候会背调学信网,最好不要这样。 “27届 ”和“28届以下 ”公司招聘的预期是不一样的。
实习简历求拷打
点赞 评论 收藏
分享
12-24 20:46
武汉大学 Java
点赞 评论 收藏
分享
评论
5
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务