在Chrome扩展开发中,Content Scripts(内容脚本)是与网页交互的核心机制。它允许你的扩展直接读取和修改用户正在浏览的网页内容,是实现广告拦截、页面增强、数据抓取、自动化操作等功能的基础。本文将系统讲解Content Scripts的工作原理、注入方式、隔离世界机制、DOM操作技巧以及与后台Service Worker的通信实战。

一、Content Scripts是什么
Content Scripts是运行在网页上下文中的JavaScript文件,它们由浏览器在匹配的页面上自动注入。与普通的页面脚本不同,Content Scripts拥有一个特殊权限:它们可以访问Chrome扩展的API子集(如
1 | chrome.runtime |
、
1 | chrome.storage |
),同时又能够操作当前网页的DOM。
但Content Scripts有一个关键限制:它们运行在隔离世界(Isolated World)中。这意味着Content Scripts与页面本身的JavaScript共享同一个DOM,但不共享JavaScript变量、函数和原型链。这是Chrome扩展安全模型的核心设计,避免了网页脚本污染扩展逻辑,也防止扩展意外修改页面JS环境。
隔离世界的具体表现
- DOM共享:Content Scripts和页面脚本看到的是同一个DOM树,对DOM的修改互相可见
- JS环境隔离:Content Scripts中定义的
1window.foo
不会在页面脚本中可见,反之亦然
- 原型链隔离:Content Scripts中的
1Array.prototype
修改不影响页面中的数组
- 事件隔离:Content Scripts添加的事件监听器和页面脚本的监听器互不干扰,但都能收到同一个DOM事件
二、声明Content Scripts的三种方式
在Manifest V3中,可以通过三种方式注入Content Scripts,每种方式适用于不同的场景。
1. manifest.json静态声明
最常用的方式,在
1 | manifest.json |
中声明匹配规则,浏览器会在页面加载时自动注入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 {
"manifest_version": 3,
"name": "My Extension",
"version": "1.0",
"content_scripts": [
{
"matches": ["https://*.example.com/*"],
"exclude_matches": ["*://*/*quiz*"],
"include_globs": ["https://*/*/article/*"],
"exclude_globs": ["https://*/*/private/*"],
"css": ["styles/content.css"],
"js": ["content.js"],
"run_at": "document_idle",
"all_frames": false,
"match_about_blank": false
}
]
}
关键参数说明:
| 参数 | 说明 | 取值 | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
必须字段,定义URL匹配模式 | 如
|
||||||||
|
排除特定URL | 同上 | ||||||||
|
对matches结果再按通配符过滤路径 |
|
||||||||
|
注入时机 |
/
/
|
||||||||
|
是否注入到所有iframe |
/
|
||||||||
|
注入到哪个世界(MV3新增) |
(默认) /
|
2. chrome.scripting动态注入
当需要根据用户操作或特定条件注入时,使用编程式注入更灵活。需要在manifest中声明
1 | scripting |
权限:
1
2
3
4
5 {
"manifest_version": 3,
"permissions": ["scripting", "activeTab"],
"host_permissions": ["https://*.example.com/*"]
}
然后在Service Worker中调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 // background.js (Service Worker)
chrome.action.onClicked.addListener(async (tab) => {
if (!tab.id) return;
try {
await chrome.scripting.executeScript({
target: { tabId: tab.id, allFrames: false },
files: ['content.js']
});
console.log('Content script injected successfully');
} catch (err) {
console.error('Injection failed:', err);
}
});
// 也可以注入内联代码
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (pageTitle) => {
document.title = pageTitle;
return document.title;
},
args: ['Modified by Extension']
});
动态注入的优势在于按需执行:不会在所有匹配页面自动运行,只在用户点击扩展按钮或满足特定逻辑时才注入,减少了资源消耗。
3. MAIN世界注入(MV3新特性)
Manifest V3引入了
1 | world: "MAIN" |
选项,允许Content Scripts直接运行在页面的主世界中,与页面JS共享变量和函数。这是访问页面自定义对象、修改页面JS行为的唯一方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 // 静态声明方式
"content_scripts": [{
"matches": ["https://*.example.com/*"],
"js": ["main-world.js"],
"world": "MAIN",
"run_at": "document_start"
}]
// 动态注入方式
chrome.scripting.executeScript({
target: { tabId: tab.id },
world: 'MAIN',
func: () => {
// 可以直接访问页面JS环境
window.myPageObject.customMethod();
}
});
需要注意的是,MAIN世界中的脚本无法访问
1 | chrome.*.api |
,如果需要与扩展通信,必须配合ISOLATED世界脚本作为桥梁。
三、DOM操作实战技巧

