《⚡️万字速通 Chrome 扩展开发 🔥》

2,023 阅读37分钟

阅读本文你将学到什么

  1. 操作用户正在浏览的页面
  2. 跨域请求
  3. 常驻后台
  4. 带选项页面的扩展
  5. 扩展页面间的通信
  6. 存储数据

前言

我发现很多讲解编程的书籍,在前面都会详细地讲解相关的预备知识,而大多数读者却更希望马上进行实践。没错,人们总是对基础知识很排斥,这也就是为什么在教育行业开始推崇自顶向下的教材设计方案了——先让读者看到一个最接近表面的东西,之后再慢慢深入地讲解内在的原理和基础。所以我决定一会儿在每个小节,先带大家写一个Demo程序。这样不仅可以让大家在实践中对基础知识掌握得更加牢靠,同时也调动了大家的积极性。

另外,本篇文章作者想用武侠风去讲述,并不是因为作者抽风了😅💦而是这篇文章所讲的 Chrome 扩展开发技能,恰似武侠世界里的绝妙招式,能拆能合,自成体系,怎能错过这个机会,快来跟我一起进入chrome扩展的武侠世界!🚶

6FD976F19F323577857906D7DFB9F2D4.jpg

在这纷繁复杂的数字江湖之中,有一本神秘的秘籍悄然现世,名为 《⚡️万字速通 Chrome 扩展开发 🔥》功法。此秘籍威力巨大,修炼者若能参透其中奥秘,便可在网络世界中纵横驰骋,操控网页如同掌控江湖风云。

在这秘籍的修行之路上,前一篇已为诸位铺下基石,想看前篇的大侠请看这里: 《Chrome 插件开发:构建插件与调试知识全掌握 🚀》。在此基础之上,我等当齐心协力,继续深入探索这奇妙功法,开启一场惊心动魄的探索之旅,探寻其中深藏的真谛。

正如文章开头描述,阅读本篇文章,您将学会:

  1. 操作用户正在浏览的页面 —— 幻影迷踪手
  2. 跨域请求 —— 天涯咫尺步
  3. 常驻后台 —— 隐世守护诀
  4. 带选项页面的扩展 —— 百变如意囊
  5. 扩展页面间的通信 —— 传音入密术
  6. 存储数据 —— 乾坤储物戒

每个功能均可单独修炼,开发完整 Chrome 扩展时又可融会贯通,发挥强大威力。

接下来,本文将逐一拆解各技能,从入门到精通,助大侠们掌握精髓,纵横 Chrome 扩展江湖。

正文

1.操作用户正在浏览的页面

话说武林之中,高手过招讲究见招拆招,而在这网络江湖里,用户浏览网页时也会遇到各种状况,此时便需要我们运用 Chrome 扩展的功力来化解。

咱先唠唠为啥要掌握用 Chrome 扩展操作用户正在浏览的页面的技能。想象一下,你正在浏览一篇密密麻麻的学术论文,眼睛都快看花了,要是能一键把文字变大、颜色变柔和,那得多爽😌。再比如,你打开一个满是广告的网页,心烦意乱,要是有个扩展能瞬间把广告清理干净,是不是超棒😌?这就是用 Chrome 扩展操作页面的魅力,能给用户带来极致的浏览体验。

比如uBlock Origin Lite插件

最佳浏览器广告拦截扩展,功能全面、效果一流,而且资源占用极低。

正是利用了chrome插件可以操作用户正在浏览的页面的能力,将页面中的广告识别出来之后进行过滤。

过滤之前:充满广告😒 image.png

过滤之后:干净清爽🤩 image.png

uBlock Origin Lite 插件实现广告拦截,主要靠:

  • 规则过滤:内置大量过滤规则,由开发者和社区维护。一方面针对常见广告域名,拦截对应请求,如adserver.example.com这类已知广告服务器域名。另一方面,利用元素隐藏规则,依据 CSS 选择器定位广告元素,像特定类名advertisement-banner或标签结构<div id="ad-container">,找到后隐藏它们。

  • 内容脚本注入:通过内容脚本,在页面加载时注入代码。解析 DOM 结构,遍历 DOM 树,根据元素属性、类名、标签名及位置识别广告元素,再按规则处理。

所以知道操作用户正在浏览的页面的厉害了吧!🤩

现在来做一个小Demo,简单体验一下操作用户正在浏览的页面的Dom这个能力。

Demo介绍:安装Demo插件后,进入百度首页,让用户永远点不到百度的搜索按钮,鼠标一旦hover到按钮上,按钮就会立刻随机刷新位置。

Demo效果: 1737369228304.gif

Demo源码:

1. 创建文件结构

首先,创建以下文件和目录结构:

never-click/
├── content
│   └── content.js
└── manifest.json

image.png

2.编写 manifest.json 文件

{
    "manifest_version": 3,
    "name": "永远点不到的搜索按钮",
    "version": "1.0",
    "description": "让百度搜索按钮在鼠标悬停时随机移动",
    "content_scripts": [
        {
            "matches": ["https://www.baidu.com/"],
            "js": ["./content/content.js"],
            "run_at": "document_end"
        }
    ]
}

通过Chrome扩展我们可以对用户当前浏览的页面进行操作,实际上就是对用户当前浏览页面的DOM进行操作。通过Manifest中的content_scripts属性可以指定将哪些脚本何时注入到哪些页面中,当用户访问这些页面后,相应脚本即可自动运行,从而对页面DOM进行操作。

解释

  • manifest_version: 遵循 Manifest V3 规范。

  • name: 插件的名称。

  • version: 插件的版本号。

  • description: 插件的描述。

  • content_scripts: 定义了要注入到网页中的脚本信息。

    • matches: 表示将脚本注入到所有的网页中。
    • js: 要注入的脚本文件是 content/content.js
    • run_at: 表示在文档加载完成后执行脚本。

3.编写 content.js 文件

// 初始化按钮移动功能的主函数
function initializeButton() {
    // 获取百度搜索按钮的包装元素
    const searchButtonWrapper = document.getElementById('s_btn_wr');
    
    if (searchButtonWrapper) {
        // 定义按钮随机移动的函数
        function randomizeButtonPosition() {
            // 生成 200-500px 范围内的随机位置
            const randomTop = Math.floor(Math.random() * 301 + 200);  // 301 = (500-200+1)
            const randomLeft = Math.floor(Math.random() * 301 + 200);
            
            // 设置按钮的定位和位置
            searchButtonWrapper.style.position = 'fixed';
            searchButtonWrapper.style.top = randomTop + 'px';
            searchButtonWrapper.style.left = randomLeft + 'px';
        }
        
        // 当鼠标悬停在按钮上时触发随机移动
        searchButtonWrapper.addEventListener('mouseenter', randomizeButtonPosition);
    }
}

