第三章:与页面共舞 —— Content Scripts 的魔法

第三章:与页面共舞 ------ Content Scripts 的魔法

本章目标:理解并掌握 Content Scripts 的工作原理,并成功地向特定网页"注入"我们自己的脚本,实现对网页内容的读取和修改。


从"驾驶舱"到"外派无人机"

想象一下我们目前的扩展。它就像一艘停泊在空间站里的飞船。我们的 Popup 就是这艘飞船的驾驶舱------设计精良,灯光闪烁,按钮按下去也会有悦耳的提示音。但它终究被限制在空间站(浏览器工具栏)这个小小的港湾里。它能看到外面广袤的星辰大海(无数的网页),却无法真正地触及它们。

如果我们想分析一颗行星(读取某个网页的数据),或者想在一颗荒芜的星球上插上旗帜(修改某个网页的外观),光待在驾驶舱里是远远不够的。我们需要派遣一种工具------一种可以脱离飞船、直接降落在行星表面、并执行我们指令的**"外派无人机"**。

在浏览器扩展的世界里,这种"无人机"就是我们今天的主角------Content Scripts

Content Scripts(内容脚本)是一段特殊的 JavaScript 代码。它的神奇之处在于,它不运行在 Popup 的小世界里,也不运行在扩展的后台,而是被直接注入到用户当前浏览的网页上下文中,与网页自身的 HTML、CSS 和 JavaScript "生活"在一起。

这意味着什么?

  • 它可以直接访问和操作网页的 DOM :就像无人机降落后,可以用机械臂直接抓取地面上的石头一样。document.querySelector, document.getElementById 这些我们熟悉的 DOM 操作,在 Content Script 里操作的,就是那个网页本身的元素。
  • 它可以与我们扩展的其他部分通信:这架无人机虽然身处远方,但它随身携带了"无线电",可以把在行星上收集到的数据(比如网页标题、文章内容)传送回我们的飞船驾驶舱(Popup)或指挥中心(Background Script)。

掌握了 Content Scripts,你的扩展能力将发生质的飞跃。你可以制作出:

  • 网页增强工具:给 Twitter 增加"一键截图"按钮,给 GitHub 仓库增加文件大小显示。
  • 数据抓取工具:自动从电商网站的产品页面提取价格、评论和图片。
  • 自动化工具:自动填充表单、自动点击"下一页"进行翻页。
  • 阅读辅助工具 :像我的 Chat2File 一样,读取页面上的对话内容;或者像翻译插件一样,划词进行翻译。

今天,我们将亲手打造并派遣我们的第一架"无人机",让它在浩瀚的互联网海洋中,为我们插上第一面旗帜。


3.1 深入理解 Content Scripts:一个"熟悉的陌生人"

在我们开始编写代码之前,我们必须先搞清楚 Content Script 的三个核心特性。它就像一个我们既熟悉又陌生的朋友。

1. 运行环境:共享的 DOM,隔离的"世界" (Isolated World)

这是 Content Script 最核心,也最容易让人混淆的概念。

想象一个网页,比如 www.github.com。它本身就像一个完整的生态系统,有自己的 HTML 结构(骨架)、CSS(外观),还有它自己的 JavaScript(生命活动,比如你点击按钮时触发的交互)。

当我们的 Content Script 被注入到这个页面时,会发生一件非常奇妙的事情:

  • 共享的 DOM :我们的 Content Script 和页面原有的 JavaScript,共享同一个 DOM 。这意味着,页面上的任何一个元素,双方都能看到,也都能修改。如果 Content Script 把一个 <h1> 标签的颜色改成了红色,页面原有的脚本也能立刻看到这个变化。这就是我们能修改网页内容的基础。

  • 隔离的"世界" (Isolated World):然而,除了 DOM 之外,它们几乎是完全隔离的。我们的 Content Script 运行在一个独立的、被沙箱保护的 JavaScript 环境中。这意味着:

    • 变量不互通 :你在 Content Script 里定义的变量 let mySecret = '123';,页面的原生脚本是绝对访问不到的。反之亦然,你无法直接读取页面 JS 定义的全局变量(比如一个网站用 var a = 10;,你在 Content Script 里访问 a 会得到 undefined)。
    • 函数不互通:你无法直接调用页面 JS 定义的函数,反之亦然。
    • 库不冲突:如果一个网页自己用了 jQuery 2.0 版本,你的 Content Script 完全可以放心大胆地引入 jQuery 3.0 版本,它们之间不会产生任何冲突,因为它们生活在两个"平行宇宙"里,只是恰好能看到并操作同一个"物理世界"(DOM)。

