欢迎光临

Chrome扩展Content Scripts完全指南:页面注入、隔离世界与DOM操作实战

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

Chrome扩展Content Scripts开发

一、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中定义的
    1
    window.foo

    不会在页面脚本中可见,反之亦然

  • 原型链隔离:Content Scripts中的
    1
    Array.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
    }
  ]
}

关键参数说明:

参数 说明 取值
1
matches
必须字段,定义URL匹配模式

1
https://*.example.com/*
1
exclude_matches
排除特定URL 同上
1
include_globs
对matches结果再按通配符过滤路径
1
https://*/*/article/*
1
run_at
注入时机
1
document_start

/

1
document_end

/

1
document_idle
1
all_frames
是否注入到所有iframe
1
true

/

1
false
1
world
注入到哪个世界(MV3新增)
1
ISOLATED

(默认) /

1
MAIN

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操作实战技巧

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跨域处理

Content Scripts运行在网页的上下文中,受页面CSP(内容安全策略)限制。如果页面设置了严格的CSP,注入内联脚本或样式可能被阻止。

绕过CSP的技巧

当遇到CSP错误时,有几个解决方案:

  • 使用外部资源:将脚本和样式打包到扩展中,通过
    1
    chrome.runtime.getURL

    引用

  • 动态修改CSP头:在
    1
    declarativeNetRequest

    中修改响应头(需相应权限)

  • 使用MAIN世界注入:MV3的
    1
    world: "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时机的选择

三个注入时机对应不同的需求:

  • 1
    document_start

    :在DOM构造之前注入,适合需要在页面脚本执行前hook函数的场景。此时无法访问DOM。

  • 1
    document_end

    :DOM完成解析时注入,但图片等资源可能未加载完。

  • 1
    document_idle

    (默认):浏览器在空闲时注入,介于

    1
    document_end

    1
    window.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的方式与普通网页略有不同:

  1. 打开目标网页,按F12打开DevTools
  2. 在Console面板顶部,将上下文从
    1
    top

    切换到你的扩展名称

  3. 此时执行的所有命令都在Content Scripts的隔离世界中运行
  4. 在Sources面板的
    1
    Content scripts

    标签下可以找到注入的脚本并设置断点

DevTools调试Content Scripts

如果是动态注入的脚本,可以在

1
chrome.scripting.executeScript

调用前打开Sources面板的

1
Event Listener Breakpoints

1
Script

1
Script First Statement

,这样脚本一注入就会暂停。

总结

Content Scripts是Chrome扩展与网页交互的核心桥梁。掌握隔离世界机制、三种注入方式、DOM操作技巧、消息通信模式以及CSP处理,是开发高质量扩展的基础。在实际开发中,建议遵循以下原则:

  • 最小权限原则:只在需要的页面注入,尽量使用
    1
    activeTab

    替代宽泛的

    1
    host_permissions
  • 性能优先:避免在
    1
    document_start

    执行重逻辑,善用

    1
    MutationObserver

    而非轮询

  • 样式隔离:注入UI时使用Shadow DOM,避免被页面CSS污染
  • 异步通信
    1
    onMessage

    异步处理时记得返回

    1
    true
  • 资源清理:在SPA中特别关注监听器和观察者的清理,防止内存泄漏

Manifest V3引入的

1
world: "MAIN"

选项为需要深度操作页面JS环境的扩展提供了官方支持,配合传统的ISOLATED世界脚本,可以覆盖几乎所有与网页交互的场景。合理运用这些机制,你的Chrome扩展将能够安全、高效地与任意网页进行深度交互。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » Chrome扩展Content Scripts完全指南:页面注入、隔离世界与DOM操作实战
分享到: 更多 (0)