// 首次执行初始化
initializeButton();

4.加载扩展

  • 打开 Chrome 浏览器, 进入扩展程序管理页面(下图所示步骤 或 输入 chrome://extensions/)。

  • 开启开发者模式(右上角的开关)。

  • 点击 "加载已解压的扩展程序",选择你创建的 never-click 文件夹。

完成以上步骤后,当你访问百度搜索网页时,将会永远点不到搜索按钮

image.png

image.png

5.打开百度首页

试试看吧!

1737369228304.gif

通过这个小Demo,大家已经知道了:

1.什么是 操作用户正在浏览的页面Dom

2.如何 操作用户正在浏览的页面Dom

这个技能很重要,但是单独使用这个技能并不能发挥它的厉害之处,只有与其他的技能结合才能形成必杀技。

请学习第二招

2.跨域请求

各位江湖豪杰,方才我们已见识了 Chrome扩展操作用户正在浏览的页面 的奇妙本领,可谓是在这数字江湖中渐入佳境。然而,江湖之路,总是充满了各种挑战与险阻,接下来我们便要踏入一个稍显棘手的领域 —— 跨域请求。

跨域指的是JavaScript通过XMLHttpRequest请求数据时,调用JavaScript的页面所在的域 和 被请求页面的域不一致。对于网站来说,浏览器出于安全考虑是不允许跨域。另外,对于域相同,但端口或协议不同时,浏览器也是禁止的。下表给出了进一步的说明:

image.png

但这个规则如果同样限制Chrome扩展应用,就会使其能力大打折扣,所以Google允许Chrome扩展应用不必受限于跨域限制。但出于安全考虑,需要在Manifest的permissions属性中声明需要跨域的权限。

比如,如果我们想设计一款获取维基百科数据并显示在其他网页中的扩展,就要在Manifest中进行如下声明:

{ 
    ...
    "permissions": [ "*://*.wikipedia.org/*" ]
}

这样Chrome就会允许你的扩展在任意页面请求维基百科上的内容了。

接下来,我们将通过一个生动有趣的Demo,来详细阐述如何发送跨域请求。

Demo描述: 演示如何在Chrome插件中实现跨域请求。点击【获取跨域图片】按钮,在popup页面展示跨域接口返回的图片。

Demo效果:

image.png

Demo源码:

1. 创建项目结构

sentence-demo/
├── manifest.json
└── popup/
    ├── popup.html
    └── popup.js

image.png

2.编写 manifest.json

{
    "manifest_version": 3,
    "name": "跨域获取信息",
    "version": "1.0",
    "description": "跨域获取信息",
    
    "permissions": [
        "activeTab"
    ],
    
    "host_permissions": [
        "https://b.bdstatic.com/*"
    ],
    
    "action": {
        "default_popup": "popup/popup.html"
    }
}

说明:

  • manifest_version: 使用最新的 V3 标准

  • host_permissions: 指定允许访问的跨域资源地址,配置到需要跨域请求的地址,这样Chrome就会允许你发送这些跨域请求。

  • action: 定义扩展的弹出窗口

3.编写 popup.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>获取名言警句</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            padding: 10px;
            width: 300px; /* 设置固定宽度 */
        }
        button {
            padding: 10px 20px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background-color: #45a049;
        }
        button:disabled {
            background-color: #cccccc;
            cursor: not-allowed;
        }
        p {
            margin-top: 10px;
            line-height: 1.5;
            min-height: 20px;
        }
    </style>
</head>
<body>
    <button id="fetchQuote">获取跨域图片</button>
    <p id="quoteDisplay"></p>
    <script src="popup.js"></script>
</body>
</html>

4. 编写 popup.js

document.getElementById('fetchQuote').addEventListener('click', async function () {
    try {
        // 发起请求获取图片数据
        const response = await fetch('https://b.bdstatic.com/searchbox/icms/searchbox/img/cheng_boy.png');
        // 将响应转换为 Blob 对象
        const blob = await response.blob();
        // 创建一个临时的 URL
        const imageUrl = URL.createObjectURL(blob);
        
        // 获取或创建 img 元素
        let imgElement = document.getElementById('imageDisplay');
        if (!imgElement) {
            imgElement = document.createElement('img');
            imgElement.id = 'imageDisplay';
            document.getElementById('quoteDisplay').appendChild(imgElement);
        }
        
        // 设置图片源
        imgElement.src = imageUrl;
        
        // 清理 URL 对象(可选,但建议做)
        imgElement.onload = () => {
            URL.revokeObjectURL(imageUrl);
        };
    } catch (error) {
        console.error('获取图片时出错:', error);
        document.getElementById('quoteDisplay').textContent = '获取失败,请重试';
    }
});

5. 加载扩展

  • 打开 Chrome 浏览器,访问 chrome://extensions/
  • 开启开发者模式(右上角的开关)。
  • 点击 “加载已解压的扩展程序”,选择包含上述文件的文件夹。

这样,当用户点击插件图标打开弹出窗口,再点击 “获取跨域图片” 按钮时,插件就会通过跨域请求获取图片并显示出来,从而演示跨域请求的过程。

image.png


image.png


image.png


问题 1:

如何确定一个请求属于跨域请求,且该请求的接口未将关键响应头 Access-Control-Allow-Origin 配置为*

一、判断是否跨域
首先,我们需要判断一个请求是否属于跨域请求。当以下情况发生时,可判定为跨域请求:

  • 检查协议、域名、端口号这三个要素,只要其中一个不同,此请求即为跨域请求。

二、判断接口的关键响应头 Access-Control-Allow-Origin 配置
在确认请求为跨域的基础上,我们进一步判断接口是否将关键响应头 Access-Control-Allow-Origin 配置为 *。具体操作如下:

  • 使用浏览器开发者工具

    1. 打开浏览器的开发者工具(通常按 F12 键),切换至网络(Network)面板。
    2. 触发你所怀疑的跨域请求,以便观察请求的详细信息。
    3. 在该请求的响应头中查找 Access-Control-Allow-Origin
    4. 若该响应头不存在,或者其值不为 *(此值表示允许来自所有源的请求),并且也不包含请求源的域名,那么此请求很可能会被浏览器的同源策略阻止。
    5. 例如,假设你的网页位于 http://mywebsite.com 并发起请求,若响应头 Access-Control-Allow-Origin 的值是 http://anotherwebsite.com 或者未进行设置,那么来自 http://mywebsite.com 的请求将会被阻止。

下面我们来探究一下,如果不配置 host_permissions 会引发何种情况呢?

1.删掉host_permissions配置文件内容