为什么要有"隔离世界"?

这是出于安全稳定性的考虑。

  • 安全性:防止恶意扩展窃取网页的敏感信息。如果变量互通,一个恶意扩展就可以轻松读取你在网页上输入的密码(如果它被存在 JS 变量里的话)。
  • 稳定性 :防止你的扩展代码与网页原有代码发生冲突。如果没有隔离,你定义的一个函数 function init() {} 可能会意外地覆盖掉页面本身也叫 init 的重要函数,导致整个网页崩溃。

一句话总结 :Content Script 和页面 JS 是"室友",他们住在同一个房间(共享 DOM),但各自有自己上锁的日记本(隔离的 JS 环境),互不干涉。

2. 何时以及如何注入?------ "静态"与"动态"两种派遣方式

我们怎么告诉浏览器,应该在什么时候、往哪个网页上派遣我们的"无人机"(Content Script)呢?主要有两种方式:

  • 静态注入 (Static Injection) :也叫声明式注入。这是最常用、最简单的方式。我们直接在"营业执照" manifest.json 文件里,像立合同一样,白纸黑字地写清楚我们的派遣规则。

    • 语法 :通过 manifest.json 中的 content_scripts 字段来声明。
    • 规则 :你可以指定 matches 规则,比如 ["*://*.github.com/*"],意思就是"只要用户打开任何 github.com 的页面,就自动把下面指定的这个脚本给我注入进去!"。
    • 优点:简单、直接、无需编写额外代码。
    • 缺点:不够灵活。规则是固定的,扩展一旦安装,规则就定死了。
  • 动态注入 (Programmatic Injection) :也叫命令式注入。这种方式更加灵活和强大。我们不在 manifest.json 里写死规则,而是在我们的其他脚本(比如 Background Script 或 Popup Script)里,通过调用 chrome.scripting API,在需要的时候,像指挥官下达命令一样,动态地决定向哪个标签页注入哪个脚本。

    • 语法 :使用 chrome.scripting.executeScript() 函数。
    • 场景:比如,当用户在我们的 Popup 里点击一个按钮时,我们才决定向当前激活的标签页注入一个脚本。这给了用户主动权。
    • 优点:极度灵活,按需执行,节省资源。
    • 缺点:需要编写更多的逻辑代码来控制。

在本章,我们将首先聚焦于更基础、更常用的静态注入

3. 权限问题:访问 chrome.* API 的限制

我们的"无人机"虽然强大,但它的"无线电"功率是受限的。

在 Content Script 中,你可以自由使用绝大部分标准的 Web API,比如 documentwindowfetch 等。但是,你只能访问一小部分 chrome.* 扩展 API

具体来说,你可以使用的主要是:

  • chrome.runtime:用于和扩展的其他部分进行消息通信。这是最重要的一个!
  • chrome.i18n:用于国际化。
  • 还有一些其他的,但核心就是 runtime

不能 直接在 Content Script 里使用像 chrome.tabs, chrome.bookmarks, chrome.storage 这样的高级 API。

为什么? 同样是出于安全考虑。这是一种"权限分离"的设计思想。把强大的能力集中在扩展的"指挥中心"(Background Script)里,而"外派无人机"(Content Script)只负责执行具体任务和数据收发。如果无人机需要操作书签,它不能自己动手,而是必须通过"无线电"(chrome.runtime.sendMessage)向指挥中心发出请求:"报告指挥中心,我需要创建一个书签,请指示!" 然后由指挥中心来执行实际操作。

这确保了所有敏感操作都有一个统一的、可控的入口。

好了,理论知识已经足够武装我们的大脑了。是时候进入实战,派遣我们的第一架无人机了!


3.2 项目实战(小插曲):为 GitHub 插上我们的旗帜

为了让你能最直观地感受到 Content Script 的威力,我们先来做一个有趣的小插曲,偏离一下我们"标签页管家"的主线任务。

