浏览器插件开发-content script
content script 是运行在网页上下文(DOM)中的 JavaScript 脚本,由浏览器扩展注入,能访问网页的 DOM 结构,但和网页自身的脚本、扩展的后台脚本(background)有严

浏览器插件开发-content script

发布时间:2025-12-27 (2025-12-27)

content script 是运行在网页上下文(DOM)中的 JavaScript 脚本,由浏览器扩展注入,能访问网页的 DOM 结构,但和网页自身的脚本、扩展的后台脚本(background)有严格的隔离。

核心特点

  1. 能读写当前网页的 DOM,修改样式、文本、节点等;
  2. 不能访问网页自身的全局变量 / 函数(如网页里定义的 window.xxx);
  3. 不能直接访问扩展的 chrome.api 全部方法(仅能访问 chrome.runtimechrome.storage 等少数 API);
  4. 可通过 chrome.runtime.sendMessage 与扩展的后台脚本通信。

先跑通最简单的一个示例

先在manifest.json里面加入

 "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "css": [],
      "run_at": "document_end"
    }
  ]

然后在content.js里面写上

// 核心:操作网页DOM(这是content script最基础的能力)
console.log("Content Script注入成功!");
// 修改网页标题
document.title = "被Content Script修改后的标题";
// 向网页插入一个元素
const div = document.createElement("div");
div.style.cssText = "position:fixed;top:0;left:0;background:red;color:white;padding:10px;z-index:9999;";
div.innerText = "这是Content Script插入的内容";
document.body.appendChild(div);

访问任意网站,就可以看到插入的效果了

manifest.json 配置

{
  "manifest_version": 3,
  "content_scripts": [
    {
      // 1. 匹配规则(核心,必选)
      "matches": ["https://*.example.com/*"],  // 匹配的URL(支持通配符),<all_urls>表示所有url
      "exclude_matches": ["https://example.com/admin/*"],  // 排除的URL
      "include_globs": ["*example.com/*/test*"],  // 更灵活的匹配(通配符更强)
      "exclude_globs": ["*example.com/*/test/old*"],  // 排除的glob规则
      
      // 2. 注入的脚本/样式(核心)
      "js": ["content.js", "utils.js"],  // 注入的JS文件(按顺序执行)
      "css": ["content.css"],  // 注入的CSS文件(先于JS加载)
      
      // 3. 注入时机
      "run_at": "document_end",  // 可选:document_start/document_end/document_idle(默认)
      
      // 4. 注入上下文(关键)
      "all_frames": false,  // 是否注入到网页的iframe中(默认false,仅主框架)
      "match_about_blank": false,  // 是否注入到about:blank页面(需matches匹配)
      "world": "ISOLATED",  // 运行上下文:ISOLATED(隔离,默认)/MAIN(网页主上下文)
      
      // 5. 权限相关(V3新增)
      "match_as_incognito": false  // 是否在无痕模式中注入(需扩展开启无痕权限)
    }
  ]
}
  • world:ISOLATED(隔离上下文,安全)/MAIN(网页主上下文,可直接访问网页全局变量,但风险高);
  • all_frames:设为 true 时,网页内所有 iframe 都会注入脚本(需注意 iframe 跨域问题);

核心能力(DOM / 样式 / 事件)

dom操作

content script 可完全操控网页 DOM,包括增删改查、事件监听

// 1. 查询/修改DOM
const title = document.querySelector("h1");
title.textContent = "被Content Script修改";

// 2. 新增DOM
const btn = document.createElement("button");
btn.innerText = "点击触发";
document.body.appendChild(btn);

// 3. 监听DOM事件(两种方式)
// 方式1:直接监听(推荐)
btn.addEventListener("click", () => alert("Content Script 触发点击"));
// 方式2:监听网页全局事件(如监听网页的点击)
document.addEventListener("click", (e) => {
  console.log("点击的元素:", e.target);
});

// 4. 监听DOM变化(MutationObserver)
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    console.log("DOM变化:", mutation.addedNodes); // 监听新增节点(如广告)
  });
});
// 监听body下的所有DOM变化
observer.observe(document.body, { childList: true, subtree: true });

如果是vue、react这种动态渲染的页面,不能直接在content.js里面使用document去选择元素,是取不到的,需要监听body的dom变化

console.log("Content Script注入成功!");

// 目标元素选择器
const targetSelector = ".my_site";

// 监听DOM变化的函数
function waitForElement(selector, callback) {
    // 先检查元素是否已存在
    const element = document.querySelector(selector);
    if (element) {
        callback(element);
        return;
    }

    // 创建DOM观察者
    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            // 检查新增的节点中是否包含目标元素
            const addedNodes = Array.from(mutation.addedNodes);
            for (const node of addedNodes) {
                // 节点是元素,且自身/子节点包含目标元素
                if (node.nodeType === 1) {
                    const target = node.matches(selector) ? node : node.querySelector(selector);
                    if (target) {
                        observer.disconnect(); // 找到元素后停止监听
                        callback(target);
                        return;
                    }
                }
            }
        });
    });

    // 配置监听规则:监听body下所有子节点变化
    observer.observe(document.body, {
        childList: true, // 监听子节点添加/删除
        subtree: true,   // 监听所有后代节点
        attributes: false,
        characterData: false
    });
}