我们这样,把manifest.json文件中的host_permissions配置文件删掉: 删掉之前:

"host_permissions": [
    "https://b.bdstatic.com/*"
],

删掉之后:

"host_permissions": [
    //这里删掉
],

2.刷新插件

image.png

3.打开调试信息

image.png

4.查看错误 删掉host_permissions配置文件内容之后,再去请求发现,确实是报错跨域问题了。

Google允许Chrome扩展应用不必受限于跨域限制。但出于安全考虑,需要在Manifest的permissions属性中声明需要跨域的权限。

image.png

这样,你就学会了使用chrome扩展成功获取跨域请求内容的技能!此乃初入江湖的一大进步也!

3.常驻后台

各位江湖豪杰!在这 Chrome 扩展的奇妙江湖里,我们已然在诸多技能上有所建树,似那掌握了 “跨域请求” 的天涯咫尺步,可跨越疆界;又似精通 “操作用户页面” 的幻影迷踪手,能在页面 DOM 元素间穿梭自如。但江湖险恶,变幻莫测,仅靠这些恐怕还不足以立足。

今番要研习的是一门神秘莫测的奇功 —— 常驻后台,此乃 “隐世守护诀”。在这风云涌动的数字江湖中,此诀犹如一位隐世高手,悄然藏身于幕后,时刻守护着我们的网页江湖。

有时我们希望扩展不仅在用户主动发起时(如开启特定页面或点击扩展图标等)才运行,而是希望扩展自动运行并常驻后台来实现一些特定的功能,比如:

  1. 实时网络监测:自动监控网络请求,实时分析网络流量,识别潜在的恶意网站访问。一旦发现可疑请求,如向已知的恶意 IP 发送数据,立即发出警报,提醒用户注意网络安全,避免隐私信息泄露和遭受网络攻击。
  2. 广告智能拦截:持续扫描用户访问的每一个网页,实时识别并拦截各类广告。不仅能屏蔽常见的弹窗广告、横幅广告,还能智能识别并过滤那些伪装成正常内容的隐性广告,为用户打造一个清爽、无干扰的浏览环境,同时节省网络流量和页面加载时间。
  3. 多语言自动翻译:当用户浏览外文网页时,无需手动触发,扩展在后台自动检测网页语言,并实时将页面内容翻译为用户预设的语言。无论是文章、菜单还是按钮,都能瞬间以熟悉的语言呈现,打破语言障碍,让用户轻松畅享全球资讯。

Chrome允许扩展应用在后台常驻一个页面以实现这样的功能。在一些典型的扩展中,UI页面,如popup页面或者options页面,在需要更新一些状态时,会向后台页面请求数据,而当后台页面检测到状态发生改变时,也会通知UI界面刷新。

后台页面与UI页面可以相互通信,这将在后续的章节中做进一步的说明,这里将主要说说后台页面是如何工作的。

在Manifest中指定background域可以使扩展常驻后台。background可以包含三种属性,分别是scriptspagepersistent。如果指定了scripts属性,则Chrome会在扩展启动时自动创建一个包含所有指定脚本的页面;如果指定了page属性,则Chrome会将指定的HTML文件作为后台页面运行。通常我们只需要使用scripts属性即可,除非在后台页面中需要构建特殊的HTML——但一般情况下后台页面的HTML我们是看不到的。persistent属性定义了常驻后台的方式——当其值为true时,表示扩展将一直在后台运行,无论其是否正在工作;当其值为false时,表示扩展在后台按需运行,这就是Chrome后来提出的Event Page。Event Page可以有效减小扩展对内存的消耗,如非必要,请将persistent设置为falsepersistent的默认值为true


接下来写个Demo体验一下。

Demo描述: 用户可在选项页面添加关键词,后台脚本会自动运行,监测用户浏览的所有网页。当页面加载完成时,它会向网页注入一个脚本,遍历页面文本内容,使用高级的 DOM 操作技巧,精确查找用户设置的关键词。一旦找到,会使用Range对象将关键词用黄色高亮显示,同时不破坏原文档结构。

Demo效果: 可以看到在设置关键词之后,浏览掘金首页,跟关键词匹配上的文字都黄色高亮显示了。看下面代码就可以发现,其实就是对页面的Dom进行匹配,匹配上了之后将文字替换成带有黄色高亮的<span>

1737428630577.gif

Demo源码:

1. 创建项目结构

.
├── background
│   └── background.js
├── manifest.json
├── options
│   ├── options.html
│   └── options.js
└── popup
    ├── popup.html
    └── popup.js

image.png

2.manifest.json

{
    "manifest_version": 3,
    "name": "关键词高亮提醒插件",
    "version": "1.0",
    "description": "一个能在网页中高亮显示用户设定关键词的Chrome插件",
    "permissions": [
        "storage",
        "tabs",
        "activeTab",
        "scripting"
    ],
    "host_permissions": [
        "https://juejin.cn/*",
        "http://*/*",
        "https://*/*"
    ],
    "action": {
        "default_popup": "./popup/popup.html"
    },
    "options_page": "./options/options.html",
    "background": {
        "service_worker": "./background/background.js"
    }
}

3.popup.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>关键词高亮提醒</title>
    <style>
        body {
            width: 300px;
            padding: 20px;
            font-family: Arial, sans-serif;
            text-align: center;
            background-color: #f5f5f5;
        }
        h1 {
            color: #333;
            font-size: 18px;
            margin-bottom: 15px;
        }
        p {
            color: #666;
            font-size: 14px;
            margin-bottom: 20px;
        }
        button {
            background-color: #4CAF50;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #45a049;
        }
    </style>
</head>
<body>
    <h1>关键词高亮提醒插件</h1>
    <p>点击打开选项页面设置关键词</p>
    <button id="openOptions">打开选项页面</button>
    <script src="popup.js"></script>
</body>
</html>

4. popup.js

// popup.js
document.getElementById('openOptions').addEventListener('click', function() {
    if (chrome.runtime.openOptionsPage) {
        chrome.runtime.openOptionsPage();
    } else {
        window.open(chrome.runtime.getURL('options.html'));
    }
});

5. options.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>关键词设置</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            width: 800px;
            margin: 0 auto;
            padding: 40px;
            background-color: #f5f5f5;
        }
        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 30px;
        }
        .input-container {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
            justify-content: center;
        }
        input {
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            width: 300px;
            font-size: 14px;
        }
        button {
            padding: 10px 20px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #45a049;
        }
        #keywordList {
            list-style: none;
            padding: 0;
            margin: 0;
        }
        #keywordList li {
            background: white;
            margin: 10px 0;
            padding: 15px;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .delete-btn {
            background-color: #f44336;
            padding: 5px 10px;
        }
        .delete-btn:hover {
            background-color: #da190b;
        }
    </style>
