什么是浏览器插件?
简单来说浏览器插件,是浏览器上的一种工具,可以提供一些浏览器没有的功能,帮你做一些有趣的事情。开发者可以根据自己的喜欢,去实现一些功能。插件基于Web技术(html、css、js)构建。
举个栗子????
FeHelper.JSON插件
功能:格式化JSON、编码转化、markdown、代码压缩等功能。
二维码生成器
功能:可以根据当前浏览的网页地址,生成一个二维码。
SwitchyOmega Proxy
功能:你懂的。
Hello World
manifest.json
Chrome 浏览器插件没有严格的文件结构约束,只需要保证文件夹根目录有manifest.json文件**,**该文件的内容会概括插件所需的资源、权限等等。
一个段简单的示例:
{"manifest_version":2,//必填"name":"my-plugin",//必填"version":"0.1.0"//必填}
manifest_version:代表了manifest文件的版本,浏览器会根据这个值去指定该版本拥有的功能。
name:插件的名称。
version:插件版本。
将manifest.json文件放到一个文件夹内。
chrome://extensions/
在浏览器地址栏输入chrome://extensions/打开“拓展程序”页面。
注意:需要启用右上角的 “开发者模式” 才能加载已解压的插件文件:
加载已解压的插件
启用之后点击加载已解压的拓展程序,选择刚刚我们放入了manifest.json的文件夹,之后你会看到:
新增了一个我们刚刚添加的插件,而且浏览器右上角也会有我们的一个图标:
此时已经加载了一个插件了,但是这个插件除了占用浏览器的一个位置除外,没有任何作用。
如果没有设置插件图标,那么插件的第一个字符会成为插件的默认icon。
让插件看起来更“插件”一点
为了让这个插件更“完善”一点,我们给它加一个icon和描述,并且点击出现一个popup页面,popup 页面一般用来承载临时性的交互,且生命周期很短:单击图标打开popup,焦点离开又立即关闭,可以通过default_popup字段来定义。
{....."description":"这是一段描述",//插件管理页面的icon"icons":{"84":"./icon/ball.png"},//浏览器右上角的图标和内容"browser_action":{"default_icon":"./icon/ball.png","default_title":"我的插件","default_popup":"./html/popup.html"}}
此时我们的目录结构也变成了这样:
给popup.html加上内容:
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width,initial-scale=1.0"><metahttp-equiv="X-UA-Compatible"content="ie=edge"><title>my-plugin</title></head><body><pstyle="width:200px;text-align:center;">helloworld!!</p></body></html>
之后,我们点击插件右下角的“刷新”按钮:
你会发现插件有了icon和描述:
并且右上角的icon也变了,点击一下,会弹出我们刚刚编写的popup.html页面:
现在,我们一个“完整”的插件就已经做好了。
manifest.json 配置介绍
background
{..."background":{//提供一个页面给background"page":"./html/background.html"//或者若干个js文件,后台会默认生成一个空白的html"scripts":["./js/background.js"]}}
background配置项,为插件的后台常驻页面,生命周期随着浏览器的生命周期一样,浏览器一启动,后台页面就会开始运行,直到浏览器被关闭;或者在插件管理页面,将该插件禁用了,后台页面也会停止运行。
另外,background拥有的权限比较高,几乎可以调用所有的Chrome扩展API(除了devtools),同时拥有直接跨域的能力。
page:指定一个网页为后台页面。
scripts:指定若干个js文件,后台会自动生成一个html,并按顺序调用这些js文件。
注意:page和scripts选项只能二选一,不然会报错。
配置好之后,属性插件,会出现一个背景页选项:
我使用的是一个background.js文件:
function_back(){console.log('background.js')}console.log('running...')
点进去看看里面装的什么玩意:
没错,是一个普通的后台页面,如果background.js和其他页面有通信,则可以在这里进行查看请求或者调试代码。
如果使用page选项,打开也是这个样子。
另外:由于background是一直在后台运行的,为了优化性能,可以增加一个配置:
{..."background":{..."persistent":false}}
这样,插件就会在被需要时加载,在空闲时被关闭。比如安装、更新插件的时候,或者有其他页面与background通信的时候才会被加载。
content-scripts
content-scripts能够在合适的时机(页面载入前、载入后、空闲时)注入脚本,允许内容脚本更改其JavaScript环境,而不与页面或其他内容脚本发生冲突。
例如,原页面有个按钮,并且给按钮添加了一个点击事件:
<html><buttonid="mybutton">clickme</button><script>vargreeting="hello,";varbutton=document.getElementById("mybutton");button.person_name="Bob";button.addEventListener("click",function(){alert(greeting+button.person_name+".");},false);</script></html>
在content-scripts中,加入以下代码:
vargreeting="hola,";varbutton=document.getElementById("mybutton");button.person_name="Roberto";button.addEventListener("click",function(){alert(greeting+button.person_name+".");},false);
当页面运行之后,脚本内容也会在插件定义的时间运行,当页面点击按钮时,会出现两次弹窗。
content-scripts配置:
{..."content_scripts":[{//在匹配的URL中运行,<all_urls>表示所有的URL都会运行。"matches":["<all_urls>"],//注入的js,会按顺序运行。"js":["./js/content.js"],//css引入需谨慎,因为可能会影响全局的样式,同样也能接收多个css文件,会按顺序插入到页面中"css":["./css/style.css"],//代码注入的时机,可选值:"document_start","document_end",or"document_idle",最后一个表示页面空闲时,默认document_idle"run_at":"document_start"},{"matches":["/"],"js":["./js/other.js"],"run_at":"document_start"}],...}
content.js代码如下:
console.log('hello,fromcontent.js');
other.js代码如下:
console.log('hello,fromother.js...')
更新插件,当在 /drive/home/运行时:
因为【 /drive/home/】只匹配到了<all_urls>的规则,所以之后运行content.js
当在/运行时:
同时命中了2个规则,所以content.js和other.js都会运行,顺序也是正确的。
content-scripts 和原始页面共享DOM,但是不共享JS,如要访问页面JS(例如某个JS变量),只能通过inject-scripts来实现。content-scripts能够访问的Chrome API的权限也比较低,只能访问以下四个API:
chrome.extension(getURL , inIncognitoContext , lastError , onRequest , sendRequest)
chrome.i18n
chrome.runtime(connect , getManifest , getURL , id , onConnect , onMessage , sendMessage)
chrome.storage
Inject-scripts
inject-scripts 是通过DOM操作插入的JS代码,通常在content-scripts只能操作DOM,但是却无法访问页面的JS,借助content-scripts可以操作DOM的能力,往页面中插入JS文件,给页面提供调用插件API的能力,以及和background通信的能力。
在插入之前,需配置一下web可访问的资源,同时content-scripts的调用时机换成"document_end"或者"document_idle",不然会无法获取DOM,导致插入失败。在manifest.json中添加以下内容:
{..."content_scripts":[{"matches":["<all_urls>"],"js":["./js/content.js"],"run_at":"document_end"},...],"web_accessible_resources":["js/inject.js"],...}
inject.js的内容如下:
functionmockApi(){console.log('thisisfrominject.js')}
content.js增加以下代码:
(function(){letpath='js/inject.js';letscript=document.createElement('script');script.setAttribute('type','text/javascript');//注意,路径需用Chrome API 生成,这个方法可以获得插件的资源的真实路径。//类似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.jsscript.src=chrome.extension.getURL(path);script.onload=function(){//在执行完代码之后移除script标签this.parentNode.removeChild(this);}document.body.appendChild(script);})();
更新插件后,页面就可以访问inject.js的方法:
permissions
插件后台有的操作需要配置相应的权限,例如本地存储、网络请求、通知等等,示例:
{..."permissions":["contextMenus",//右键菜单"tabs",//标签"notifications",//通知"webRequest",//web请求"webRequestBlocking","storage"//插件本地存储],...}
完整的manifest配置
官方文档:/extensions/manifest
通信
popup和background通信
popup可以通过 chrome.extension.getBackgroundPage() API 直接获取到background的上下文,从而调用background的方法来通信:
//popup.jsvarbackend=chrome.extension.getBackgroundPage();backend.test();//访问bbackground的函数
background可以通过chrome.extension.getViews({type:'popup'}) 获取到popup的上下文,前提是popup页面是打开的状态下。
letviews=chrome.extension.getViews({type:'popup'});letpopup=nullif(views.length>0){popup=views[0];//直接访问popup的函数popup.test();}
这里需要注意一点:
在popup页面,你如果想编写js,请将js编写在一个文件里面,然后引入进来,不然会报错,这是因为Chrome的安全政策规定的:/extensions/contentSecurityPolicy
popup错误示范:
<!DOCTYPEhtml><htmllang="en"><head>...</head><body><pstyle="width:200px;text-align:center;">helloworld!!</p><script>//不能直接在里面写</script></body></html>
正确姿势:
<!DOCTYPEhtml><htmllang="en"><head>...</head><body><pstyle="width:200px;text-align:center;">helloworld!!</p><scriptsrc="../js/popup.js"></script></body></html>
content-scripts和background通信
content-scripts可以通过 chrome.runtime.sendMessage(message) 给background发送消息:
chrome.runtime.sendMessage('messagecontent',(res)=>{console.log('frombackground:',res)});
background通过chrome.runtime.onMessage.addListener()监听content-scripts发送的消息:
chrome.runtime.onMessage.addListener(function(message,sender,callback){console.log(mesasge);//meesagecontentcallback&&callback('yesthisfrombackground')});
background主动给content-scripts发消息,首先得查找要给哪个tab发消息,使用chrome.tabs.query 这个方法查找到tab,再使用chrome.tabs.sendMessage 方法给tab发消息:
//{active:true,currentWindow:true}表示查找当前屏幕下的active状态的tab;chrome.tabs.query({active:true,currentWindow:true},function(tabs){chrome.tabs.sendMessage(tabs[0].id,'messagecontent',(res)=>{console.log('fromcontent:',res)});});
content-scripts通过chrome.runtime.onMessage.addListener 去监听事件:
chrome.runtime.onMessage.addListener(function(message,sender,callback){console.log(message,sender)callback&&callback('yesthisfromcontent')});
注意:
1.消息内容可以直接发送JSON格式的对象。
2.popup和content的通信方式与上面一样。
3.如果popup和background都监听了从content发来的消息,两者都能收到监听消息,但是callback只会触发一次,被谁触发取决与谁先发送。
inject-scripts和content-scripts
inject-scripts和content-scripts通信有两种方法:
1.window.postMessage发送,window.addEventListener接收
2.还有一种是自定义的DOM事件;
但是很少情况会是content-scripts去调inject-scripts,因为,可以,但是没必要....content-scripts完全可以自己处理一些API的事件监听,况且inject-scripts也只是content-scripts生成并插入到DOM里面的,所以在content-scripts眼里,inject-scripts就是个弟弟...
但是,很多用户触发的事件,需要通过inject-scripts告诉content-scripts,content-scripts再给background通信并且去做一些事情,然后再发消息告诉inject-scripts,从这个角度看:content-scripts就是一个inject-scripts的工具人!
(扯平了,完美。)
inject-scripts给content-scripts发消息:
window.postMessage({"test":'你好!工具人!'},'*');
content-scripts接收消息:
window.addEventListener("message",function(message){console.log('来了老弟!',message.data);},false);
同样的,content-scripts给inject-scripts发消息是一样的。
练练手:HTTP Header 插件
实现一个HTTP Header 插件,可以实现动态添加header,并且给网络请求自动加上header,header参数可以配置。
示例图:
Background 功能设计
background复制存储、操作headers,对所有浏览器请求做一层拦截,并加上启用的headers。
注意:因为涉及到网络请求,所以需在manifest.json中添加权限:
{..."permissions":["storage",//本地存储"webRequest",//网络请求"webRequestBlocking",//网络请求阻塞式"<all_urls>"//匹配的URL]...}
Background 功能伪代码:
// headers数据结构, 附带默认值;(可以改为本地存储)。constheaders=[{key:'Content-Type',value:'application/x-www-form-urlencoded',enable:false,},{key:'Test-Header',value:'按F进入坦克',enable:true,},];//获取、新增、删除、启用禁用functiongetHeaders(){returnheaders;}functionaddHeader(header){headers.push(header);}functiondeleteHeader(index){headers.splice(index,1);}functiontoggleHeader(index){headers[index].enable=!headers[index].enable;}...//请求拦截器//Oninstall在被安装的时候去初始化chrome.runtime.onInstalled.addListener(function(){//添加事件chrome.webRequest.onBeforeSendHeaders.addListener(requestHandle,{urls:["<all_urls>"],//拦截所有URL的请求},["blocking","requestHeaders"]);//阻塞式console.log('load');});//添加headerfunctionrequestHandle(request){letrequestHeaders=request.requestHeaders;//添加headersheaders.forEach(item=>{if(item.enable){requestHeaders.push({name:item.key,value:item.value,});}});return{requestHeaders};}
chrome.webRequest的生命周期:
详细参考:/extensions/webRequest
popup 页面设计
popup页面提供增加、删除、启用禁用功能接口,并且在每次打开popup页面的时候去background获取最新的header数据,展示在前台。
popup.js 功能伪代码:
//popup页面被打开时,去后台获取最新headerwindow.onload=function(){letbackend=chrome.extension.getBackgroundPage();//调用background方法,获得headersletheaders=backend.getHeaders();//渲染headercreateElement(headers);}//增加按钮functionaddHeader(){letbackend=chrome.extension.getBackgroundPage();letkey=document.querySelector('.key');letvalue=document.querySelector('.value');letheader={key:key.value,value:value.value,enable:true}//调用background方法,新增headersbackend.addHeader(header);createElement(header);}//启用禁用、删除功能functiontoggleHeader(index){letbackend=chrome.extension.getBackgroundPage();backend.toggleHeader(index);}functiondelHeader(index){letbackend=chrome.extension.getBackgroundPage();backend.deleteHeader(index);}
效果
打开popup,添加一个header:
随便打开一个网页,打开控制台查看RequestHeaders:
总结
很多权限、功能需要在manifest.json配置。
content-scripts、popup、background、inject-scripts拥有的权限不一样,通信方式也不一样,理解各个脚本的特点,组合使用。
开发调试可在后台背景页查看信息,popup、inject-scripts、content-scripts可直接审查元素调试。
Chrome 插件还有很多功能这里没有详细介绍,例如devtools。感兴趣的同学可以查阅下面的参考文档。
参考文档
官方文档:/extensions
参考博客:/liuxianan/p/chrome-plugin-develop.html