// 调用函数,等待元素出现后绑定事件
waitForElement(targetSelector, (my_site) => {
    console.log("找到目标元素:", my_site);
    my_site.addEventListener("click", function (ev) {
        ev.preventDefault();
        console.log("我被点了");
    });
});

可以在content script里面获取localStorage和httpOnly=false的cookie

console.log(localStorage)
console.log(document.cookie)

样式操作

  • 静态注入:通过 manifest.json 的 css 字段注入(优先级高于网页样式);
"content_scripts": [
  {
    "matches": ["https://www.fengfengzhidao.com/*"],
    "js": ["content.js"],
    "css": ["content.css"],
    "run_at": "document_end"
  }
]

一定要注意样式的优先级,多打开控制台调试

  • 动态注入:在 JS 中创建 <style> 标签注入:
// 动态注入样式
const style = document.createElement("style");
style.id = "custom-style";
style.textContent = `
  body { background: #f5f5f5 !important; }
  .ad { display: none !important; } /* 屏蔽广告 */
`;
document.head.appendChild(style);

// 动态修改样式
document.body.style.fontSize = "16px";

访问网页全局变量(突破隔离)

如果需要在content script里面获取网页的window,需要 "world": "MAIN"

"content_scripts": [
    {
      "matches": ["*://*/*"],
      "js": ["content.js"],
      "css": [],
      "run_at": "document_end",
      "world": "MAIN"
    }
  ]

通信机制

content script 无法直接访问扩展后台 / 弹窗 / 侧边栏,需通过消息通信

与后台通信

content script发送消息

chrome.runtime.sendMessage({
    type: "FROM_CONTENT", // 消息类型(方便 service 区分不同消息)
    data: {
        pageUrl: window.location.href, // 当前网页URL
        message: "你好,我是content script!"
    }
}).then(res => {
    console.log("后台回复的消息", res)
}).catch(err => {
    console.log("出错了", err)
})

后台接收消息并回复

// 监听来自 content script 的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    // 打印收到的消息和发送者信息
    console.log("收到content script的消息:", message);
    console.log("发送者信息:", sender); // sender包含标签页、frame等信息

    // 验证消息类型(可选,用于区分不同业务的消息)
    if (message.type === "FROM_CONTENT") {
        // 构造回复数据
        const reply = {
            status: "success",
            message: "你好,我是service worker!已收到你的消息",
            receivedData: message.data // 回显收到的数据
        };

        // 发送回复(注意:如果是异步操作,需要返回true保持端口开放)
        sendResponse(reply);
    }

    // 🌟 重要:如果 sendResponse 在异步操作中调用,必须返回 true
    // 示例(异步场景):
    // setTimeout(() => {
    //   sendResponse({ status: "success", message: "异步回复" });
    // }, 1000);
    // return true;
});

没有代码提示的问题

因为编辑器不认识chrome这个全局对象

如果是webstrorm的话,直接下载chrome的ts声明

npm i -D @types/chrome

与popup通信

和给service worker发消息类似,但是popup必须是点开状态

chrome.runtime.sendMessage({
    type: "xxx", // 消息类型(方便SW区分不同消息)
    data: {
        pageUrl: window.location.href, // 当前网页URL
        message: "你好,我是content script!"
    }
}).then(res => {
    console.log("popup 回复的消息", res)
}).catch(err => {
    console.log("出错了", err)
})

popup接收消息并回复

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    console.log('popup 收到消息:', message);

    // 判断消息类型,处理不同逻辑
    if (message.type === 'xxx') {
        // 模拟处理逻辑(比如读取插件存储、调用API等)
        const replyContent = `已收到你的消息「${message.data.message}」,时间:${message.data.time}`;

        // 向Content script发送响应(同步回复)
        sendResponse({
            success: true,
            reply: replyContent
        });
    }
})

chorme storage

storage有三钟,sync,local,session

只要用 chrome.storage,必须先在 manifest.json 中声明 storage 权限

"permissions":["storage"]

他们三种的区别

特性维度 chrome.storage.sync chrome.storage.local chrome.storage.session
存储位置 云端 + 本地(同步到 Chrome 账号) 仅本地设备 内存(临时),插件进程结束即销毁
数据持久化 永久(除非手动删除 / 同步失败) 永久(除非手动删除 / 清除浏览器数据) 临时(插件关闭 / 浏览器重启后丢失)
存储大小限制 约 100KB(总数据),单条 8KB 约 5MB(可通过扩展权限提升) 约 10MB(内存级,无严格硬限制)
同步特性 多设备同步(登录同一 Chrome 账号) 仅当前设备,不同步 仅当前浏览器会话,不同步
适用场景 跨设备共享的轻量配置(如主题偏好、账号设置) 单设备的大量数据(如本地缓存、历史记录) 临时状态(如弹窗临时数据、会话级缓存)
读写性能 较慢(需云端同步) 较快(纯本地) 极快(内存操作)

storage读、写、清空、删除

// 写入
await chrome.storage.sync.set({ theme: 'dark', autoSave: true });

// 读取
const { theme } = await chrome.storage.sync.get('theme');

// 清空
await chrome.storage.sync.clear()

// 单个键删除
await chrome.storage.[sync/local/session].remove('键名');

// 多个键批量删除
await chrome.storage.[sync/local/session].remove(['键名1', '键名2']);

监听storage变化

// 监听任意Storage的变化
chrome.storage.onChanged.addListener((changes, areaName) => {
  console.log(`存储区域${areaName}发生变化:`, changes);
});