</head>
<body>
    <h1>设置关键词</h1>
    <div class="input-container">
        <input type="text" id="keywordInput" placeholder="请输入要监控的关键词">
        <button id="addKeyword">添加关键词</button>
    </div>
    <ul id="keywordList"></ul>
    <script src="options.js"></script>
</body>
</html>

6. options.js

// options.js
document.getElementById('addKeyword').addEventListener('click', function() {
    const keyword = document.getElementById('keywordInput').value;
    if (keyword) {
        chrome.storage.local.get(['keywords'], function(result) {
            let keywords = result.keywords || [];
            keywords.push(keyword);
            chrome.storage.local.set({ keywords: keywords }, function() {
                displayKeywords();
                document.getElementById('keywordInput').value = '';
            });
        });
    }
});

function displayKeywords() {
    document.getElementById('keywordList').innerHTML = '';
    chrome.storage.local.get(['keywords'], function(result) {
        const keywords = result.keywords || [];
        keywords.forEach(function(keyword, index) {
            const li = document.createElement('li');
            li.textContent = keyword;
            const deleteButton = document.createElement('button');
            deleteButton.textContent = '删除';
            deleteButton.addEventListener('click', function() {
                chrome.storage.local.get(['keywords'], function(result) {
                    let keywords = result.keywords || [];
                    keywords.splice(index, 1);
                    chrome.storage.local.set({ keywords: keywords }, displayKeywords);
                });
            });
            li.appendChild(deleteButton);
                document.getElementById('keywordList').appendChild(li);
            });
    });
}

displayKeywords();

7. background.js(后台脚本)

chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
    if (changeInfo.status === 'complete') {
        chrome.storage.local.get(['keywords'], function(result) {
            const keywords = result.keywords || [];
            if (keywords.length > 0) {
                chrome.scripting.executeScript({
                    target: { tabId: tabId },
                    func: highlightKeywords,
                    args: [keywords]  // 传递参数给函数
                });
            }
        });
    }
});

// 定义要注入的函数
function highlightKeywords(keywords) {
    keywords.forEach(function (keyword) {
        const regex = new RegExp(keyword, 'gi');
        const elements = document.querySelectorAll('body *');
        elements.forEach(function (element) {
            if (element.childNodes.length === 1 && element.childNodes[0].nodeType === Node.TEXT_NODE) {
                const node = element.childNodes[0];
                let match;
                while ((match = regex.exec(node.textContent))!== null) {
                    const range = document.createRange();
                    range.setStart(node, match.index);
                    range.setEnd(node, match.index + match[0].length);
                    const span = document.createElement('span');
                    span.style.backgroundColor = 'yellow';
                    span.textContent = match[0];
                    range.surroundContents(span);
                    // 重置 lastIndex,确保连续匹配
                    regex.lastIndex = 0;
                }
            }
        });
    });
}

// 监听标签页激活事件
chrome.tabs.onActivated.addListener(function(activeInfo) {
    // 获取当前激活的标签页信息
    chrome.tabs.get(activeInfo.tabId, function(tab) {
        // 检查标签页是否已加载完成
        if (tab.status === 'complete') {
            // 从本地存储中获取关键词
            chrome.storage.local.get(['keywords'], function(result) {
                const keywords = result.keywords || [];
                // 如果关键词数组不为空
                if (keywords.length > 0) {
                    // 在当前标签页中执行高亮关键词的脚本
                    chrome.scripting.executeScript({
                        target: { tabId: activeInfo.tabId },
                        func: highlightKeywords,
                        args: [keywords] // 传递关键词数组作为参数
                    });
                }
            });
        }
    });
});

// 监听新标签页创建事件
chrome.tabs.onCreated.addListener(function(tab) {
    // 从本地存储中获取关键词
    chrome.storage.local.get(['keywords'], function(result) {
        const keywords = result.keywords || []; // 获取关键词数组,如果不存在则为一个空数组
        // 如果关键词数组不为空
        if (keywords.length > 0) {
            // 在新创建的标签页中执行高亮关键词的脚本
            chrome.scripting.executeScript({
                target: { tabId: tab.id }, // 指定目标标签页
                func: highlightKeywords, // 要执行的函数
                args: [keywords] // 传递关键词数组作为参数
            });
        }
    });
});

// 监听标签页高亮事件
chrome.tabs.onHighlighted.addListener(function(highlightInfo) {
    // 遍历所有高亮的标签页ID
    highlightInfo.tabIds.forEach(function(tabId) {
        // 获取当前标签页的信息
        chrome.tabs.get(tabId, function(tab) {
            // 检查标签页是否已加载完成
            if (tab.status === 'complete') {
                // 从本地存储中获取关键词
                chrome.storage.local.get(['keywords'], function(result) {
                    const keywords = result.keywords || []; // 获取关键词数组,如果不存在则为一个空数组
                    // 如果关键词数组不为空
                    if (keywords.length > 0) {
                        // 在高亮的标签页中执行高亮关键词的脚本
                        chrome.scripting.executeScript({
                            target: { tabId: tabId }, // 指定目标标签页
                            func: highlightKeywords, // 要执行的函数
                            args: [keywords] // 传递关键词数组作为参数
                        });
                    }
                });
            }
        });
    });
});

// 监听标签页被替换事件
chrome.tabs.onReplaced.addListener(function(addedTabId, removedTabId) {
    // 从本地存储中获取关键词
    chrome.storage.local.get(['keywords'], function(result) {
        const keywords = result.keywords || []; // 获取关键词数组,如果不存在则为一个空数组
        // 如果关键词数组不为空
        if (keywords.length > 0) {
            // 在新替换的标签页中执行高亮关键词的脚本
            chrome.scripting.executeScript({
                target: { tabId: addedTabId }, // 指定目标标签页
                func: highlightKeywords, // 要执行的函数
                args: [keywords] // 传递关键词数组作为参数
            });
        }
    });
});

8.安装插件

  • 打开 Chrome 浏览器,访问 chrome://extensions/
  • 开启开发者模式(右上角的开关)。
  • 点击 “加载已解压的扩展程序”,选择包含上述文件的文件夹。

这样,当用户点击插件图标打开弹出窗口,再点击 “获取跨域图片” 按钮时,插件就会通过跨域请求获取图片并显示出来,从而演示跨域请求的过程。

image.png


image.png


好了,可以看看效果了。只要设置好了关键词,随便进入哪个页面,都会把关键词标记为高亮黄色,并且还是静默运行,用户感知很低。

