【实战】Chrome V3插件开发,只看这一篇文章就够了

18,933 阅读14分钟

我正在参加「掘金·启航计划」

Chrome插件开发

Chrome插件是一种方便实用的浏览器扩展程序,可以增强用户的浏览器体验

前言

大家好、今天带大家一起学习Chrome插件开发的知识。基于Manifest V3版本的;

本篇文章,是一个插件开发的实战教程,通过实战,来学习Chrome插件开发的相关知识。

通过这篇文章,我们可以学习很多Chrome插件开发的知识:

  • 插件开发流程
  • 插件Manifest V3的配置
  • 内容脚本content_scripts相关知识
  • 插件配置页面options_page相关开发
  • 后台页面background(service_worker)相关知识
  • chrome.action,如:downloads
  • 插件数据存储机制
  • 消息通信机制
  • 右键菜单
  • 徽章配置

前置知识

在开始开发Chrome插件前,我们需要掌握一些基础知识,包括:

  • HTML、CSS、JavaScript的基础语法和操作
  • 插件开发的基础知识。比如:Manifest

接下来跟大家一起开发吧。

功能介绍

我们要基于Manifest V3来开发这个插件。因为V2即将要遗弃了。

开始吧!

首先,给插件取个响亮的名字:DragonFly(飞龙在天)

当然,如果只是是名字好听,是没啥用的,得有内涵。它有以下功能。

  1. 显示图片信息
  2. 图片下载
  3. 支持批量下载
  4. 支持图片过滤配置
  5. 显示图片数量

这里再介绍一下这个插件的应用场景。

  1. 安装插件
  2. 打开网页
  3. 插件初始化,注入脚本
  4. 脚本中获取img元素,给元素添加mouseover事件,用于显示图片信息(这里有问题,当img元素被其他元素遮住的时候,则获取不到这个元素)
  5. 同时也给img元素添加dragend事件
  6. 调用下载api进行下载图片

功能已经很丰富了,接下来我们开发了。

如果你还不会插件的开发知识,没关系,跟着我来一起学习

开始开发

新建项目

既然是开发工作,肯定得先建一个插件项目,怎么建呢?很简单,就是随便建一个文件夹。

mkdir dragon-fly

ok。项目建好了。

接下来就是配置项目。配置项目呢,就得了解一下manifest.json配置文件

项目配置

接下来,编写一个manifest.json文件来描述你的插件。

这个文件必须放在插件的根目录中。也就是dragon-fly目录下。

{
  "manifest_version": 3,
  "name": "DragonFly(飞龙在天)",
  "version": "1.0.0",
  "description": "一个非常高大上的图片下载插件",
  "icons": {
    "16": "icon16.png",
    "32": "icon32.png",
    "48": "icon48.png",
    "128": "icon128.png"
  }
}

我们逐一解释每个属性:

  • manifest_version: 描述manifest文件的版本号,必须为3。
  • name: 描述插件的名称。这个属性就是配置我们插件名称,用于显示在插件列表
  • version: 描述插件的版本号。
  • description: 描述插件的简要说明。
  • icons: 描述插件图标的大小和文件路径。

目前、我只需要这些插件配置信息就够了,只是啥功能也没有而已。

现在就可以安装一下这个插件了,感受一下吧。

本地插件的安装需要打开开发模式 在浏览器输入chrome://extensions/打开插件页面,点击右上角的开发者模式,在点击加载已解压的扩展程序,选择我们刚刚新建的那个插件项目文件夹。

可以看到插件正常显示了。

WX20230428-103928@2x.png

WX20230428-104114@2x.png

添加功能

好的,接下来,我们给插件添加第一个功能:鼠标移动到图片元素上,显示图片的信息(存储大小,真实尺寸,显示尺寸)

由于我们的插件是需要操作dom,并且不需要一直在后台运行,只需要再打开网页的时候运行。

所以我们使用内容脚本(content_scripts)的方式运行插件即可。(插件还支持后台页面,后面再说这个)

