javascript-Chrome扩展实例(二) Jan 20, 2023 · ecmascript javascript · 分享到: Chrome扩展实例(二) 在Chrome扩展实例(一)中我们用一个例子走通了扩展开发的大体流程,实现了简单的换背景颜色功能。其中执行的js函数将上下文设置为浏览的网页页面,用的是chrome.scripting.executeScript API来改变上下文环境。这其实是Chrome扩展content script的一种,这即是本篇文章介绍的重点。 Content Script Content Script可用接口 Content Script上下文 Inject Scripts 静态声明(declared statically)注入 实例:阅读时间统计 动态声明(declared dynamically)注入 实例:专注模式 为什么不太适合注入javascript (可选)为扩展添加键盘快捷方式 以编程方式注入(programmatically injected) 实例:无图模式 总结 参考文档 Content Script Content script是扩展中运行在网页上下文中的typescript/javascript/css文件,它直接作用于网页的DOM,能够直接访问、修改网页元素。但是由于content script的上下文与扩展不同,因此扩展本身交互时需要消息传递机制。同时,content script并不能像service worker或popup那样可以使用几乎所有的Chrome API,它能够使用的Chrome API很有限。 Content Script可用接口 Content Script能够直接使用的Chrome API如下: i18n (语言国际化接口) storage (存储接口) runtime (运行时接口) connect getManifest getURL id onConnect onMessage sendMessage 虽然content script无法直接调用其他Chrome API,但是可以利用消息传递机制,通过service worker或popup等间接地调用其他Chrome API。 Content Script上下文 上下文可以说是Content Script最核心的内容。默认情况下,content script运行的上下文是一个独立的环境。这个独立环境是所在扩展所独享的,因此content script默认情况下只能操作所在扩展中的内容。扩展独立的空间保证content script的内容不会与网页页面内容、其他扩展的内容不会产生冲突。 但是,如果content script只能在扩展独立的空间中发挥作用,那么它就没法访问、修改网页元素,从而实现目标功能了。因此,Chrome提供一种叫做“Inject scripts”的技术,来修改content script执行时的上下文。 Inject scripts改变上下问的方式有三种: 静态声明(declared statically)注入 动态声明(declared dynamically)注入 以编程方式注入(programmatically injected) Inject Scripts 直观地说,Inject Scripts就是将扩展中typescript/javascript/css文件注入到特定的运行环境中,这样就能够用目标环境的上下文覆盖原来文件的上下文。而inject script的三种模式,可以根据开发需求,酌情选择。 静态声明(declared statically)注入 静态声明注入是inject script最常用的模式。这种模式需要在manifest.json文件中提前写入。优点是方便简洁,缺点是缺乏灵活性,需要提前对manifest.json内容进行规划。静态声明注入使用的manifest.json中的content_scripts字段,基本模式如下: 1{ 2 "name": "扩展名称", 3 //... 4 "content_scripts": [ 5 { 6 "matches": ["https://*.github/*"], 7 "css": ["my-styles.css"], 8 "js": ["content-script.js"], 9 "run_at": "document_idle", 10 "match_about_blank": false, 11 "match_origin_as_fallback":true 12 } 13 ], 14 //... 15} 简单来说,content_script在匹配成功matches字段的网页中,注入js指定的javascript文件和css指定的css文件,其中js和css都可以指定一组文件。maches,js,css此三个字段是content_script的核心字段,后面三个字段都是功能配置字段。matches字段的使用详情可参见文章https://developer.chrome.com/docs/extensions/mv3/match_patterns/以及补充内容https://developer.chrome.com/docs/extensions/mv3/content_scripts/#matchAndGlob。三种功能字段介绍如下: run_at:在什么时候注入内容文件,有三个选项document_start、document_end、document_idle,默认选项是document_idle。 document_start:DOM开始载入。 document_end:DOM主体部分载入完毕,资源文件(如图像、脚本)可能尚在载入中。 document_idle:DOM和资源文件全部载入完毕。 match_about_blank:如果matches字段能够匹配空页面about:blank,注入是否生效,默认false。常见于通配符匹配场景。 match_origin_as_fallback:当页面中包含框架(frame),如果框架的URL不匹配matches字段,但是框架所在的母网页匹配matches字段,内容注入是否生效,默认为true。这个属性适用于manifest V3及以上版本的扩展,同时由于HTML5中框架(frame)字段遭到删除,这条可能主要用于兼容老版本网页或<iframe>标签。 下面我们就用一个例子解释静态声明注入的用法。 实例:阅读时间统计 有了《chrome扩展入门》《chrome扩展实例(一)》两篇文章,我们对扩展的基本开发流程已有了基本的了解。现在我们就省略已知的步骤,快速实现一个新的扩展。首先,依旧是manifest.json文件: 1{ 2 "manifest_version": 3, 3 "name": "Reading time", 4 "version": "1.0", 5 "description": "估计阅读文章所需要的时间", 6 7 "icons": { 8 "16": "images/icon-16.png", 9 "32": "images/icon-32.png", 10 "48": "images/icon-48.png", 11 "128": "images/icon-128.png" 12 }, 13 "content_scripts": [ 14 { 15 "js": [ 16 "scripts/content.js" 17 ], 18 "matches": [ 19 "https://surprisedcat.github.io/studynotes/*", 20 "https://surprisedcat.github.io/projectnotes/*" 21 ] 22 } 23 ] 24} manifest.json文件前面几项都没什么再需要解释的了,icons的素材来自URLhttps://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/tutorial.reading-time/images(如果链接失效,可以随自己喜好找合适的图片)。新增的内容content_scripts字段即为静态声明注入content_scripts,注入的js脚本来自扩展根目录下的scripts/content.js,扩展可用的网页是匹配matches字段的本人博客网页^_^。该扩展运行时,Chrome浏览器会给扩展提供网页URL,当matches字段匹配成功时,注入js脚本的功能生效(默认在网页完全载入的document_idle阶段启动注入)。 接下来,新建目录scripts并在其下新建js文件content.js,并添加如下代码: 1//content_script.js 2 3//获取网页中的文章<article>标签(并不是所有网页都有,我的博客中有这个标签) 4const article = document.querySelector("article"); 5//判断是否成功,不成功则返回null,也可能返回多个,为了方便我们先不考虑 6if (article) { 7 const text = article.textContent;//获取标签中文本 8 //分中英文统计字符 9 //中文,/\p{Unified_Ideograph}/ug 匹配所有中文 10 const chineseChar = text.matchAll(/\p{Unified_Ideograph}/ug); 11 const chineseNum = [...chineseChar].length;//计算文本长度 12 //英文,去除中文后再匹配 13 const englishChar = text.replace(/[^\w-]/g, ' ').matchAll(/[^\s]+/g); 14 const englishNum = [...englishChar].length;//计算文本长度 15 //假设我们每分钟阅读中文400个字,英文300单词,计算阅读时长 16 const readingTime = Math.round(chineseNum / 400+ englishNum / 300); 17 //创建<p>元素存放结果 18 const badge = document.createElement("p"); 19 badge.textContent = `⏱️ ${readingTime} min read`; 20 21 //在<h1>标题后面添加阅读时间的<p>元素 22 const heading = article.querySelector("h1"); 23 heading.insertAdjacentElement("afterend", badge); 24} 需要指出,这个content.js文件中的DOM操作针对的是本人博客网页的操作,并不能无缝移植到其他网页上。接下来,我们载入这个扩展。当我们打开一般网页时,这个扩展由于匹配字段并不成功,不会生效,只有当我们访问 1https://surprisedcat.github.io/studynotes/* 2https://surprisedcat.github.io/projectnotes/* 这两组网页时,扩展才会生效。那么,我们就访问上一篇文章《javascript-chrome扩展实例(一)》的URLhttps://surprisedcat.github.io/projectnotes/javascript-chrome%E6%89%A9%E5%B1%95%E5%AE%9E%E4%BE%8B%E4%B8%80/,显然时能够匹配的,注意看大标题“javascript-Chrome扩展实例(一)”下面确实多出了一行阅读时间。 静态声明注入实验成功!附:reading_time文件结构。 1reading_time 2 ├─manifest.json 3 ├─images 4 │ ├─icon-128.png 5 │ ├─icon-16.png 6 │ ├─icon-32.png 7 │ └─icon-48.png 8 └─scripts 9 └─content.js 动态声明(declared dynamically)注入 如果某些网站不是那么知名,扩展无法在manifest.json设计时就预见到或者对于某个匹配到的网站,并不是要总是注入内容脚本,需要在运行时再决定。对于这两种情况,静态声明注入就无法胜任,我们需要一种更灵活的内容注入方式,这就需要动态声明(declared dynamically)注入。 从Chrome 96开始,我们可以调用chrome.scripting API进行动态声明注入,其主要方法包括: 注册content script:chrome.scripting.registerContentScripts,chrome.scripting.insertCSS。 查看当前所有动态注册的content script:chrome.scripting.getRegisteredContentScripts。 更新content script:chrome.scripting.updateContentScripts。 删除已注册的content script:chrome.scripting.unregisterContentScripts,chrome.scripting.removeCSS。 动态声明注入通常至少需要两个权限:activeTab和scripting,其他权限根据所需的功能额外再提供。 区别与静态声明注入使用matches字段来决定哪些URL执行内容注入,动态声明注入使用注入目标(Injection targets)来决定内容注入对象。注入目标使用tabID来唯一决定。 tabID是Chrome标签页面window对象的ID,当我们打开多个tab页面时,每个tab页面都是一个独立的window对象,它们通过不同tabId区分,默认内容注入只在页面的主框架中有效。 实例:专注模式 现在网页上面有很多杂七杂八的元素,当我们阅读时很容易被这些元素分心,因此我们想做一个扩展,能够暂时性地让这些杂七杂八的元素消失,是我们能够更专心地阅读文章。本例使用的素材修改自官方教程https://developer.chrome.com/docs/extensions/mv3/getstarted/tut-focus-mode/。 我们整体思路如下:先选CSDN网站为例,上面有很多妨碍阅读的元素,我们点击该扩展后,能将这些元素的CSS的display属性修改为none。假设我们已经提前写好了一个CSS文件,只要注入此文件就能实现(而非一个个地设置哪些元素应该不可见)。此外,我们还需要设置一个键盘快捷键,能够方便地在一般模式和专注模式中切换。 我们首先设计manifest.json文件,因为不需要静态声明注入,所以不需要content_scripts字段,取而代之的是动态声明注入所需要的activeTab和scripting两个权限。 1{ 2 "manifest_version": 3, 3 "name": "Focus Mode", 4 "description": "Enable reading mode on Chrome's official Extensions and Chrome Web Store documentation.", 5 "version": "1.0", 6 "icons": { 7 "16": "images/icon-16.png", 8 "32": "images/icon-32.png", 9 "48": "images/icon-48.png", 10 "128": "images/icon-128.png" 11 }, 12 "background": { 13 "service_worker": "background.js" 14 }, 15 "action": { 16 "default_icon": { 17 "16": "images/icon-16.png", 18 "32": "images/icon-32.png", 19 "48": "images/icon-48.png", 20 "128": "images/icon-128.png" 21 } 22 }, 23 "permissions": ["scripting", "activeTab"] 24} manifest其他部分之前都应该讲解过了,现阶段唯一需要指出的是我们会在service worker的background.js中动态声明注入内容。 为了区分当前网页是一般模式还是专注模式,我们给扩展的图标添加一个小徽章(badge),当开启专注模式时显示“ON”,否则显示“OFF”。所谓“badge”就是在扩展图标上显示一些文本,可以用来更新一些小的扩展状态提示信息。因为“badge”空间有限,所以只支持4个以下的字符(英文4个,中文2个)。“badge”无法通过配置文件来指定,必须通过代码实现,设置badge文字和颜色可以分别使用chrome.action.setBadgeText({text: 'WORD'})和chrome.action.setBadgeBackgroundColor({color:[255, 0, 0, 255]})。 每次点击扩展图标,就换切换网页状态,同时badge状态也会跟着改变。 1//background.js 2 3//初始状态下,状态为OFF 4chrome.runtime.onInstalled.addListener(() => { 5 chrome.action.setBadgeText({ 6 text: "OFF", 7 }); 8}); 9 10//添加监听事件,点击扩展action图标 11//tab默认指当前的tab页面 12chrome.action.onClicked.addListener(async(tab) => { 13 //获取当前badge状态 14 const prevState = await chrome.action.getBadgeText({ tabId: tab.id }); 15 //点击后的状态总是和之前相反 16 const nextState = prevState === 'ON' ? 'OFF' : 'ON'; 17 //更改badge状态的文字 18 await chrome.action.setBadgeText({ tabId: tab.id, text: nextState }); 19}) 以上代码实现了Badge状态文字的切换。我们希望在CSDN的网页上实现开启专注模式时,杂乱元素不可见,因此我们要添加CSDN的URL作为判别条件,同时根据Badge状态文字,决定注入还是取消注入CSS文件。因此,完善后的background.js代码如下: 1//background.js 2 3//初始状态下,状态为OFF 4chrome.runtime.onInstalled.addListener(() => { 5 chrome.action.setBadgeText({ 6 text: "OFF", 7 }); 8 }); 9 10//目标URL 11const CSDN_url = 'https://blog.csdn.net/'; 12 //添加监听事件,点击扩展action图标 13 //tab默认指当前的tab页面 14chrome.action.onClicked.addListener(async(tab) => { 15//如果以CSDN_URL开头则执行内容脚本 16 if(tab.url.startsWith(CSDN_url)){ 17 //获取当前badge状态 18 const prevState = await chrome.action.getBadgeText({ tabId: tab.id }); 19 //点击后的状态总是和之前相反 20 const nextState = prevState === 'ON' ? 'OFF' : 'ON'; 21 //更改badge状态的文字 22 await chrome.action.setBadgeText({ tabId: tab.id, text: nextState }); 23 //根据Badge状态文字执行注入CSS和取消注入CSS 24 if(nextState === "ON"){ 25 await chrome.scripting.insertCSS({//注入CSS 26 files: ["css/csdn.css"], 27 target: { tabId: tab.id } 28 }); 29 } else if (nextState === "OFF") { 30 await chrome.scripting.removeCSS({//取消注入CSS 31 files: ["css/csdn.css"], 32 target: { tabId: tab.id } 33 }); 34 } 35 } 36}) 这样就以动态声明注入实现了两种模式CSS的切换。csdn.css的内容如下: 1/*CSDN 专注模式样式表*/ 2/*URL: *blog.csdn.net/* */ 3#csdn-toolbar{display: none} 4#mainBox > aside{display: none} 5#recommend-right{display: none} 6#toolBarBox{display: none} 7#mainBox > main > div.first-recommend-box.recommend-box{display: none} 8#mainBox > main > div.second-recommend-box.recommend-box{display: none} 9.csdn-side-toolbar {display: none} 10#pcCommentBox{display: none} 11#mainBox > main > div.recommend-box.insert-baidu-box.recommend-box-style{display: none} 12#mainBox > main > div.template-box{display: none} 13#mainBox > main > div.blog-footer-bottom{display: none} 14#blogHuaweiyunAdvert > div{display: none} 15#blogColumnPayAdvert{display: none} 16body > div.main_father.clearfix.d-flex.justify-content-center{width: 100%;background-color: aliceblue} 17#mainBox{width: 100%;background-color: aliceblue} 18#mainBox > main{width: 100%;background-color: aliceblue} 19#blogExtensionBox{display: none} 20#js_content > pre{display: none} 21/*使用内联样式表以及!important的顽固分子*/ 22/*一般情况不要再扩展中使用!important*/ 23#treeSkill{display: none!important} 24#pcCommentBox{display: none!important} 25#recommendNps{display: none!important} 26body.nodata{background: none!important;background-color:aliceblue!important;background-image: none!important} 现在我们可以重新载入扩展,选择CSDN的博客来测试下效果。示例URL:https://blog.csdn.net/wuyxinu/article/details/115839575。 一般模式: 专注模式: 动态声明注入试验成功!附:focus_mode文件结构。 1focus_mode 2├─background.js 3├─manifest.json 4├─css 5│ └─ csdn.css 6└─images 7 ├─icon-128.png 8 ├─icon-16.png 9 ├─icon-32.png 10 └─icon-48.png 为什么不太适合注入javascript 上面的例子我们动态注入的是CSS样式表,那么是否可以注入javascript文件呢?可以的。事实上,最开始我对官方示例改造时就用的是动态注入javascript。但是动态注入javascript时需要指定runAt,而之前我们说明runAt只有三个时刻,这三个时刻都需要重新载入网页,对于扩展来说实在不方便,因此我们采用了不需要重载网页的CSS样式表来做示例。 那么,有没有一种不需要重载网页就能运行javascript的内容注入模式呢?当然有的,就是下一节介绍的以编程方式注入。 (可选)为扩展添加键盘快捷方式 为了使用方便,我们还可以给扩展添加键盘快捷方式。比如可以通过快捷键启用/关闭专注模式。我们只需要在manifest.json最后添加如下代码: 1//... 2{ 3 "commands": { 4 "_execute_action": { 5 "suggested_key": { 6 "default": "Ctrl+B", 7 "mac": "Command+B" 8 } 9 } 10 } 11} command字段代表监听的键盘事件。_execute_action等同于点击扩展图标事件action.onClicked(),因此我们不需要额外添加任何代码。suggested_key则是指定的快捷键,对于不同的操作系统(win、mac)快捷键有所区别。 更多关于键盘快捷键的内容可参考https://developer.chrome.com/docs/extensions/reference/commands/。 以编程方式注入(programmatically injected) 我们前来介绍的两种内容注入方式都或多或少有些缺点。静态声明注入需要开发者极富远见,在manifest.json设计阶段就能够决定未来各种情况,否则就得频繁地更新插件;动态声明注入后又得重载网页让javascript生效,十分麻烦。能否可以让注入的js内容实时生效呢?此时就可以使用以编程方式注入(programmatically injected)。它允许扩展使用chrome.scripting.executeScript API在特定事件或特殊场景执行内容注入。 以编程方式注入是最灵活多变的注入方法,它不仅可以像前两种方式那样选择Javascript或CSS文件进行注入,还可以选择可用的Javascript函数进行注入,例如在《javascript-chrome扩展实例(一)》中的修改背景颜色的实例,就是采用了Javascript函数注入的方式。但是如果该模式运用的不合理就会使得代码变得杂乱而无条理。其一般形式代码框架如下: 1//使用js/css文件注入方式 2chrome.action.onClicked.addListener((tab) => { 3 chrome.scripting.executeScript({ 4 target: { tabId: tab.id },//注入的目标tab页面 5 files: ["content-script.js"]//注入的文件 6 }); 7}); 8 9//使用js函数注入方式 10chrome.action.onClicked.addListener((tab) => { 11 chrome.scripting.executeScript({ 12 target : {tabId : tab.id},//注入的目标tab页面 13 func : injectedFunction,//注入的函数,这个函数必须当前脚本可调用的 14 args : [ "arg1","arg2" ]//函数的参数 15 }); 16}); 17 18function injectedFunction() { 19 document.body.style.backgroundColor = "orange"; 20} 请注意,在使用函数注入的方式时,注入的函数是chrome.scripting.executeScript调用中引用的函数的副本,而不是原始函数本身。因此,函数的主体必须是自包含的(self-contained),即不能使用函数以外的上下文内容;对函数外部变量的引用将导致内容脚本引发引用错误(ReferenceError)。 以编程方式注入通常至少需要两个权限:activeTab和scripting,其他权限根据所需的功能额外再提供。 实例:无图模式 这个实例将使用以编程方式注入删除当前激活tab页面的所以图像元素<img>,这个简单的扩展只包含三种文件:manifest.json、background.js和images文件夹中的图标。 manifest.json文件如下: 1{ 2 "manifest_version": 3, 3 "name": "No Images", 4 "version": "1.0", 5 "description": "Remove all the images in the web pages", 6 "background": { 7 "service_worker": "background.js" 8 }, 9 "action": { 10 "default_icon": { 11 "16": "images/icon-16.png", 12 "32": "images/icon-32.png", 13 "48": "images/icon-48.png", 14 "128": "images/icon-128.png" 15 } 16 }, 17 "icons": { 18 "16": "images/icon-16.png", 19 "32": "images/icon-32.png", 20 "48": "images/icon-48.png", 21 "128": "images/icon-128.png" 22 }, 23 "permissions": ["activeTab", "scripting"] 24} 这里没有什么新的知识。只需要注意以编程方式注入需要两种权限activeTab和scripting。我们在background.js中实现删除图片元素的js功能代码: 1//background.js 2 3chrome.action.onClicked.addListener(async(tab) => { 4 if(!tab.url.includes("chrome://")) {//chrome设置页面不生效 5 //以编程方式注入 6 chrome.scripting.executeScript({ 7 target: { tabId: tab.id },//tabId默认是当前激活tab页面的ID 8 function: removeImages//调用函数 9 }); 10 } 11}); 12 13function removeImages(){ 14 //选出所有<img>元素 15 const elememts = document.querySelectorAll("img"); 16 if(elememts){ 17 for(const item of elememts){ 18 //元素删除的模式,并非直接删除,而是通过父元素删除 19 item.parentNode.removeChild(item); 20 } 21 } 22} 我们载入扩展,用百度主页来做测试: 之前,下图中百度图标是存在的: 点击扩展后,百度主页的图标被删除了: 以编程方式注入内容实验成功! 总结 本文以三种方式实现了CSS/Javascript内容的注入,分别是静态声明注入、动态声明注入和以编程方式注入。首先需要注意注入的内容所在上下文与扩展所在上下文的区别。注入内容的上下文需要与目标tab网页一致。其次,我们需要根据不同的场景选择合理的注入方式,还需要给予注入操作权限。内容注入是Chrome扩展实现其功能多样性的灵活。 参考文档 Google官方文档https://developer.chrome.com/docs/extensions/mv3/ Content Script https://developer.chrome.com/docs/extensions/mv3/content_scripts/ Focus Mode https://developer.chrome.com/docs/extensions/mv3/getstarted/tut-focus-mode/ Page redder https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/sample.page-redder