9.代码解释:

  • manifest.json

    • manifest_version: 遵循 Chrome 扩展的 Manifest V3 规范。
    • permissions: 声明所需的权限,包括存储、标签页、通知和活动标签页的权限。
    • action.default_popup: 定义了弹出窗口的 HTML 文件。
    • options_page: 定义了用户设置关键词的选项页面。
    • background.service_worker: 定义了后台服务脚本。
  • popup.html 和 popup.js

    • 提供一个按钮,点击后可打开选项页面。
  • options.html 和 options.js

    • 允许用户输入关键词,添加和删除关键词,将关键词存储在chrome.storage.local中。
  • background.js

    • 包含多个事件监听器:

      • chrome.tabs.onUpdated: 监听标签页更新,当页面更新完成时,根据存储的关键词列表注入脚本对页面内容进行关键词高亮。
      • chrome.tabs.onActivated: 监听标签页激活,对激活的页面进行关键词高亮处理。
      • chrome.tabs.onCreated: 监听新标签页创建,对新页面进行关键词高亮处理。
      • chrome.tabs.onHighlighted: 监听标签页高亮,对高亮的页面进行关键词高亮处理。
      • chrome.tabs.onReplaced: 监听标签页替换,对替换后的页面进行关键词高亮处理。
  • 关键词高亮代码片段

    • 通过chrome.tabs.executeScript将一个函数注入到页面中,该函数使用RegExp将关键词在页面中进行全局不区分大小写的查找,并将找到的关键词使用<span style="background-color: yellow;">包裹,实现高亮。

有个小问题: chrome.tabs.onHighlighted事件就是chrome.tabs.onActivated。这俩功能是一样的?为什么还要写两个,他们的不同是什么?

chrome.tabs.onHighlighted 和 chrome.tabs.onActivated 这两个事件虽然在某些情况下可以实现类似的功能,但它们的触发条件和使用场景是不同的。

主要区别:

1. 触发条件:

  • chrome.tabs.onActivated:当用户激活一个标签页时触发。这个事件只会在用户直接点击标签页或使用键盘快捷键切换标签页时触发。

  • chrome.tabs.onHighlighted:当用户高亮(选择)一个或多个标签页时触发。这个事件可以在用户通过鼠标拖动选择多个标签页时触发。

  • 使用场景:

  • chrome.tabs.onActivated 更适合用于处理单个标签页的激活事件,比如当用户切换到某个特定标签页时需要执行某些操作。

  • chrome.tabs.onHighlighted 更适合用于处理多个标签页的选择事件,比如当用户选择了一组标签页时,可以对这些标签页执行相同的操作。


4.带选项页面的扩展

想象一下,在江湖中,大侠们拥有一个神奇的 “百变如意囊”,可以根据不同的情况变幻出各种所需之物,满足各种需求。在 Chrome 扩展的世界里,这一技能也有着异曲同工之妙, 带选项页面的扩展就如同这个神奇的 “百变如意囊”,它能为我们的扩展增添更多的灵活性和个性化。

既然扩展允许用户进行个性化设置,就需要向用户提供一个选项页面。Chrome通过Manifest文件的options_page属性为开发者提供了这样的接口,可以为扩展指定一个选项页面。当用户在扩展图标上点击右键,选择菜单中的“选项”后,就会打开这个页面。

对于网站来说,用户的设置通常保存在Cookies中,或者保存在网站服务器的数据库中。对于JavaScript来说,一些数据可以保存在变量中,但如果用户重新启动浏览器,这些数据就会消失。那么如何在扩展中保存用户的设置呢?我们可以使用HTML5新增的localStorage接口。除了localStorage接口以外,还可以使用其他的储存方法。后面将专门拿出一节来说数据存储,这里我们先使用最简单的localStorage方法储存数据。

localStorage是HTML5新增的方法,它允许JavaScript在用户计算机硬盘上永久储存数据(除非用户主动删除)。但localStorage也有一些限制,首先是localStorage和Cookies类似,都有域的限制,运行在不同域的JavaScript无法调用其他域localStorage的数据;其次是单个域在localStorage中存储数据的大小通常有限制(虽然W3C没有给出限制),对于Chrome这个限制是5MB2;最后localStorage只能储存字符串型的数据,无法保存数组和对象,但可以通过jointoStringJSON.stringify等方法先转换成字符串再储存。

带选项页面的扩展,其实在上一Demo: 常驻后台 中用到了,就是options.htmloptions.js, 这里的Demo,偷个小懒,还是用上面的改变背景颜色, 上一个Demo已经跟着做了一遍的就不用再做了,完全是一样的,这里为了文章的完整性,还是复制粘贴一下。

Demo描述: 用户可在选项页面添加关键词,后台脚本会自动运行,监测用户浏览的所有网页。当页面加载完成时,它会向网页注入一个脚本,遍历页面文本内容,使用高级的 DOM 操作技巧,精确查找用户设置的关键词。一旦找到,会使用Range对象将关键词用黄色高亮显示,同时不破坏原文档结构。

Demo效果: 可以看到在设置关键词之后,浏览掘金首页,跟关键词匹配上的文字都黄色高亮显示了。看下面代码就可以发现,其实就是对页面的Dom进行匹配,匹配上了之后将文字替换成带有黄色高亮的<span>

1737428630577.gif

Demo源码:

1. 创建项目结构

.
├── background
│   └── background.js
├── manifest.json
├── options
│   ├── options.html
│   └── options.js
└── popup
    ├── popup.html
    └── popup.js

image.png

2.manifest.json

{
    "manifest_version": 3,
    "name": "关键词高亮提醒插件",
    "version": "1.0",
    "description": "一个能在网页中高亮显示用户设定关键词的Chrome插件",
    "permissions": [
        "storage",
        "tabs",
        "activeTab",
        "scripting"
    ],
    "host_permissions": [
        "https://juejin.cn/*",
        "http://*/*",
        "https://*/*"
    ],
    "action": {
        "default_popup": "./popup/popup.html"
    },
    "options_page": "./options/options.html",
    "background": {
        "service_worker": "./background/background.js"
    }
}

3.popup.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>关键词高亮提醒</title>
    <style>
        body {
            width: 300px;
            padding: 20px;
            font-family: Arial, sans-serif;
            text-align: center;
            background-color: #f5f5f5;
        }
        h1 {
            color: #333;
            font-size: 18px;
            margin-bottom: 15px;
        }
        p {
            color: #666;
            font-size: 14px;
            margin-bottom: 20px;
        }
        button {
            background-color: #4CAF50;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #45a049;
        }
    </style>
</head>
<body>
    <h1>关键词高亮提醒插件</h1>
    <p>点击打开选项页面设置关键词</p>
    <button id="openOptions">打开选项页面</button>
    <script src="popup.js"></script>
</body>
</html>

4. popup.js