我们的目标 :当用户打开任何一个 GitHub 页面时,我们自动在这个页面的左下角,添加一个固定的小小的、带着我们扩展 Logo 的"徽章",点击这个徽章还能弹出一个 alert

这个小功能虽然简单,但它完美地展示了 Content Script 的核心能力:匹配特定网站修改页面 DOM执行自定义 JS

第一步:创建我们的 Content Script 文件

首先,在我们的项目根目录 my-first-extension 下,创建一个新的文件夹,专门用来存放我们的脚本文件。我们叫它 scripts

sql 复制代码
📂 my-first-extension/
├── 📂 images/
├── 📂 scripts/   <-- 新建这个文件夹
├── 📄 manifest.json
└── 📄 popup.html

然后,在 scripts 文件夹里,创建一个新的 JavaScript 文件,就叫 content.js

sql 复制代码
📂 my-first-extension/
└── 📂 scripts/
    └── 📄 content.js  <-- 新建这个文件

打开 content.js,写入以下代码。这段代码就是我们"无人机"要执行的指令集。

javascript 复制代码
// scripts/content.js

console.log("你好,来自 Content Script 的问候!我已成功注入到这个页面。");

// 创建一个"徽章"元素
const badge = document.createElement('div');

// 使用内联样式,给徽章添加样式
badge.style.position = 'fixed';
badge.style.bottom = '20px';
badge.style.left = '20px';
badge.style.padding = '10px 15px';
badge.style.backgroundColor = '#4A90E2';
badge.style.color = 'white';
badge.style.borderRadius = '8px';
badge.style.zIndex = '9999'; // 确保它在最上层
badge.style.fontFamily = 'sans-serif';
badge.style.fontSize = '14px';
badge.style.cursor = 'pointer';
badge.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';

badge.textContent = '🚩 My First Extension Was Here!';

// 为徽章添加点击事件
badge.addEventListener('click', () => {
    alert('你发现了我!这是由你的扩展添加的交互。');
});

// 将徽章添加到页面的 body 中
document.body.appendChild(badge);

console.log("徽章已成功添加到页面!");

让我们来分析一下这段指令:

  1. console.log(...): 我们的老朋友,调试信息。当这个脚本成功注入后,我们应该能在目标网页的开发者工具控制台里看到这条信息。
  2. document.createElement('div'): 我们凭空创建了一个 div 元素。
  3. badge.style...: 我们用 JavaScript 直接给这个 div 添加了一大堆 CSS 样式,把它变成了一个漂亮的、固定在左下角的蓝色徽章。zIndex: '9999' 是个小技巧,用来确保我们的元素能"浮"在网页绝大多数元素之上,不被遮挡。
  4. badge.textContent: 我们设置了徽章上显示的文字。
  5. badge.addEventListener('click', ...): 我们给徽章绑定了一个点击事件。当用户点击它时,会弹出一个 alert 警告框。
  6. document.body.appendChild(badge): 这是最关键的一步。我们把这个精心制作的徽章,作为子元素,添加到了当前网页<body> 标签的末尾。于是,它就成为了页面的一部分,并被浏览器渲染出来。

第二步:在 manifest.json 中声明派遣规则

我们的"无人机指令集" (content.js) 已经写好了,但浏览器还不知道什么时候该派遣它。现在,我们要去"市政厅"更新我们的"营业执照"。

打开 manifest.json 文件。我们需要在里面添加一个新的顶级字段:"content_scripts"

请将你的 manifest.json 修改为如下内容(注意添加的位置和逗号):

json 复制代码
{
  "manifest_version": 3,
  "name": "我的第一个扩展",
  "version": "1.0.0",
  "description": "这是一个从零到一教程中诞生的神奇扩展!",
  "icons": {
    "16": "images/icon16.png",
    "48": "images/icon48.png",
    "128": "images/icon128.png"
  },
  "action": {
    "default_icon": "images/icon16.png",
    "default_title": "点我,有惊喜!",
    "default_popup": "popup.html"
  },

  "content_scripts": [
    {
      "matches": ["*://*.github.com/*"],
      "js": ["scripts/content.js"]
    }
  ]
}

