Plasmo 学习路线图 (三):Content Script 开发详解:与网页内容深度交互

深入学习 Plasmo 框架中的 Content Script 开发。了解如何在 Plasmo 中创建 Content Script、其重要性及隔离世界机制。通过实际代码示例,掌握 Content Script 如何与网页 DOM 深度交互,包括修改元素、读取数据和注入 CSS 样式,构建功能强大的浏览器扩展

一、 前篇回顾

简单回顾系列文章前两篇内容:

通过前文的介绍,我们已经熟练掌握了如何从零到一创建一个 plasmo项目,以及如何开发 Popup页面。在这一篇中,我们将介绍使用 plasmo简化开发另一个重要组件:Content Script

Content Script 在浏览器插件中扮演着重要的角色:它是唯一能够直接接触并修改用户访问的网页内容的组件。

二、 Content Script 简介

2.1 什么是 Content Script

Content Script是一段被注入到浏览器网页中的脚本,它运行在网页的上下文环境中,可以访问和修改网页的 DOMContent Script和网页自身的 JavaScript程序不同,它运行在隔离的环境中(Isolated world),Content Script不能直接跟网页自身的 JavaScript进行变量及函数访问。此外,Content Script也不能直接访问大部分的 Chrome扩展 APIPopup/Service Worker中的变量。总结如下:

  • 定义: Content Script 是一段被注入到浏览器网页中的脚本;
  • 运行环境: 它运行在网页的上下文环境中(Isolated World,隔离世界),可以访问和修改网页的 DOM
  • 与网页脚本的关系: 与网页自身的 JavaScript完全隔离;
  • 与扩展其他部分的关系: 与扩展的 API隔离,无法访问 Popup/Service Worker变量。

2.2 Content Script 的重要性及常见用途

Content Script是实现与网页互动功能的基础,常见的用途有:

  • 修改网页的 CSS 样式,改变页面外观(例如:夜间模式、屏蔽广告元素);
  • 向网页中注入新的 HTML 元素,添加新的 UI或信息;
  • 从网页中读取特定的数据(例如:抓取页面上的价格、文本);
  • 注入 JavaScript文件、模拟用户操作,与网页元素进行交互(例如:自动点击按钮、填写表单) 我们会在稍后的内容中讲解。

2.3 隔离世界 (Isolated World) 的重要性

其实我们可以把 Isolated World理解为浏览器为 Content Script提供的一种沙箱机制。通过将 Content ScriptJavaScript环境与网页的 JavaScript环境隔离开来,有效地防止了变量冲突和安全漏洞,保障了浏览器扩展和网页的稳定、安全运行,并允许多个扩展在同一页面上互不干扰地工作。虽然 Content Script可以在隔离世界中访问和修改网页的 DOM,但其代码本身是与网页代码隔离的。如果需要 Content Script和网页之间进行更深度的 JavaScript层面交互,需要通过 window.postMessage 等特定的消息传递机制来实现。 隔离世界的好处有:

  • 保护扩展:防止恶意网页脚本访问或篡改扩展的关键数据和功能。
  • 保护网页:防止 Content Script意外地干扰网页自身的 JavaScript运行。

正因为隔离世界,Content Script需要通过消息传递 (Message Passing) 来与扩展的其他部分(如 Service WorkerPopup)进行通信。这在后续的内容中会详细讲解。

三、 在 Plasmo 中开发 Content Script

3.1 Plasmo 中的 Content Script 文件约定

在传统的浏览器扩展开发中,一般有三种方式开发 Content Script

  1. 静态声明 (Static Declarations):manifest.json 文件中声明,浏览器会在指定的页面加载时自动注入;
  2. 动态声明 (Dynamic Declarations): 在运行时根据需要注册和取消注册 Content Script
  3. 程序化注入 (Programmatic Injection): 使用 chrome.scripting.executeScript API 在特定时机将脚本注入到网页中。

但是在 Plasmo框架中,Content Script的开发得到了很大的简化。跟 Popup相同,Content ScriptPlasmo中也是通过文件约定来识别和打包的,同样也有两种方式创建 Content Script文件:

  1. 在项目的根目录创建 content.ts文件(或其他支持的扩展名,例如:.tsx.js.jsx);
  2. 在根目录创建 content文件夹,然后在其中创建 index.ts文件。

分别如图: VS Code文件资源管理器显示Plasmo项目根目录下的content.tsx文件,这是创建Content Script的第一种方式 VS Code文件资源管理器显示Plasmo项目根目录下content文件夹中的index.tsx文件,这是创建Content Script的第二种方式

这两种写法是一致的,我们以第一种为例继续演示后续的内容。

3.2 创建并验证第一个 Content Script