// popup.js
document.getElementById('openOptions').addEventListener('click', function() {
    if (chrome.runtime.openOptionsPage) {
        chrome.runtime.openOptionsPage();
    } else {
        window.open(chrome.runtime.getURL('options.html'));
    }
});

5. options.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>关键词设置</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            width: 800px;
            margin: 0 auto;
            padding: 40px;
            background-color: #f5f5f5;
        }
        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 30px;
        }
        .input-container {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
            justify-content: center;
        }
        input {
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            width: 300px;
            font-size: 14px;
        }
        button {
            padding: 10px 20px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #45a049;
        }
        #keywordList {
            list-style: none;
            padding: 0;
            margin: 0;
        }
        #keywordList li {
            background: white;
            margin: 10px 0;
            padding: 15px;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .delete-btn {
            background-color: #f44336;
            padding: 5px 10px;
        }
        .delete-btn:hover {
            background-color: #da190b;
        }
    </style>
</head>
<body>
    <h1>设置关键词</h1>
    <div class="input-container">
        <input type="text" id="keywordInput" placeholder="请输入要监控的关键词">
        <button id="addKeyword">添加关键词</button>
    </div>
    <ul id="keywordList"></ul>
    <script src="options.js"></script>
</body>
</html>

6. options.js

// options.js
document.getElementById('addKeyword').addEventListener('click', function() {
    const keyword = document.getElementById('keywordInput').value;
    if (keyword) {
        chrome.storage.local.get(['keywords'], function(result) {
            let keywords = result.keywords || [];
            keywords.push(keyword);
            chrome.storage.local.set({ keywords: keywords }, function() {
                displayKeywords();
                document.getElementById('keywordInput').value = '';
            });
        });
    }
});

function displayKeywords() {
    document.getElementById('keywordList').innerHTML = '';
    chrome.storage.local.get(['keywords'], function(result) {
        const keywords = result.keywords || [];
        keywords.forEach(function(keyword, index) {
            const li = document.createElement('li');
            li.textContent = keyword;
            const deleteButton = document.createElement('button');
            deleteButton.textContent = '删除';
            deleteButton.addEventListener('click', function() {
                chrome.storage.local.get(['keywords'], function(result) {
                    let keywords = result.keywords || [];
                    keywords.splice(index, 1);
                    chrome.storage.local.set({ keywords: keywords }, displayKeywords);
                });
            });
            li.appendChild(deleteButton);
                document.getElementById('keywordList').appendChild(li);
            });
    });
}

displayKeywords();

7. background.js(后台脚本)

chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
    if (changeInfo.status === 'complete') {
        chrome.storage.local.get(['keywords'], function(result) {
            const keywords = result.keywords || [];
            if (keywords.length > 0) {
                chrome.scripting.executeScript({
                    target: { tabId: tabId },
                    func: highlightKeywords,
                    args: [keywords]  // 传递参数给函数
                });
            }
        });
    }
});

// 定义要注入的函数
function highlightKeywords(keywords) {
    keywords.forEach(function (keyword) {
        const regex = new RegExp(keyword, 'gi');
        const elements = document.querySelectorAll('body *');
        elements.forEach(function (element) {
            if (element.childNodes.length === 1 && element.childNodes[0].nodeType === Node.TEXT_NODE) {
                const node = element.childNodes[0];
                let match;
                while ((match = regex.exec(node.textContent))!== null) {
                    const range = document.createRange();
                    range.setStart(node, match.index);
                    range.setEnd(node, match.index + match[0].length);
                    const span = document.createElement('span');
                    span.style.backgroundColor = 'yellow';
                    span.textContent = match[0];
                    range.surroundContents(span);
                    // 重置 lastIndex,确保连续匹配
                    regex.lastIndex = 0;
                }
            }
        });
    });
}

// 监听标签页激活事件
chrome.tabs.onActivated.addListener(function(activeInfo) {
    // 获取当前激活的标签页信息
    chrome.tabs.get(activeInfo.tabId, function(tab) {
        // 检查标签页是否已加载完成
        if (tab.status === 'complete') {
            // 从本地存储中获取关键词
            chrome.storage.local.get(['keywords'], function(result) {
                const keywords = result.keywords || [];
                // 如果关键词数组不为空
                if (keywords.length > 0) {
                    // 在当前标签页中执行高亮关键词的脚本
                    chrome.scripting.executeScript({
                        target: { tabId: activeInfo.tabId },
                        func: highlightKeywords,
                        args: [keywords] // 传递关键词数组作为参数
                    });
                }
            });
        }
    });
});

// 监听新标签页创建事件
chrome.tabs.onCreated.addListener(function(tab) {
    // 从本地存储中获取关键词
    chrome.storage.local.get(['keywords'], function(result) {
        const keywords = result.keywords || []; // 获取关键词数组,如果不存在则为一个空数组
        // 如果关键词数组不为空
        if (keywords.length > 0) {
            // 在新创建的标签页中执行高亮关键词的脚本
            chrome.scripting.executeScript({
                target: { tabId: tab.id }, // 指定目标标签页
                func: highlightKeywords, // 要执行的函数
                args: [keywords] // 传递关键词数组作为参数
            });
        }
    });
});

// 监听标签页高亮事件
chrome.tabs.onHighlighted.addListener(function(highlightInfo) {
    // 遍历所有高亮的标签页ID
    highlightInfo.tabIds.forEach(function(tabId) {
        // 获取当前标签页的信息
        chrome.tabs.get(tabId, function(tab) {
            // 检查标签页是否已加载完成
            if (tab.status === 'complete') {
                // 从本地存储中获取关键词
                chrome.storage.local.get(['keywords'], function(result) {
                    const keywords = result.keywords || []; // 获取关键词数组,如果不存在则为一个空数组
                    // 如果关键词数组不为空
                    if (keywords.length > 0) {
                        // 在高亮的标签页中执行高亮关键词的脚本
                        chrome.scripting.executeScript({
                            target: { tabId: tabId }, // 指定目标标签页
                            func: highlightKeywords, // 要执行的函数
                            args: [keywords] // 传递关键词数组作为参数
                        });
                    }
                });
            }
        });
    });
});

// 监听标签页被替换事件
chrome.tabs.onReplaced.addListener(function(addedTabId, removedTabId) {
    // 从本地存储中获取关键词
    chrome.storage.local.get(['keywords'], function(result) {
        const keywords = result.keywords || []; // 获取关键词数组,如果不存在则为一个空数组
        // 如果关键词数组不为空
        if (keywords.length > 0) {
            // 在新替换的标签页中执行高亮关键词的脚本
            chrome.scripting.executeScript({
                target: { tabId: addedTabId }, // 指定目标标签页
                func: highlightKeywords, // 要执行的函数
                args: [keywords] // 传递关键词数组作为参数
            });
        }
    });
});