我们来剖析一下这个新添加的 content_scripts 字段:

  • "content_scripts": [ ... ]: 它的值是一个数组。这意味着你可以定义多套不同的派遣规则。比如,一套规则用于 GitHub,另一套规则用于知乎。
  • { ... }: 数组里的每一个对象,就是一套独立的派遣规则。
  • "matches": ["*://*.github.com/*"]: 这是规则的核心------匹配模式 (Match Patterns)
    • 它的值也是一个数组,你可以指定多个匹配模式。
    • *://*.github.com/* 是一个非常强大的表达式,我们来拆解它:
      • * (在 :// 前): 匹配 http 或者 https
      • * (在 .github.com 前): 匹配 github.com 的任何子域名,比如 gist.github.com, www.github.com 或者直接就是 github.com
      • /* (在最后): 匹配域名后面的任何路径。
    • 总而言之,这个模式的意思是:"无论协议是 http 还是 https,无论是 GitHub 的哪个子域名,也无论后面的路径是什么,只要是 GitHub 网站下的任何页面,都给我匹配上!"
  • "js": ["scripts/content.js"]: 这指定了当匹配成功时,需要注入的 JavaScript 文件列表。
    • 它的值也是一个数组,你可以按顺序注入多个 JS 文件。
    • 这里的路径 "scripts/content.js" 是相对于我们扩展的根目录的。

小提示 :你还可以添加 "css": ["styles/my-styles.css"] 字段,来同时注入 CSS 文件。这比在 JS 里写内联样式要优雅得多,我们以后会用到。

第三步:部署与验证

万事俱备,只欠东风!

  1. 保存 你修改过的 manifest.json 和新建的 content.js
  2. 回到 chrome://extensions 页面,刷新 我们的扩展。这是至关重要的一步,因为我们修改了 manifest.json,必须重新加载才能让新规则生效。
  3. 现在,打开一个新的标签页,访问 https://github.com(或者 GitHub 上的任何其他页面)。

仔细观察你的浏览器窗口!就在页面加载完成的一瞬间,你应该能看到:

  • 在页面的左下角,出现了一个我们设计的蓝色徽章,上面写着 "🚩 My First Extension Was Here!"
  • 用鼠标点击这个徽章,一个系统 alert 弹窗会跳出来,内容是 "你发现了我!这是由你的扩展添加的交互。"

现在,让我们进行更深入的验证:

  1. 在 GitHub 页面上,右键 -> 检查 ,打开这个网页的开发者工具
  2. 切换到 Console 面板。
  3. 你应该能在日志的最顶端或某个地方,看到我们写的那条 console.log:"你好,来自 Content Script 的问候!我已成功注入到这个页面。"
  4. 切换到 Elements 面板,滚动到 <body> 标签的最底部,你就能找到我们动态添加的那个 <div> 元素。

再做一个对比实验:打开一个非 GitHub 的网站,比如 www.google.com。你会发现,徽章没有出现,控制台里也没有我们的日志。这证明了我们的 matches 规则在精确地工作!

恭喜你!你已经成功地派遣了你的第一架"无人机",并让它在目标星球上留下了清晰的印记。

你现在已经掌握了一种足以改变整个浏览体验的强大能力。这个小小的徽章,是你通往无限创意世界的一张门票。


3.3 回到主线:为我们的"标签页管家"做准备

好了,精彩的"小插曲"结束了。虽然在 GitHub 上插旗很酷,但它和我们的"智能标签页管家"这个最终目标关系不大。

那么,Content Script 在我们的主线项目中将扮演什么角色呢?

回想一下我们的目标:管理标签页 。很多时候,我们需要的不仅仅是标签页的 URL,我们可能还需要一些页面内部的信息。比如:

  • 我们想获取当前页面的精确标题(有些标题可能是由 JS 动态生成的)。
  • 我们想提取当前页面的主要内容摘要
  • 我们想知道当前页面是否在播放视频

这些信息,都存在于页面的 DOM 或 JS 环境中,是 chrome.tabs API 无法直接提供的。这时候,就需要我们的 Content Script "无人机"降落到页面上,去"勘探"这些信息,然后通过"无线电"发回给我们的 Popup 或 Background。

虽然我们这一章还不会实现这个通信过程(那是后面章节的重头戏),但我们可以为之打下基础。

让我们把刚才的 content.jsmanifest.json 修改一下,让它更符合我们主线项目的需求。

修改 content.js

我们不再需要那个徽章了。让我们把它改成一个更通用的、准备好进行信息勘探的脚本。

javascript 复制代码
// scripts/content.js

console.log("标签页管家 Content Script 已加载!");

// 这是一个占位函数,未来我们可以用它来获取页面信息
function getPageDetails() {
    const title = document.title;
    const url = window.location.href;
    
    // 假设我们未来还想获取页面的描述 meta 标签
    const descriptionMeta = document.querySelector('meta[name="description"]');
    const description = descriptionMeta ? descriptionMeta.content : '没有找到描述';

    return {
        title: title,
        url: url,
        description: description
    };
}

// 现在我们只是在控制台打印一下,证明我们可以获取到信息
console.log("当前页面详情:", getPageDetails());

修改 manifest.json

我们的标签页管家,理论上应该能作用于所有 网页,而不仅仅是 GitHub。所以,我们需要修改 matches 规则。

有一个特殊的匹配模式可以做到这一点:"<all_urls>"

json 复制代码
// manifest.json
...
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["scripts/content.js"],
      "run_at": "document_idle"
    }
  ],
...

这里我们做了两个重要的改动:

  1. "matches": ["<all_urls>"]: 这告诉浏览器,向所有 http, https, ftp, file 协议的页面注入我们的脚本。这基本上涵盖了你日常浏览的所有网页。
  2. "run_at": "document_idle": 这是一个新的、非常有用的属性。它指定了脚本注入的时机。它有三个可选值:
    • "document_start": 在 document.head 刚创建,<body> 还没出现,页面其他脚本还没运行时就注入。速度最快,适合需要抢在页面加载前做一些事情的场景。
    • "document_end": 在 DOM 加载完成之后,但在图片、iframe 等子资源加载完成之前注入。这是默认值
    • "document_idle": 在浏览器认为页面已经加载完成并空闲下来之后注入。这是最晚、最"礼貌"的注入时机,它能确保不会因为我们的脚本注入而拖慢页面的初始加载和渲染。对于我们这种非紧急的、获取信息的任务来说,这是最佳选择。

再次部署与验证

保存文件,刷新扩展。现在,随便打开任何几个网站,比如百度、B站、知乎。在每一个页面的开发者工具控制台里,你都应该能看到我们的 Content Script 打印出的日志和它从当前页面提取到的标题、URL 等信息。

我们已经成功地为我们的"标签页管家"部署了一支庞大的"无人机舰队",它们现在潜伏在你的每一个标签页中,时刻准备着响应指挥中心的召唤。


本章总结与展望

在这一章,我们解锁了一项至关重要的超能力:

  1. 深入理解了 Content Scripts:我们搞清楚了它"共享DOM,隔离JS"的独特运行环境,以及它受限的 API 访问权限。
  2. 掌握了静态注入 :我们学会了如何通过修改 manifest.json,使用 content_scripts 字段和 matches 模式,将我们的脚本精确地注入到目标网站。
  3. 实践了 DOM 操作:我们通过一个有趣的"插旗"实例,亲手修改了 GitHub 的页面,直观感受到了 Content Script 的威力。
  4. 为未来铺路 :我们将脚本的注入范围扩展到了所有页面,并学习了 run_at 属性,为我们后续项目的开发做好了准备。

现在,我们的扩展已经拥有了"驾驶舱"(Popup)和"外派无人机"(Content Script)。但它们还像是两个独立的部门,无法有效地协同工作。驾驶舱里的指挥官无法向无人机下达指令,无人机也无法将勘探到的宝贵数据传回。

这正是我们下一章要解决的核心问题。我们将要搭建起整个扩展的"神经中枢"和"通信网络"------ Background Scripts (Service Worker)Messaging (消息通信)

下一章将是理论和实践结合得最紧密,也是最能体现扩展开发精髓的一章。准备好迎接挑战,我们将把所有独立的部件,组装成一个真正协同工作的强大系统!

相关推荐
醉城夜风~33 分钟前
CSS元素显示模式(display)
前端·css
AI大模型-小华1 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子4 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅4 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni4 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学5 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端
赵庆明老师6 小时前
Vben精讲:21-详解web-antd:tsconfig.json
前端·json·vim
wc886 小时前
微软EDGE浏览器功能学习
前端·学习·edge
破z晓6 小时前
javascript 导出excel表
开发语言·javascript·excel