内容脚本(content_scripts)的特性:

  • 在页面打开,或者页面加载结束,或者页面空闲的时候注入
  • 共享页面dom,也就是说可以操作页面的dom
  • JS隔离的,插件中的js定义并不会影响页面的js,也不能引用页面中的js变量、函数

开始使用content_scripts:

content_scripts 有多种使用方式:

  1. 静态注入。在manifest.json文件中声明
  2. 动态注入。chrome.scripting.registerContentScripts
  3. 编码注入。chrome.scripting.executeScript

我们一般使用静态注入。

manifest.json文件中添加一下content_scripts配置:

{
    ...,
    "content_scripts": [
        {
          "matches": ["https://*/*"],
          "js": ["src/main.js"]
        }
      ],
    ...
}

以上代码就是静态声明了content_scripts

content_scripts还有动态注入的方式,其实就是通过调用api的方法来注入,如下示例代码:

chrome.scripting
  .registerContentScripts([{
    id: "session-script",
    js: ["content.js"],
    persistAcrossSessions: false,
    matches: ["*://example.com/*"],
    runAt: "document_start",
  }])
  .then(() => console.log("registration complete"))
  .catch((err) => console.warn("unexpected error", err))

动态注入可以scripts注入的时机更可控、或者可以更新、删除content_scripts。

content_scripts属性是一个数组,也就是说我们可以配置多个脚本规则,数组的每个元素包含多个属性:

  • matches 指定此内容脚本将被注入到哪些页面。必填
  • js 要注入匹配页面的 JavaScript 文件列表。选填
  • css 要注入匹配页面的 CSS 文件列表。选填
  • run_at 指定何时应将脚本注入页面。有三种类型,document_start,document_end,document_idle。默认为document_idle。选填

当然了,还有一些其他很少用到的属性,这里就不介绍了。有兴趣的可以自己查看官方文档。文章最后有官方文档链接。

显示图片信息

通过上面的配置,可以发现,脚本的路径为src/main.js,所以我们在项目根目录下新建一个src目录,然后再src目录下新建一个mian.js文件。

添加第一个功能,显示图片基本信息:

/**
 * 显示网络图片的内存大小
 * @param {*} src 
 * @returns 
 */
function getByte(src){
    return fetch(src).then(function(res){
        return res.blob()
      }).then(function(data){
        return (data.size/(1024)).toFixed(2)+'kB'
      })
}

/**
 * 基于dom的title属性来设置显示图片信息
 * @param {*} el 
 * @param {number} byte zijie
 */
function showInfo(el,byte){
    var html=`真实尺寸:${el.naturalWidth}*${el.naturalHeight}\n显示尺寸:${el.width}*${el.height}\n存储大小:${byte}`;
    el.title=html
}

/**
 * 在document上代理mouseover事件
 */
document.addEventListener('mouseover',function(e){
    //移动到图片元素上时、则显示信息
    if(e.target.tagName=='IMG'){
        getByte(e.target.src).then(byte=>{
            showInfo(e.target,byte)
        })
    }
},true)

好了,让我体验一下自己开的插件吧。

由于我们前面已经安装过插件了,所以在浏览器的插件页面直接点击插件的刷新按钮即可重新加载插件。

然后打开需要测试的网站,鼠标移动到图片上即可看到图片信息。

WX20230504-094647@2x.png

添加拖拽事件

接下来、我们给图片元素添加拖拽事件,因为是拖拽下载,所以我们只使用dragend事件即可.

main.js中添加以下代码:

/**
 * 在document上代理mouseover事件
 */
document.addEventListener('dragend',function(e){
    if(e.target.tagName=='IMG'){
        //TODO 下载
    }
})

上面代码,实现了图片的拖拽事件。接下来、要在这里实现下载功能

下载功能

实现图片下载功能有俩种方式:

  1. 在内容脚本(content_scripts)中使用原生js实现下载图片功能
  2. 使用插件chrome.downloads.downloadAPI实现下载

原生实现下载就是我们平时在页面开发中那种。在main.js中直接下载就行了。

这里由于是介绍插件开发,所以我们使用chrome.downloads.download来实现下载。