我们先来开发第一个 Content Script,它只有一个简单的功能,在控制台输出加载成功。代码如下:

console.log('Plasmo Content Script Loaded Successfully!');

然后我们开始验证:

  1. 终端运行 pnpm dev
  2. 在浏览器中打开任意网站,例如百度;
  3. 打开开发者工具,切换到 Console(控制台)标签;
  4. 查看 Content Script输出的日志信息。

如图所示: Chrome浏览器开发者工具控制台显示Content Script成功加载的日志信息,验证Plasmo Content Script正常运行

这里的报错是因为我们没有导出 React组件,如果想消除报错可以把代码修改一下:

import React, { useEffect } from 'react';
const LogicOnlyContentScript = () => {
  useEffect(() => {
    console.log('Plasmo Content Script Loaded Successfully!');
  }, []);
  return null; // 不渲染任何UI
};
export default LogicOnlyContentScript;

刷新页面,发现报错已经没有了,如图: Chrome浏览器开发者工具控制台显示Content Script正常加载,无错误信息,验证React组件导出修复成功

3.3 与网页 DOM交互:修改页面内容

下面我们再来看一下 Content Script是如何修改页面内容的,我们来实现一个简单的改变网页背景颜色和在页面顶部插入一条消息的功能。代码如下:

import React, { useEffect } from 'react';
const LogicOnlyContentScript = () => {
  useEffect(() => {
    console.log('Plasmo Content Script Loaded Successfully!!');
    document.body.style.backgroundColor = 'lightblue'; // 改变背景色
    const message = document.createElement('h1');
    message.textContent = 'Hello from Plasmo Content Script!';
    document.body.prepend(message); // 在 body 开头插入消息
  }, []);
  return null; // 不渲染任何UI
};
export default LogicOnlyContentScript;

然后我们刷新页面,由于 Plasmo支持热加载,我们不需要重启服务。结果如图: 网页显示Content Script修改后的效果,页面背景变为浅蓝色,顶部插入Hello消息,演示DOM操作功能

3.4 与网页 DOM交互:读取页面数据

接下来我们再来看看 Content Script是如何读取页面数据的,我们来简单实现一下读取网页标题和页面元素。代码如下:

    import React, { useEffect } from 'react';
    const LogicOnlyContentScript = () => {
      useEffect(() => {
        console.log('Plasmo Content Script Loaded Successfully!!');
        console.log('Page Title:', document.title); // 获取网页标题
        const firstHeading = document.querySelector('body'); // 获取页面第一个 h1 元素
        if (firstHeading) {
            console.log('body Text:', firstHeading.textContent.slice(0, 100)); // 获取前面 100 个字符
        }
        // 示例:获取特定 class 元素的文本
        const hiddenElement = document.querySelector('.hidden');
        if (hiddenElement) {
            console.log('Text from .hidden:', hiddenElement.textContent);
        }
      }, []);
      return null; // 不渲染任何UI
    };
    export default LogicOnlyContentScript;

然后我们刷新页面,结果如图: Chrome浏览器开发者工具控制台显示Content Script读取页面数据的结果,包括页面标题和body元素内容

3.5 Content Script 中的样式 (CSS 注入)

接下来我们演示一下 Content Script中的样式注入,即 CSS注入。在 Plasmo框架中,Content Script的样式注入有两种方式:

  1. content.tsx中直接 import一个 .css文件, 然后 Plasmo会自动处理打包和注入;
  2. 通过JavaScript动态创建 <style>标签,并添加到 <head>中。

我们分别演示如下:

3.5.1 使用 import关键字导入 .css文件

创建 content.css文件,代码如下:

/* content.css */
.plasmo-injected-message {
  color: red;
  border: 1px solid blue;
  padding: 10px;
}

然后在 content.tsximport,然后修改属性,代码如下:

import React, { useEffect } from 'react';
import './content.css';
const LogicOnlyContentScript = () => {
  useEffect(() => {
    console.log('Plasmo Content Script Loaded Successfully!!');
    console.log('Page Title:', document.title); // 获取网页标题
    const firstHeading = document.querySelector('body'); // 获取页面第一个 h1 元素
    if (firstHeading) {
        console.log('body Text:', firstHeading.textContent.slice(0, 100));
    }
    // 示例:获取特定 class 元素的文本
    const hiddenElement = document.querySelector('.hidden');
    if (hiddenElement) {
        console.log('Text from .hidden:', hiddenElement.textContent);
    }
    // 示例:修改属性
    const message = document.createElement('h1');
    message.textContent = 'Hello from Plasmo Content Script with Style!';
    message.classList.add('plasmo-injected-message'); // 添加 CSS 类
    document.body.prepend(message);
  }, []);
  return null; // 不渲染任何UI
};
export default LogicOnlyContentScript;

