当前位置: 首页 > news >正文

与页面共舞 —— 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

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

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

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

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

// scripts/content.jsconsole.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 修改为如下内容(注意添加的位置和逗号):

{"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

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

// scripts/content.jsconsole.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>"

// 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 (消息通信)

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

http://www.lryc.cn/news/613111.html

相关文章:

  • 面向对象之类、继承和多态
  • leafletMap封装使用
  • 动手学深度学习13.11. 全卷积网络 -笔记练习(PyTorch)
  • Linux 中断系统全览解析:从硬件到软件的全路线理解
  • 外部排序总结(考研向)
  • MongoDB数据存储界的瑞士军刀:cpolar内网穿透实验室第513号挑战
  • 数据结构:双向链表(Doubly Linked List)
  • 生成对抗网络(GAN)实战 - 创建逼真人脸图像
  • 电路相量法
  • (易视宝)易视TV is-E4-G-全志A20芯片-安卓4-烧写卡刷工具及教程
  • C++的“模板”
  • day069-Jenkins基础使用与参数化构建
  • golang的面向对象编程,struct的使用
  • 急危重症专科智能体”构建新一代急诊、手术与重症中心的AI医疗方向探析
  • 【深度学习机器学习】构建情绪对话模型:从数据到部署的完整实践
  • 小鸡模拟器安卓版:经典街机游戏的移动体验
  • Elcomsoft Wireless Security Auditor 安装教程-安全检测工具使用指南
  • 数据结构----栈和队列认识
  • 深度学习-卷积神经网络CNN-1×1卷积层
  • 仓库管理系统-21-前端之入库和出库管理
  • Windows中安装rustup-init.exe以及cargo build报错443
  • 零基础-动手学深度学习-9.3. 深度循环神经网络
  • 流程图使用规范
  • Android 之 面试八股文
  • GCC与NLP实战:编译技术赋能自然语言处理
  • 解决GitHub无法打开
  • idea开发工具中git如何忽略编译文件build、gradle的文件?
  • 复杂井眼测量中,陀螺定向和磁通门定向哪个更胜一筹?
  • 幕后英雄 —— Background Scripts (Service Worker)
  • 浅析 Berachain v2 ,对原有 PoL 机制进行了哪些升级?