我们需要再manifest.json中添加downloads权限来使用该api。

注意,chrome的部分api不能直接在content_scripts中使用,所以我们需要一个后台页面来使用这个api来实现下载。

首先,添加权限:

{
    ...,
    "permissions": [
      "downloads"
    ],
    ...
}

添加后台页面脚本(background)配置:

{
    ...,
    "background":{
        "service_worker": "src/service_worker.js"
    },
    ...
}

通过上面的配置。

我们需要一个后台脚本(src/service_worker.js),在src目录下新建一个service_worker.js脚本文件

//service_worker.js
function download(url){
    var options={
        url:url
    }
    chrome.downloads.download(options)
}

问题来了、在content_scripts中如何调用background中的函数呢?

那就是通过页面通信的机制来调用。

页面通信

为什么需要页面通信?

由于content_scripts是在网页中运行的,而非在扩展的上下文中,因此它们通常需要某种方式与扩展的其余部分进行通信。 扩展页面(options_page,bakcground,popup)和内容脚本(content_scripts)之间的通信通过使用消息传递进行。 任何一方都可以侦听从另一端发送的消息,并在同一通道上做出响应。消息可以包含任何有效的 JSON 对象(空值、布尔值、数字、字符串、数组或对象)。

发送

从内容脚本(content_scripts) 发送到 扩展页面(options_page,bakcground,popup),代码示例:

(async () => {
  const response = await chrome.runtime.sendMessage({greeting: "hello"});
  console.log(response);
})();

从扩展页面(options_page,bakcground,popup)发送到 内容脚本(content_scripts) 代码示例:

(async () => {
    //获取当前的tab页面
  const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
  const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
  console.log(response);
})();
接收

接收消息的方法都是一样的,通过runtime.onMessage事件侦听器来处理消息

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(sender.tab ?
                "from a content script:" + sender.tab.url :
                "from the extension");
    if (request.greeting === "hello")
      //处理完消息后、通知发送方
      sendResponse({farewell: "goodbye"});
  }
);

除了上面介绍runtime.onMessage的方式进行通信。插件还提供了长连接和消息传递API的方法来实现通信:

这里不做介绍了,可以访问官方文档(developer.chrome.com/docs/extens…)

接下来改造前面的下载功能,让功能可以跑起来。

实现下载功能

在前面的main.js脚本中的dragend事件中,添加发送消息的代码:

/**
 * 在document上代理dragend事件
 */
document.addEventListener('dragend',async function(e){
    if(e.target.tagName=='IMG'){
        //发生消息,从content_scripts发送到扩展页面
        await chrome.runtime.sendMessage({type:'down',data:e.target.src});
    }
})

然后再src/service_worker.js中添加接收消息的处理器:


function download(url){
    var options={
        url:url
    }
    chrome.downloads.download(options)
}
//接收消息处理器
chrome.runtime.onMessage.addListener(function(message, sender,sendResponse) {
	if (message.type == 'down') {
        //调用下载方法
		download(message.data)
	}
});

这样就完成了下载功能的开发。

安装插件试一试吧。刷新页面,鼠标移动到一个图片元素上,显示图片信息,然后拖拽一下图片,图片就下载到本地了。

完美。

接下来,我们加一点个性化的功能,比如右键菜单批量下载,配置页面实现图片过滤,在插件图标上显示当前页面上图片的数量。这些功能增加用户的体验。

右键菜单

当用户在网页中单击鼠标右键时,会打开一个列有复制、粘贴等选项的菜单,即右键菜单。它可以为用户提供很多快捷便利的功能。

Chrome 将右键菜单的权限开放,因此开发者可以在里面添加一个菜单

同样、右键菜单功能需要权限配置,在manifest.json中添加权限配置:

{
    ...,
    "permissions": [
      "contextMenus"
    ],
    ...
}

同时我们需要显示一个菜单前的图标,需要再icons里配置一个16像素的图标。我们在项目配置章节已经配置好了。

contextMenusapi也不能再content_scripts中使用。所以需要再扩展页面(后台页面)中创建菜单。