刷新页面,如图: 网页显示Content Script注入CSS样式后的效果,Hello消息带有红色文字和蓝色边框,演示样式注入功能 注意:注入的 CSS会影响整个网页,需要注意选择器的作用范围,避免干扰网页原有样式。可以使用更具体的选择器或者 Shadow DOM(更高级)。

3.5.2 通过 JavaScript动态创建 <style>标签,并添加到 <head>

为了区分,我们把样式的文字颜色设置为黑色,代码如下:

import React, { useEffect } from 'react';
// import './content.css'; // 移除此行
const LogicOnlyContentScript = () => {
  useEffect(() => {
    console.log('Plasmo Content Script Loaded Successfully!!');
    console.log('Page Title:', document.title); // 获取网页标题
    // 动态创建 <style> 标签并添加 CSS 规则
    const styleElement = document.createElement('style');
    styleElement.textContent = `
      /* Dynamically Injected Styles */
      .plasmo-injected-message {
        color: black;
        border: 1px solid blue;
        padding: 10px;
      }
    `;
    document.head.appendChild(styleElement);
    const firstHeading = document.querySelector('body'); // 获取页面第一个 h1 元素
    if (firstHeading) {
        console.log('body Text:', firstHeading.textContent.slice(0, 100));
    }
    // 示例:获取特定 class 元素的文本
    const hiddenElement = document.querySelector('.hidden');
    if (hiddenElement) {
        console.log('Text from .hidden:', hiddenElement.textContent);
    }
    // 示例:修改属性
    const message = document.createElement('h1');
    message.textContent = 'Hello from Plasmo Content Script with Style!';
    message.classList.add('plasmo-injected-message'); // 添加 CSS 类
    document.body.prepend(message);
  }, []);
  return null; // 不渲染任何UI
};
export default LogicOnlyContentScript;

刷新页面,效果如图所示: 网页显示Content Script通过JavaScript动态创建style标签的效果,Hello消息文字为黑色,演示动态样式注入方法

3.6 进阶:Content Script配置

除了以上内容,我们还可以从 content.ts 导出一个对象来配置 Content Script的行为。当我们有多个 Content Script文件时,更推荐的做法是新建 content文件夹,把这些 Content Script文件都放在 content文件夹中统一管理,然后通过 Plasmo提供的 PlasmoCSConfig来导出并配置。我们把上面的代码修改成一个简单的示例,主要功能是在百度的页面上使用 Content Script实现上面的效果,代码如下:

import type { PlasmoCSConfig, PlasmoGetStyle } from "plasmo";
import React, { useEffect } from 'react';
// import './content.css'; // 移除此行
export const config: PlasmoCSConfig = {
  matches: ["https://www.baidu.com/"], // 示例:匹配所有URL
  all_frames: true       // 示例:在所有frame中注入
};
export const getStyle: PlasmoGetStyle = () => {
  const styleElement = document.createElement('style');
  styleElement.textContent = `
    /* Styles injected via Plasmo getStyle */
    .plasmo-injected-message {
      color: black; /* 用户更新的颜色 */
      border: 1px solid blue;
      padding: 10px;
    }
  `;
  return styleElement;
};
const LogicOnlyContentScript = () => {
  useEffect(() => {
    console.log('Plasmo Content Script Loaded Successfully!!');
    console.log('Page Title:', document.title); // 获取网页标题
    const firstHeading = document.querySelector('body'); // 获取页面第一个 h1 元素
    if (firstHeading) {
        console.log('body Text:', firstHeading.textContent.slice(0, 100));
    }
    // 示例:获取特定 class 元素的文本
    const hiddenElement = document.querySelector('.hidden');
    if (hiddenElement) {
        console.log('Text from .hidden:', hiddenElement.textContent);
    }
    // 示例:修改属性
    const message = document.createElement('h1');
    message.textContent = 'Hello from Plasmo Content Script with Style!';
    message.classList.add('plasmo-injected-message'); // 添加 CSS 类
    document.body.prepend(message);
  }, []);
  return null; // 不渲染任何UI
};
export default LogicOnlyContentScript;

效果跟上面是一样的,这里就不截图展示了。 Content Script配置是一项好用且实用的功能,这里只是一个简单的演示效果,后续我们会有更深入的讨论,敬请期待。

四、 下一步

总的来说,Content Script的能力主要是直接与网页互动,从而实现强大的功能。Content Script不能直接调用 PopupService Worker的函数,它们需要使用消息传递(Message Passing)来沟通。关于消息传递(Message Passing),我们将在下一篇讲解。 期待您的持续关注。

结尾 感谢阅读本篇 Content Script教程!