等待元素出现的可靠方案
现代Web应用常使用动态渲染,直接
1 | querySelector |
可能找不到元素。使用
1 | MutationObserver |
是最佳实践:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 // content.js
function waitForElement(selector, timeout = 10000) {
return new Promise((resolve, reject) => {
const element = document.querySelector(selector);
if (element) {
resolve(element);
return;
}
const observer = new MutationObserver((mutations, obs) => {
const node = document.querySelector(selector);
if (node) {
obs.disconnect();
resolve(node);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
reject(new Error(`Element ${selector} not found within ${timeout}ms`));
}, timeout);
});
}
// 使用示例
waitForElement('.article-content', 15000)
.then((el) => {
el.style.fontSize = '18px';
el.style.lineHeight = '1.8';
console.log('Article styled successfully');
})
.catch(console.error);
SPA路由变化监听
单页应用(SPA)不触发传统页面跳转,
1 | run_at |
只在初始加载时生效。监听URL变化是必要的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 let lastUrl = location.href;
new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
onUrlChange();
}
}).observe(document, { subtree: true, childList: true });
function onUrlChange() {
console.log('URL changed to:', location.href);
// 根据新路由执行对应逻辑
if (location.pathname.startsWith('/article/')) {
enhanceArticlePage();
}
}
安全地插入UI组件
Content Scripts注入的UI组件容易受到页面CSS污染。使用Shadow DOM隔离样式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 function createFloatingPanel() {
const host = document.createElement('div');
host.id = 'my-ext-host';
host.style.position = 'fixed';
host.style.top = '20px';
host.style.right = '20px';
host.style.zIndex = '2147483647';
document.documentElement.appendChild(host);
// 创建Shadow DOM隔离样式
const shadow = host.attachShadow({ mode: 'closed' });
// 注入样式
const style = document.createElement('style');
style.textContent = `
:host {
all: initial;
}
.panel {
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
width: 320px;
font-family: -apple-system, sans-serif;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
.panel h3 {
margin: 0 0 12px;
color: #333;
font-size: 16px;
}
`;
shadow.appendChild(style);
// 注入内容
const panel = document.createElement('div');
panel.className = 'panel';
panel.innerHTML = '<h3>扩展面板</h3>由Content Script注入</p>';
shadow.appendChild(panel);
return { host, shadow };
}
createFloatingPanel();
四、与Service Worker的通信
Content Scripts无法直接访问大部分Chrome API(如网络请求、跨域访问、书签管理等),需要通过消息传递将任务委托给Service Worker处理。
一次性消息传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 // content.js - 发送请求
chrome.runtime.sendMessage(
{ type: 'FETCH_DATA', url: 'https://api.example.com/data' },
(response) => {
if (chrome.runtime.lastError) {
console.error('Message error:', chrome.runtime.lastError.message);
return;
}
console.log('Received data:', response);
}
);
// background.js - 处理请求
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === 'FETCH_DATA') {
fetch(request.url)
.then(res => res.json())
.then(data => sendResponse({ success: true, data }))
.catch(err => sendResponse({ success: false, error: err.message }));
return true; // 关键:保持消息通道开启,等待异步sendResponse
}
});
注意:当
1 | onMessage |
监听器需要异步处理时,必须返回
1 | true |
,否则
1 | sendResponse |
会因通道关闭而失效,这是最常见的坑之一。
长连接通信(Port)
对于频繁通信场景,使用长连接更高效:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 // content.js
const port = chrome.runtime.connect({ name: 'content-bg' });
port.postMessage({ type: 'SUBSCRIBE', events: ['price_update'] });
port.onMessage.addListener((msg) => {
if (msg.type === 'PRICE_UPDATE') {
updatePriceDisplay(msg.data);
}
});
port.onDisconnect.addListener(() => {
console.log('Port disconnected, attempting reconnect...');
setTimeout(() => reconnectPort(), 1000);
});
// background.js
chrome.runtime.onConnect.addListener((port) => {
if (port.name === 'content-bg') {
port.onMessage.addListener((msg) => {
if (msg.type === 'SUBSCRIBE') {
// 开始推送数据
const interval = setInterval(() => {
port.postMessage({
type: 'PRICE_UPDATE',
data: { price: Math.random() * 100 }
});
}, 2000);
port.onDisconnect.addListener(() => {
clearInterval(interval);
});
}
});
}
});
五、跨域与CSP问题处理