示例代码:

chrome.contextMenus.create({
    type: 'normal',
    title: '右键菜单',
    contexts:['all'],
    id:'menu-1'
});

代码解释:

  • type 用于配置菜单的类型,有4中类型:普通菜单,复选菜单,单选菜单,分割线。
  • title 菜单的名字。
  • contexts 用于配置菜单在什么情况下可以显示。包括all、page、frame、selection、link、editable、image、video、audio和 launcher。
    • 比如在有内容被选择的时候才显示菜单
  • id 菜单的编号,唯一。

同时可以给菜单配置子菜单,如:

chrome.contextMenus.create({
    type: 'normal',
    title: '右键菜单-子',
    contexts:['all'],
    id:'menu-2',
    parentId:'menu-1'
});

这样、我们在点击右键的时候,就可以看到我们配置的菜单了。

对于我们这个插件而言,只需要1个菜单就够了。

src/service_worker.js添加方法:

chrome.contextMenus.create({
    type: 'normal',
    title: '批量导出',
    id:'menu-1'
});

由于我们需要实现一个批量导出页面上所有的图片的功能,所以需要操作dom,根据前面说的,我们需要消息机制在内容脚本(content_scripts)中获取图片元素的地址,然后再交给扩展页面来下载。

注意:

右键菜单的点击事件,需要通过chrome.contextMenus.onClicked来实现。

代码量比较多,我就直接贴代码了:

//src/main.js 
//接收扩展页面的请求,获取图片元素返回
chrome.runtime.onMessage.addListener(function(message, sender,sendResponse) {
	if (message.type == 'images') {
		var imgs=document.querySelectorAll('img');
        var srcs=Array.from(imgs).map(img=>img.src)
        sendResponse(srcs);
	}
});
//src/service_worker.js
function download(url){
    var options={
        url:url
    }
    chrome.downloads.download(options)
}

//通过消息机制获取页面上的image元素
async function onMenuClick(){
    //获取当前打开的tab
    const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
    //发送消息,告诉页面,我们需要获取图片元素
    var response=await chrome.tabs.sendMessage(tab.id,{type:'images'});
    //循环下载
    (response||[]).map(download)
}

/**
 * 添加右键菜单
 */
chrome.contextMenus.create({
    type: 'normal',
    title: '批量导出',
    contexts:['all'],
    id:'menu-1'
});

/**
 * 右键菜单点击事件
 */
chrome.contextMenus.onClicked.addListener(function(data){
    if(data.menuItemId=='menu-1'){
        onMenuClick(data)
    }
})

/**
 * 消息接收机制
 */
chrome.runtime.onMessage.addListener(function(message, sender,sendResponse) {
	if (message.type == 'down') {
		download(message.data)
	}
});

这样就完成了右键批量下载的功能。

WX20230504-170816@2x.png

配置页面

上面的批量下载功能是不是很棒,但是有个问题,它会把当前页面所有的image都下载下来了,这样不好,有些图片不是我们想要的。

这个时候我们就可以通过配置页面来,实现插件的个性化配置。

manifest.json中可以通过配置options page属性,为插件指定一个配置页面。当用户在插件图标上点击右键,选择菜单中的“选项”后,就会打开这个页面。

对于没有图标的扩展,可以在 chrome://extensions 页面中单击“选项”

那么配置的数据存在哪呢?

不用慌,chrome的插件机制提供了存储相关的apichrome.storage,可以实现在插件中数据共享。 一般有三种模式:

  • chrome.storage.local
    • 数据存储在本地,在删除扩展时会被清除。配额限制约为 5 MB,但可以通过请求权限来增加"unlimitedStorage"。
  • chrome.storage.sync
    • 如果启用同步,数据将同步到用户登录的任何 Chrome 浏览器。如果禁用,它的行为类似于storage.local.
  • chrome.storage.session
    • 在浏览器会话期间将数据保存在内存中。默认情况下,它不会暴露给内容脚本,但可以通过设置更改此行为chrome.storage.session.setAccessLevel()。配额限制约为 10 MB。