8.安装插件

  • 打开 Chrome 浏览器,访问 chrome://extensions/
  • 开启开发者模式(右上角的开关)。
  • 点击 “加载已解压的扩展程序”,选择包含上述文件的文件夹。

这样,当用户点击插件图标打开弹出窗口,再点击 “获取跨域图片” 按钮时,插件就会通过跨域请求获取图片并显示出来,从而演示跨域请求的过程。

image.png


image.png


好了,可以看看效果了。

可以看到,点击【打开选项页面】

image.png


这里就是【选项页面】:

image.png


9.代码解释:

  • manifest.json

    • manifest_version: 遵循 Chrome 扩展的 Manifest V3 规范。
    • permissions: 声明所需的权限,包括存储、标签页、通知和活动标签页的权限。
    • action.default_popup: 定义了弹出窗口的 HTML 文件。
    • options_page: 定义了用户设置关键词的选项页面。
    • background.service_worker: 定义了后台服务脚本。
  • popup.html 和 popup.js

    • 提供一个按钮,点击后可打开选项页面。
  • options.html 和 options.js

    • 允许用户输入关键词,添加和删除关键词,将关键词存储在chrome.storage.local中。
  • background.js

    • 包含多个事件监听器:

      • chrome.tabs.onUpdated: 监听标签页更新,当页面更新完成时,根据存储的关键词列表注入脚本对页面内容进行关键词高亮。
      • chrome.tabs.onActivated: 监听标签页激活,对激活的页面进行关键词高亮处理。
      • chrome.tabs.onCreated: 监听新标签页创建,对新页面进行关键词高亮处理。
      • chrome.tabs.onHighlighted: 监听标签页高亮,对高亮的页面进行关键词高亮处理。
      • chrome.tabs.onReplaced: 监听标签页替换,对替换后的页面进行关键词高亮处理。
  • 关键词高亮代码片段

    • 通过chrome.tabs.executeScript将一个函数注入到页面中,该函数使用RegExp将关键词在页面中进行全局不区分大小写的查找,并将找到的关键词使用<span style="background-color: yellow;">包裹,实现高亮。

5.扩展页面间的通信

在这纷繁复杂的江湖之中,信息的传递至关重要。就如同武林高手们需要互通消息,协同作战一样,Chrome 扩展中的不同页面之间也需要密切配合,这时候 “传音入密术” 便发挥了巨大的作用, 此术可以让我们的扩展页面之间悄无声息地传递信息,就像高手们使用传音入密的功夫,将重要的消息准确无误地传达给对方,而旁人却无法察觉。

在 Chrome 扩展开发领域。,论是扩展内部多个页面,还是不同扩展的页面,数据传输能让它们及时获取彼此状态。

以音乐播放器扩展为例,当用户在 popup 页面点击音乐列表,就触发了一系列通信需求。此时,popup 页面需将用户指令精准传达给后台页面,后台页面接收到指令后,随即启动相应音乐的播放流程。

Chrome 为满足此类通信需求,提供了 4 个关键接口:runtime.sendMessageruntime.onMessageruntime.connectruntime.onConnect

考虑到本教程的入门特性,这里着重说说runtime.sendMessageruntime.onMessage。而runtime.connectruntime.onConnect作为进阶接口,适合有更高需求的开发者深入探索。若想获取这两个接口的详尽官方文档,可访问:developer.chrome.com/extensions/… 。

值得特别指出的是,Chrome 众多 API 中,多数无法在content_scripts中运行,但runtime.sendMessageruntime.onMessage是难得的例外。这一特性,让扩展的其他页面与content_scripts之间实现了顺畅通信。

下面,我们深入剖析这两个接口的具体使用方法:

1. runtime.sendMessage

该方法的完整形式为:

chrome.runtime.sendMessage(extensionId, message, options, callback)
  • extensionId:明确消息发送的目标扩展。若未设置该参数,默认发送至发起消息的扩展自身。
  • message:承载实际发送的内容,数据类型无限制。可以是简单的字符串,如'Hello';也能是复杂的对象,像{action: 'play'};甚至是数字2013,或者数组['Jim', 'Tom', 'Kate'] 等。
  • options:这是一个对象类型参数,包含一个布尔型属性includeTlsChannelId。该属性决定扩展在发起消息时,是否将 TLS 通道 ID 发送给监听消息的外部扩展。不过,此属性仅在扩展与网页间通信时才会用到。若对 TLS 相关技术不太熟悉,可忽略这一属性,因为options本身是可选参数。
  • callback:作为回调函数,用于接收消息发送后的返回结果,同样属于可选参数。

2. runtime.onMessage

此方法的完整形式为:

chrome.runtime.onMessage.addListener(callback)

这里的callback是必填项,作为回调函数,它会接收三个参数:

  • message:即消息的具体内容。

  • sender:包含消息发送者的相关信息,其对象具有 4 个属性:

    • tab:代表发起消息的标签,关于标签的详细内容,可查阅 4.5 节。
    • id:发送者的唯一标识。
    • url:发送者所处的页面地址。
    • tlsChannelId:TLS 通道标识。
  • sendResponse:用于对消息发送者做出响应的函数。

随我做一个小Demo体验一下:

Demo描述:

此 Demo 展示了 Chrome 扩展中的简单消息传递机制。包含popup.htmlbackground.js两部分。在popup.html的脚本popup.js中,当页面加载完成,会向后台发送'Hello'消息。而background.js中使用chrome.runtime.onMessage.addListener监听消息,若收到'Hello',将回复'Hello from background.'。通过chrome.runtime.sendMessagechrome.runtime.onMessage.addListener接口,实现了popup页面和后台页面之间的消息交互,让用户体验 Chrome 扩展页面间的通信功能。

Demo效果:

image.png

Demo源码:

1. 创建项目结构

sendMessage-demo/
├── background
│   └── background.js
├── manifest.json
└── popup
    ├── popup.html
    └── popup.js

image.png

2. manifest.json

{
    "manifest_version": 3,
    "name": "Runtime Message Demo",
    "version": "1.0",
    "description": "A simple Chrome extension to demonstrate runtime message passing",
    "permissions": [],
    "action": {
        "default_popup": "./popup/popup.html"
    },
    "background": {
        "service_worker": "./background/background.js"
    }
}

