Chrome陆续向开发者开放了大量的API。使用这些API,我们可以监听或代理网络请求,存储数据,管理标签页和Cookie,绑定快捷键、设置右键菜单,添加通知和闹钟,获取CPU、电池、内存、显示器的信息等等(还有很多没有列举出来)。具体请阅读Chrome API官方文档。请注意,使用相应的API,往往需要申请对应的权限,如IHeader申请的权限如下所示。
"permissions": [ "tabs" , "webRequest", "webRequestBlocking", "http://*/*", "https://*/*", "contextMenus", "notifications"]
以上,IHeader依次申请了标签页、请求、请求断点、http网站,https网站,右键菜单,桌面通知的权限。
Chrome Extension API中,能够修改请求的,只有chrome.webRequest了。webRequest能够为请求的不同阶段添加事件监听器,这些事件监听器,可以收集请求的详细信息,甚至修改或取消请求。
事件监听器只在特定阶段触发,它们的触发顺序如下所示。(图片来自MDN)
事件监听器的含义如下所示。
以上,凡是能够修改请求的事件监听器,都能够指定其extraInfoSpec参数数组中包含”blocking”字符串(意味着能阻塞请求并修改),反之则不行。
另外请注意,Chrome对于请求头和响应头的展示有着明确的规定,即控制台中只展示发送出去或刚接收到的字段。因此编辑后的请求字段,控制台的network栏能够正常展示;而编辑后的响应字段由于不属于刚接收到的字段,所以从控制台上就会看不到编辑的痕迹,如同没修改过一样,实际上编辑仍然有效。
事件监听器含义虽不同,但语法却一致。接下来我们就以onHeadersReceived为例,进行深入分析。
还记得我们的目标吗?想要去掉Google网站HTML响应头的X-Frame-Options
字段。请看如下代码:
// 监听的回调 var callback = function(details) { var headers = details.responseHeaders; for (var i = 0; i < headers.length; ++i) { // 移除X-Frame-Options字段 if (headers[i].name === 'X-Frame-Options') { headers.splice(i, 1); break; } } // 返回修改后的headers列表 return { responseHeaders: headers }; }; // 监听哪些内容 var filter = { urls: ["<all_urls>"] }; // 额外的信息规范,可选的 var extraInfoSpec = ["blocking", "responseHeaders"]; /* 监听response headers接收事件*/ chrome.webRequest.onHeadersReceived.addListener(callback, filter, extraInfoSpec);
chrome.webRequest.onHeadersReceived.addListener表示添加一个接收响应头的监听。以上代码中的关键参数或属性,下面逐一讲解。
既然有了添加监听的方法,自然,还会有移除监听的方法。
chrome.webRequest.onHeadersReceived.removeListener(listener);
除此之外,为了避免重复监听,还可以判断监听是否已经存在。
var bool = chrome.webRequest.onHeadersReceived.hasListener(listener);
为了保证更好的理清以上属性、方法或参数的逻辑关系,请看如下脑图:
知道了如何绑定监听器,仅仅是第一步。监听器需要在合适的时机绑定,也需要在合适的时机解绑。为了不影响Chrome的访问速度,我们只在需要的标签页创建新的监听器,因此监听器需要依赖filter来区分不同的tabId,考虑到用户可能只需要监听一部分请求类型,types的区分也是不可避免的。又由于一个Tab里不同的时间段可能会加载不同的页面,一个监听器在不同的页面下正常运行也是必须的(因此监听器的filter中不需要指定urls)。
寥寥数语,可能不足以描述出监听器状态管理的原貌,请看下图进一步帮助理解。
以上,一个请求将依次触发上述①②③④⑤五个事件回调,每个事件回调都对应着一个监听器,这些监听器分为两类(从颜色上也可看出端倪)。
若Chrome指定的标签页激活了IHeader扩展,②③⑤监听器就会记录当前标签页后续的指定类型的请求信息。若用户在激活了IHeader扩展的标签页更新了Request的请求头或响应头,①或④监听器就会被开启。不用担心监听器开启无限个,我准备了回收机制,单个标签页的所有监听器都会在标签页关闭或IHeader扩展取消激活后释放掉。
首先,为方便管理,先封装下监听器的代码。
/* 独立的监听器 */ var Listener = (function(){ var webRequest = chrome.webRequest; function Listener(type, filter, extraInfoSpec, callback){ this.type = type; // 事件名称 this.filter = filter; // 过滤器 this.extraInfoSpec = extraInfoSpec; // 额外的参数 this.callback = callback; // 事件回调 this.init(); } Listener.prototype.init = function(){ webRequest[this.type].addListener( // 添加一个监听器 this.callback, this.filter, this.extraInfoSpec ); return this; }; Listener.prototype.remove = function(){ webRequest[this.type].removeListener(this.callback); // 移除监听器 return this; }; Listener.prototype.reload = function(){ // 重启监听器(用于选项页面更新请求类型后重启所有已开启的监听器) this.remove().init(); return this; }; return Listener; })();
监听器封装好了,剩下的便是管理,监听器控制器基于标签页的维度统一管理标签页上所有的监听器,代码如下。
/* 监听器控制器 */ var ListenerControler = (function(){ var allListeners = {}; /* 所有的监听器控制器列表 */ function ListenerControler(tabId){ if(allListeners[tabId]){ /* 如有就返回已有的实例 */ return allListeners[tabId]; } if(!(this instanceof ListenerControler)){ /* 强制以构造器方式调用 */ return new ListenerControler(tabId); } /* 初始化变量 */ var _this = this; var filter = getFilter(tabId); // 获取当前监听的filter设置 /* 捕获requestHeaders */ var l1 = new Listener('onSendHeaders', filter, ['requestHeaders'], function(details){ _this.saveMesage('request', details); // 记录请求的头域信息 }); /* 捕获responseHeaders */ var l2 = new Listener('onResponseStarted', filter, ['responseHeaders'], function(details){ _this.saveMesage('response', details); // 记录响应的头域信息 }); /* 捕获 Completed Details */ var l3 = new Listener('onCompleted', filter, ['responseHeaders'], function(details){ _this.saveMesage('complete', details); // 记录请求完成时的时间等信息 }); allListeners[tabId] = this; // 记录当前的标签页控制器 this.tabId = tabId; this.listeners = { // 记录已开启的监听器 'onSendHeaders': l1, 'onResponseStarted': l2, 'onCompleted': l3 }; this.messages = {}; // 当前标签页的请求信息集合 console.log('tabId=' + tabId + ' listener on'); } ListenerControler.has = function(tabId){...} // 判断是否包含指定标签页的控制器 ListenerControler.get = function(tabId){...} // 返回指定标签页的控制器 ListenerControler.getAll = function(){...} // 获取所有的标签页控制器 ListenerControler.remove = function(tabId){...} // 移除指定标签页下的所有监听器 ListenerControler.prototype.remove = function(){...} // 移除当前控制器中的所有监听器 ListenerControler.prototype.saveMesage = function(type, message){...} // 记录请求信息 return ListenerControler; })();
通过监听器控制器的统一调度,标签页中的多个监听器才能高效的工作。
实际上,还有很多工作,上述代码还没有体现出来。比方说用户在激活了IHeader扩展的标签页更新了Request的请求头或响应头,①beforeSendHeaders或④headersReceived监听器又是怎么运作的呢?这部分内容,请结合『如何绑定header监听』节点的内容理解。
标签页控制器的状态需要由视觉体现出来,因此Page Action图标的管理也是不可避免的。通常,默认的icon可以在manifest.json中指定。
"page_action": { "default_icon": "res/images/lightning_default.png", // 默认图标 },
icon有如下3种状态(后两种状态可以互相切换)。
Chrome提供了chrome.pageAction的API供Page Action使用。目前chrome.pageAction拥有如下方法。
以上,setTitle、setIcon 和 show方法比较常用。其中,show方法有两种作用,①展示icon,②更新icon,因此一般是先设置好icon的标题和路径,然后调用show展示出来(或更新)。需要注意的是,Page Action在show方法被调用之前,是不会响应点击的,所以需要在初始化工作结束之前调用show方法。千言万语不如上代码,如下。
/* 声明3种icon状态 */ var UNINIT = 0, // 扩展未初始化 INITED = 1, // 扩展已初始化,但未激活 ACTIVE = 2; // 扩展已激活 /* 处理扩展icon状态 */ var PageActionIcon = (function(){ var pageAction = chrome.pageAction, icons = {}, tips = {}; icons[INITED] = 'res/images/lightning_green.png'; // 设置不同状态下的icon路径(相对于扩展根目录) icons[ACTIVE] = 'res/images/lightning_red.png'; tips[INITED] = Text('iconTips'); // 其它地方有处理,Text被指向chrome.i18n.getMessage,用以读取_locales中指定语言的对应字段的文本信息 tips[ACTIVE] = Text('iconHideTips'); function PageActionIcon(tabId){ // 构造器 this.tabId = tabId; this.status = UNINIT; // 默认为未初始化状态 pageAction.show(tabId); // 展示Page Action } PageActionIcon.prototype.init = function(){...} // 初始化icon PageActionIcon.prototype.active = function(){...} // icon切换为激活状态 PageActionIcon.prototype.hide = function(){...} // 隐藏icon PageActionIcon.prototype.setIcon = function(){ // 设置icon pageAction.setIcon({ // 设置icon的路径 tabId : this.tabId, path : icons[this.status] }); pageAction.setTitle({ // 设置icon的标题 tabId : this.tabId, title : tips[this.status] }); return this; }; PageActionIcon.prototype.restore = function(){// 刷新页面后,icon之前的状态会丢失,需要手动恢复 this.setIcon(); pageAction.show(this.tabId); return this; }; return PageActionIcon; })();
icon管理的准备工作ok了,剩下的就是使用了,如下。
new PageActionIcon(this.tabId).init();
对于IHeader扩展程序,一个标签页同时包含了监听器状态和icon状态的变化。因此需要再抽象出一个标签页控制器,对两者进行统一管理,从而供外部调用。代码如下。
/* 处理标签页状态 */ var TabControler = (function(){ var tabs = {}; // 所有的标签页控制器列表 function TabControler(tabId, url){ if(tabs[tabId]){ /* 如有就返回已有的实例 */ return tabs[tabId]; } if(!(this instanceof TabControler)){ /* 强制以构造器方式调用 */ return new TabControler(tabId); } /* 初始化属性 */ tabs[tabId] = this; this.tabId = tabId; this.url = url; this.init(); } trustauth.cn = function(tabId){...} // 获取指定的标签页控制器 TabControler.remove = function(tabId){ if(tabs[tabId]){ delete tabs[tabId]; // 移除指定的标签页控制器 ListenerControler.remove(tabId); // 移除指定的监听器控制器 } }; TabControler.prototype.init = function(){...} // 初始化标签页控制器 TabControler.prototype.switchActive = function(){ // 当前标签页状态切换 var icon = this.icon; if(icon){ var status = icon.status; var tabId = this.tabId; switch(status){ case ACTIVE: // 如果是激活状态,则恢复初始状态,移除监听器控制器 icon.init(); ListenerControler.remove(tabId); Message.send(tabId, 'ListeningCancel'); // 通知内容脚本从而在控制台输出取消提示(后续将讲到消息通信) break; default: // 如果不是激活状态,则激活之,添加监听器控制器 icon.active(); ListenerControler(tabId); Message.send(tabId, 'Listening'); // 并通知内容脚本从而在控制台输出监听提示 } } return this; }; TabControler.prototype.restore = function(){...} // 恢复标签页控制器的状态(针对页面刷新场景) TabControler.prototype.remove = function(){...} // 移除标签页控制器 return TabControler; })();
标签页控制器的抽象,有助于封装扩展的内部运行细节,方便了后续各种场景中对扩展的管理 。
标签页关闭或更新时,为了避免内存泄露和运行稳定,部分数据需要释放或者同步。刚刚封装好的标签页控制器就可以用来做这件事。
首先,Tab关闭时需要释放当前标签页的控制器和监听器对象。
/* 监听tab关闭的事件 */ chrome.tabs.onRemoved.addListener(function(tabId, removeInfo){ TabControler.remove(tabId); // 释放内存,移除标签页控制器和监听器 });
其次,每次Tab在执行跳转或刷新动作时,Page Action的icon都会回到初始状态并且不可点击,此时需要恢复icon之前的状态。
/* 监听tab更新的事件、包含跳转或刷新的动作 */ chrome.tabs.onUpdated.addListener(function(tabId, changeInfo){ if(changeInfo.status === 'loading'){ // 页面处于loading时触发 TabControler(tabId).restore(); // 恢复icon状态 } });
以上,页面跳转或刷新时,changeInfo将依次经历两种状态:loading
和complete
(部分页面会包含favIconUrl
或title
信息),如下所示。
随着状态管理的逐渐完善,那么,是时候进行消息通信了(不知道你注意到上述代码中出现的Message对象没有?它就是消息处理的对象)。
文章转载于:louis blog。作者:louis
原链接:http://louiszhai.github.io/2017/11/14/iheader/