Content Scripts运行在网页的上下文中,受页面CSP(内容安全策略)限制。如果页面设置了严格的CSP,注入内联脚本或样式可能被阻止。
绕过CSP的技巧
当遇到CSP错误时,有几个解决方案:
- 使用外部资源:将脚本和样式打包到扩展中,通过
1chrome.runtime.getURL
引用
- 动态修改CSP头:在
1declarativeNetRequest
中修改响应头(需相应权限)
- 使用MAIN世界注入:MV3的
1world: "MAIN"
注入的脚本不受扩展CSP限制
下面是使用
1 | declarativeNetRequest |
移除CSP的示例规则:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 // rules.json
[{
"id": 1,
"priority": 1,
"action": {
"type": "modifyHeaders",
"responseHeaders": [
{ "header": "content-security-policy", "operation": "remove" },
{ "header": "content-security-policy-report-only", "operation": "remove" }
]
},
"condition": {
"urlFilter": "||example.com^",
"resourceTypes": ["main_frame"]
}
}]
六、常见陷阱与最佳实践
1. run_at时机的选择
三个注入时机对应不同的需求:
-
1document_start
:在DOM构造之前注入,适合需要在页面脚本执行前hook函数的场景。此时无法访问DOM。
-
1document_end
:DOM完成解析时注入,但图片等资源可能未加载完。
-
1document_idle
(默认):浏览器在空闲时注入,介于
1document_end和
1window.onload之间,是大多数场景的最佳选择。
2. iframe的处理
默认情况下Content Scripts不注入到iframe中。如果需要操作iframe内容,设置
1 | all_frames: true |
。但要注意跨域iframe的权限:扩展只能注入到有
1 | host_permissions |
的域名下的iframe。
3. 避免内存泄漏
Content Scripts长期驻留在页面中,必须清理监听器和观察者:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 // 错误:每次页面导航都创建新观察者(在SPA中)
window.addEventListener('popstate', () => {
new MutationObserver(() => { /* ... */ })
.observe(document.body, { childList: true, subtree: true });
});
// 正确:使用单例模式管理
let observers = [];
function setupObserver() {
// 清理旧观察者
observers.forEach(o => o.disconnect());
observers = [];
const obs = new MutationObserver(() => { /* ... */ });
obs.observe(document.body, { childList: true, subtree: true });
observers.push(obs);
}
window.addEventListener('popstate', setupObserver);
4. CSP对内联样式的限制
许多网站不允许内联style属性。设置
1 | element.style.cssText |
或
1 | element.setAttribute('style', ...) |
可能被阻止。解决方法:在manifest的
1 | content_scripts.css |
中声明样式,或将样式注入到Shadow DOM中。
七、调试Content Scripts
调试Content Scripts的方式与普通网页略有不同:
- 打开目标网页,按F12打开DevTools
- 在Console面板顶部,将上下文从
1top
切换到你的扩展名称
- 此时执行的所有命令都在Content Scripts的隔离世界中运行
- 在Sources面板的
1Content scripts
标签下可以找到注入的脚本并设置断点

如果是动态注入的脚本,可以在
1 | chrome.scripting.executeScript |
调用前打开Sources面板的
1 | Event Listener Breakpoints |
→
1 | Script |
→
1 | Script First Statement |
,这样脚本一注入就会暂停。
总结
Content Scripts是Chrome扩展与网页交互的核心桥梁。掌握隔离世界机制、三种注入方式、DOM操作技巧、消息通信模式以及CSP处理,是开发高质量扩展的基础。在实际开发中,建议遵循以下原则:
- 最小权限原则:只在需要的页面注入,尽量使用
1activeTab
替代宽泛的
1host_permissions - 性能优先:避免在
1document_start
执行重逻辑,善用
1MutationObserver而非轮询
- 样式隔离:注入UI时使用Shadow DOM,避免被页面CSS污染
- 异步通信:
1onMessage
异步处理时记得返回
1true - 资源清理:在SPA中特别关注监听器和观察者的清理,防止内存泄漏
Manifest V3引入的
1 | world: "MAIN" |
选项为需要深度操作页面JS环境的扩展提供了官方支持,配合传统的ISOLATED世界脚本,可以覆盖几乎所有与网页交互的场景。合理运用这些机制,你的Chrome扩展将能够安全、高效地与任意网页进行深度交互。
汤不热吧