初始化项目
新建项目
编写一个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" } ...}