初始化项目
新建项目
编写一个manifest.json文件来描述你的插件。
1 2 3 4 5 6 7 8 9 10 11 12 { "manifest_version" : 3 , "name" : "weirdo-message" , "version" : "1.0.0" , "description" : "一个图片下载插件" , "icons" : { "16" : "icon16.png" , "32" : "icon32.png" , "48" : "icon48.png" , "128" : "icon128.png" } }
本地插件的安装需要打开开发模式 在浏览器输入chrome://extensions/打开插件页面,点击右上角的开发者模式,在点击加载已解压的扩展程序,选择我们刚刚新建的那个插件项目文件夹。
添加功能
我们给插件添加第一个功能:鼠标移动到图片元素上,显示图片的信息(存储大小,真实尺寸,显示尺寸)
由于我们的插件是需要操作 dom,并且不需要一直在后台运行,只需要再打开网页的时候运行。
所以我们使用内容脚本 content_scripts 的方式运行插件即可。
内容脚本(content_scripts)的特性:
在页面打开,或者页面加载结束,或者页面空闲的时候注入
共享页面 dom,也就是说可以操作页面的 dom
JS是隔离的,插件中的js定义并不会影响页面的js,也不能引用页面中的js变量、函数
content_scripts 有多种使用方式:
静态注入。在manifest.json文件中声明
动态注入。chrome.scripting.registerContentScripts
编码注入。chrome.scripting.executeScript
使用静态注入
在manifest.json文件中添加一下content_scripts配置:
1 2 3 4 5 6 7 8 9 10 { ..., "content_scripts" : [ { "matches" : [ "https://*/*" ] , "js" : [ "src/main.js" ] } ] , ...}
使用动态注入
通过调用api的方法来注入
1 2 3 4 5 6 7 8 9 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
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 function getByte (src ){ return fetch (src).then (function (res ){ return res.blob () }).then (function (data ){ return (data.size /(1024 )).toFixed (2 )+'kB' }) }function showInfo (el,byte ){ var html=`真实尺寸:${el.naturalWidth} *${el.naturalHeight} \n显示尺寸:${el.width} *${el.height} \n存储大小:${byte} ` ; el.title =html }document .addEventListener ('mouseover' ,function (e ){ if (e.target .tagName =='IMG' ){ getByte (e.target .src ).then (byte => { showInfo (e.target , byte) }) } }, true )
给图片元素添加拖拽事件 接下来我们给图片元素添加拖拽事件,因为是拖拽下载,所以我们只使用 dragend 事件即可.
1 2 3 4 5 6 7 8 document .addEventListener ('dragend' ,function (e ){ if (e.target .tagName =='IMG' ){ } })
实现下载功能 实现图片下载功能有俩种方式:
在内容脚本(content_scripts)中使用原生js实现下载图片功能
使用插件 chrome.downloads.downloadAPI 实现下载
原生实现下载这里就不赘述了
我们使用 chrome.downloads.download 来实现下载。
我们需要在 manifest.json 中添加 downloads 权限来使用该 api。
1 2 3 4 5 6 7 { ..., "permissions" : [ "downloads" ] , ...}
注意,chrome 的部分api不能直接在content_scripts中使用,所以我们需要一个后台页面来使用这个api来实现下载。
添加后台页面脚本( background )配置:
1 2 3 4 5 6 7 { ..., "background" : { "service_worker" : "src/service_worker.js" } , ...}
我们需要一个新建后台脚本src/service_worker.js
1 2 3 4 5 6 7 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)
1 2 3 4 (async () => { const response = await chrome.runtime .sendMessage ({greeting : "hello" }); console .log (response); })();
从扩展页面(options_page,bakcground,popup)发送到 内容脚本(content_scripts)
1 2 3 4 5 6 (async () => { 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事件侦听器来处理消息
1 2 3 4 5 6 7 8 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的方法来实现通信, 具体可访问官方文档
实现下载 在前面的main.js脚本中的dragend事件中,添加发送消息的代码:
1 2 3 4 5 6 7 8 9 document .addEventListener ('dragend' , async function (e ){ if (e.target .tagName == 'IMG' ){ await chrome.runtime .sendMessage ({type :'down' , data :e.target .src }); } })
然后在src/service_worker.js中添加接收消息的处理器:
1 2 3 4 5 6 7 8 9 10 11 12 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 ) } });
这样就完成了下载功能的开发。
右键菜单批量下载图片 右键菜单功能需要权限配置,在manifest.json中添加权限配置:
1 2 3 4 5 6 7 { ..., "permissions" : [ "contextMenus" ] , ...}
我们需要显示一个菜单前的图标,需要再icons里配置一个16像素的图标。我们在之前已经配置好了。
contextMenusapi也不能在content_scripts中使用。所以需要在src/service_worker.js中创建菜单。
1 2 3 4 5 6 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。比如在有内容被选择的时候才显示菜单
同时可以给菜单配置子菜单
1 2 3 4 5 6 7 chrome.contextMenus .create ({ type : 'normal' , title : '右键菜单-子' , contexts :['all' ], id :'menu-2' , parentId :'menu-1' });
对于我们这个插件而言,只需要1个菜单就够了。
1 2 3 4 5 chrome.contextMenus .create ({ type : 'normal' , title : '批量导出' , id :'menu-1' });
我们需要实现一个批量导出页面上所有的图片的功能,所以需要操作dom,根据前面说的,我们需要消息机制在内容脚本(content_scripts)中获取图片元素的地址,然后再交给扩展页面来下载。
右键菜单的点击事件,需要通过chrome.contextMenus.onClicked来实现。
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 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); } });document .addEventListener ('dragend' , async function (e ){ if (e.target .tagName == 'IMG' ){ await chrome.runtime .sendMessage ({type :'down' , data :e.target .src }); } })function download (url ){ var options = { url :url } chrome.downloads .download (options) }async function onMenuClick ( ){ 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 ) } });
这样就完成了右键批量下载的功能。
个性化配置 如果这个页面中有些图片不是我们想要的, 这个时候我们就可以通过配置页面来,实现插件的个性化配置。
在manifest.json中可以通过配置options page属性,为插件指定一个配置页面。
当用户在插件图标上点击右键,选择菜单中的“选项”后,就会打开这个页面。
配置的数据存在哪呢?
chrome的插件机制提供了存储相关的api, chrome.storage可以实现在插件中数据共享。
一般有三种模式:
chrome.storage.local : 数据存储在本地,在删除扩展时会被清除。配额限制约为 5 MB,但可以通过请求权限来增加”unlimitedStorage”。
chrome.storage.sync : 如果启用同步,数据将同步到用户登录的任何 Chrome 浏览器。如果禁用,它的行为类似于storage.local.
chr``ome.storage.session : 在浏览器会话期间将数据保存在内存中。默认情况下,它不会暴露给内容脚本,但可以通过设置更改此行为chrome.storage.session.setAccessLevel()。配额限制约为 10 MB。
这里需要在mainfest.json的permissions配置: ["storage"]
添加配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { ..., "options_ui" : { "page" : "./src/options.html" , "open_in_tab" : false } , ...} { ..., "options_page" : "./src/options.html" ...}
这里实我集成了bootstrap ui 框架,
扩展页面只能通过script标签外链的形式引入脚本
src/options.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <!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文件,添加以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 window .onload = function ( ){ 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批量下载的方法。实现根据配置信息来过滤下载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 async function onMenuClick ( ) { 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) } }); }
添加 badge
插件可以选择显示一个徽章,一个叠加在图标上的文本。 徽章可以很容易地更新浏览器操作以显示有关扩展状态的少量信息。
使用方法比较简单:
chrome.action.setBadgeText 设置徽章文本
chrome.action.setBadgeBackgroundColor 设置徽章背景色
这个api只能在扩展页面上使用,所以我们需要再内容脚本(content_scripts)中获取图片数量后,通过消息机制发送到service_worker.js中,然后调用api显示:
1 2 3 4 5 6 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 添加显示徽章的方法:
1 2 3 4 5 6 7 8 9 10 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 }) } });
更新一下 manifest.json
1 2 3 4 5 6 7 8 9 10 11 { ..., "action" : { "default_icon" : { "16" : "icon16.png" } , "default_title" : "weirdo-message" , "default_popup" : "./src/options.html" } ...}