3. popup.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Popup Page</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f0f0f0;
            margin: 0;
            padding: 20px;
            text-align: center;
        }
        h1 {
            color: #333;
            margin-bottom: 20px;
        }
        .container {
            background-color: #fff;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            padding: 20px;
            display: inline-block;
        }
        .message {
            font-size: 18px;
            color: #555;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Runtime Message Demo</h1>
        <div class="message" id="responseMessage"></div>
    </div>
    <script src="popup.js"></script>
</body>
</html>

4. popup.js

// popup.js
document.addEventListener('DOMContentLoaded', function() {
    chrome.runtime.sendMessage('Hello', function(response) {
        document.write(response);
    });
});

5. background.js

// background.js
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
    if (message === 'Hello') {
        sendResponse('Hello from background.');
    }
    // 必须返回 true 以表示你将异步调用 sendResponse
    return true;
});

代码解释:

  • manifest.json

    • manifest_version: 3 表示使用 Chrome 扩展的 Manifest V3 版本。
    • action.default_popup: 指定 popup.html 作为点击扩展图标时弹出的页面。
    • background.service_worker: 指定 background.js 作为后台服务脚本。
  • popup.html

    • 包含一个简单的页面结构,引用了 popup.js 脚本。
  • popup.js

    • document.addEventListener('DOMContentLoaded', function() {...}): 确保在页面内容加载完成后执行代码。
    • chrome.runtime.sendMessage('Hello', function(response) {...}): 向后台发送消息,消息内容为 'Hello',并定义一个回调函数,该回调函数接收后台的响应,并使用 document.write 显示在页面上。
  • background.js

    • chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {...}): 监听来自扩展其他部分的消息。
    • if (message === 'Hello') {...}: 当收到消息内容为 'Hello' 时,使用 sendResponse 发送响应 'Hello from background.'
    • return true: 因为 sendResponse 可能会异步调用,所以需要返回 true,表示你会在后续异步调用 sendResponse,否则 sendResponse 会在监听器函数执行完毕后失效。

6.安装插件

  • 打开 Chrome 浏览器,访问 chrome://extensions/
  • 开启开发者模式(右上角的开关)。
  • 点击 “加载已解压的扩展程序”,选择包含上述文件的文件夹。

image.png


image.png

6.存储数据

在江湖中,大侠们常常需要一个能收纳各种宝贝和秘籍的神奇法宝,而 “乾坤储物戒” 就是这样的存在。它拥有着广阔的空间,可将各种物品收纳其中,方便我们随时取用。在 Chrome 扩展的世界里,“乾坤储物戒” 也就是存储数据的技能,同样至关重要,我们可以使用它来存储各种信息,无论是用户的偏好设置、重要的数据,还是在江湖闯荡时收集的各种资料,都可以被安全地保存在这神奇的 “储物戒” 中。

通常来说,Chrome扩展可以采用以下三种方法中的一种来储存数据:

  1. 使用HTML5的localStorage。这种方法相对简单,可以看作是特殊的JavaScript变量,适用于保存“设置”这类简单的数据。

  2. 使用Chrome提供的存储API。这种方法可以保存任意类型的数据,但需要异步调用Chrome的API,结果需要使用回调函数接收。它适用于结构稍微复杂一些的数据。

  3. 使用Web SQL Database。这种方法需要使用SQL语句对数据库进行读写操作,相对复杂,但对于数据量庞大的应用来说是个不错的选择。

开发者应根据实际的情况选择上述三种方法中的一种或几种来存储扩展中的数据。由于我在上面小节中已经详细说了localStorage的使用方法,所以下面我将重点介绍后两种储存数据的方法。

Chrome存储API

Chrome为扩展应用提供了存储API,以便将扩展中需要保存的数据写入本地磁盘。Chrome提供的存储API可以说是对localStorage的改进,它与localStorage相比有以下区别:

  • 如果储存区域指定为sync,数据可以自动同步;
  • content_scripts可以直接读取数据,而不必通过background页面;
  • 在隐身模式下仍然可以读出之前存储的数据;
  • 读写速度更快;
  • 用户数据可以以对象的类型保存。

使用Chrome存储API必须要在Manifest的permissions中声明"storage",之后才有权限调用。Chrome存储API提供了2种储存区域,分别是sync和local。两种储存区域的区别在于,sync储存的区域会根据用户当前在Chrome上登陆的Google账户自动同步数据,当无可用网络连接可用时,sync区域对数据的读写和local区域对数据的读写行为一致。

Chrome同时还为存储API提供了一个onChanged事件,当存储区的数据发生改变时,这个事件会被激发。

Web SQL Database

Web SQL Database的三个核心方法为openDatabase、transaction和executeSql。openDatabase方法的作用是与数据库建立连接,transaction方法的作用是执行查询,executeSql方法的作用是执行SQL语句。

关于Web SQL Database的更多信息,你可以参考这里。由于原生的Web SQL Database并不算好用,也有一些开源的二次封装的库来简化Web SQL Database的使用,如html5sql

需要注意的是,以上几种数据的存储方式都不会对数据加密,如果储存的是敏感的数据,应该先进行加密处理。比如不要将用户密码的明码直接储存,而应先进行MD5加密。

对比:

存储方式描述适用场景
HTML5的localStorage操作简单,类似于特殊的JavaScript变量保存简单的数据,如“设置”等
Chrome提供的存储API可保存任意类型的数据,需要异步调用,结果需要使用回调函数接收适用于结构稍微复杂一些的数据
Web SQL Database需要使用SQL语句进行数据库读写操作,相对复杂数据量庞大的应用

总结

从 “操作用户正在浏览的页面” 的 “幻影迷踪手”,到 “跨域请求” 的 “天涯咫尺步”,再到 “常驻后台” 的 “隐世守护诀”,“带选项页面的扩展” 的 “百变如意囊”,“扩展页面间的通信” 的 “传音入密术”,以及 “存储数据” 的 “乾坤储物戒”,我们逐步掌握了一系列实用而强大的技能。这些技能如同武林秘籍中的各种绝学,既可以单独施展,展现独特威力,又能相互配合,形成一套完整的武功体系,助你在 Chrome 扩展的开发江湖中纵横驰骋。

愿各位大侠在今后的开发之路上,灵活运用这些技能,不断创新,打造出更加出色的 Chrome 扩展,在数字江湖中留下属于自己的传奇故事。

CACB8ED2053101A53C3AE64666A7E509.jpg

「 ✨ 致谢」

希望这篇关于 Chrome 扩展开发的文章能够为友友们提供一定的帮助和启发。若友友们认为此篇文章具有点价值,求点赞支持一下吧 👍💕🌹,同时,欢迎友友们在评论区留下宝贵的想法和问题 💬❤️🌹。

本专栏上一篇文章《Chrome 插件开发:构建插件与调试知识全掌握 🚀》

1065A02E8C99F17B78DE62C61AC9638C.jpg

参考书籍📚

Chrome扩展及应用开发(首发版)