开始把。

添加配置:

{
    ...,
     "options_ui": {
        "page": "./src/options.html",
        "open_in_tab": false
      },
    ...
}

上面配置页面,是使用窗口ui的模式打开配置页面。

或者使用下面的配置方式:

{
    ...,
     "options_page":"./src/options.html"
    ...
}

在新的tab里打开配置页面。

实现页面UI:

我这里为了简单实现这个UI,我集成了bootstrap ui框架,可以简单,美观的实现这个UI页面。

在src目录下添加boostrap框架,然后再options.html里引用。详细操作可以看代码。

注意:

扩展页面,只能通过script标签外链的形式引入脚本

src/options.html:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="./vendor/bootstrap/bootstrap.min.css">
        <script src="./vendor/bootstrap/bootstrap.min.js"></script>
        <script src="./options.js"></script>
    </head>
    <body>
        <div class="mb-3">
            <label for="basic-url" class="form-label">按域名过滤</label>
            <div class="input-group">
              <span class="input-group-text">domian:</span>
              <input id="filter-url" type="text" class="form-control">
            </div>
            <div class="form-text">只下载匹配该domain的图片</div>
          </div>
    </body>
</html>

然后再新建一个src/options.js文件,添加以下内容:


window.onload=function(){
    //定义存储key
    const FILTER_KEY='filterUrl';

    //保存用户配置
    function saveOptions(value){
        chrome.storage.local.set(value)
    }

    //监听输入框
    document.getElementById('filter-url').addEventListener('change',function(e){
        saveOptions({[FILTER_KEY]:e.target.value||''})
    })

    //加在默认数据
    chrome.storage.local.get([FILTER_KEY]).then((result) => {
        var value= result[FILTER_KEY];
        document.getElementById('filter-url').value=value||''
    });

}

接着,修改src/service_worker.js批量下载的方法。实现根据配置信息来过滤下载:

//通过消息机制获取页面上的image元素
async function onMenuClick() {
    //获取当前打开的tab
    const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
    //发送消息,告诉页面,我们需要获取图片元素
    var response = await chrome.tabs.sendMessage(tab.id, { type: 'images' });
    var data=response || []
    //获取配置信息
    chrome.storage.local.get(['filterUrl']).then((result) => {
        var value = result['filterUrl'];
        if (value) {
            //循环下载
            data.filter(src => src.indexOf(value) != -1).map(download)
        }else{
            data.map(download)
        }
    });
}

一切都搞定了。然后安装插件运行一下吧。完美。

WX20230504-154636@2x.png

接下来、我们在插件图标上添加一个badge,用于显示当前页面上image的数量。

标题和badge

Badge

插件可以选择显示一个徽章,一个叠加在图标上的文本。 徽章可以很容易地更新浏览器操作以显示有关扩展状态的少量信息。

使用方法比较简单:

  • browserAction.setBadgeText 设置徽章文本
  • browserAction.setBadgeBackgroundColor 设置徽章背景色

同样这个api只能在扩展页面上使用,所以我们需要再内容脚本(content_scripts)中获取图片数量后,通过消息机制发送到service_worker.js中,然后调用api显示:

//src/main.js 添加一下代码

window.addEventListener('load',async function(e){
    var imgs=document.querySelectorAll('img');
    await chrome.runtime.sendMessage({type:'badge',data:imgs.length+''});
})

由于badge text只能是string类型,所以需要将number类型转成string类型;

然后再src/service_worker.js添加显示徽章的方法:

//src/service_worker.js 添加一下代码

chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
    if (message.type == 'down') {
        download(message.data)
    }else if(message.type=='badge'){
        chrome.action.setBadgeBackgroundColor({color:'#f00'})
        chrome.action.setBadgeText({
            text:message.data
        })
    }
});

好了、插件就开发好了。

WX20230504-161239@2x.png

安装体验一下把。是不是很酷炫。

项目代码可以参考以下链接。

项目地址:gitee.com/jojowwbb/dr…

参考

官方文档