售前咨询
技术支持
渠道合作

【Chrome扩展开发】定制HTTP请求响应头域(二)

Chrome Extension API

Chrome陆续向开发者开放了大量的API。使用这些API,我们可以监听或代理网络请求,存储数据,管理标签页和Cookie,绑定快捷键、设置右键菜单,添加通知和闹钟,获取CPU、电池、内存、显示器的信息等等(还有很多没有列举出来)。具体请阅读Chrome API官方文档。请注意,使用相应的API,往往需要申请对应的权限,如IHeader申请的权限如下所示。

"permissions": [ "tabs" , "webRequest", "webRequestBlocking", "http://*/*", "https://*/*", "contextMenus", "notifications"]

以上,IHeader依次申请了标签页、请求、请求断点、http网站,https网站,右键菜单,桌面通知的权限。

WebRequest API

Chrome Extension API中,能够修改请求的,只有chrome.webRequest了。webRequest能够为请求的不同阶段添加事件监听器,这些事件监听器,可以收集请求的详细信息,甚至修改或取消请求。

事件监听器只在特定阶段触发,它们的触发顺序如下所示。(图片来自MDN

事件监听器的含义如下所示。

  • onBeforeRequest,请求发送之前触发(请求的第1个事件,请求尚未创建,此时可以取消或者重定向请求)。
  • onBeforeSendHeaders,请求头发送之前触发(请求的第2个事件,此时可定制请求头,部分缓存等有关的请求头(Authorization、Cache-Control、Connection、Content-
    Length、Host、If-Modified-Since、If-None-Match、If-Range、Partial-Data、Pragma、Proxy-
    Authorization、Proxy-Connection和Transfer-Encoding)不出现在请求信息中,可以通过添加同名的key覆盖修改其值,但是不能删除)。
  • onSendHeaders,请求头发送之前触发(请求的第3个事件,此时只能查看请求信息,可以确认onBeforeSendHeaders事件中都修改了哪些请求头)。
  • onHeadersReceived,响应头收到之后触发(请求的第4个事件,此时可定制响应头,且只能修改或删除非缓存相关字段或添加字段,由于响应头允许多个同名字段同时存在,因此无法覆盖修改缓存相关的字段)。
  • onResponseStarted,响应内容开始传输之后触发(请求的第5个事件,此时只能查看响应信息,可以确认onHeadersReceived事件中都修改了哪些响应头)。
  • onCompleted,响应接受完成后触发(请求的第6个事件,此时只能查看响应信息)。
  • onBeforeRedirect,onHeadersReceived事件之后,请求重定向之前触发(此时只能查看响应头信息)。
  • onAuthRequired,onHeadersReceived事件之后,收到401或者407状态码时触发(此时可以取消请求、同步提供凭证或异步提供凭证)。

以上,凡是能够修改请求的事件监听器,都能够指定其extraInfoSpec参数数组中包含”blocking”字符串(意味着能阻塞请求并修改),反之则不行。

另外请注意,Chrome对于请求头和响应头的展示有着明确的规定,即控制台中只展示发送出去或刚接收到的字段。因此编辑后的请求字段,控制台的network栏能够正常展示;而编辑后的响应字段由于不属于刚接收到的字段,所以从控制台上就会看不到编辑的痕迹,如同没修改过一样,实际上编辑仍然有效。

事件监听器含义虽不同,但语法却一致。接下来我们就以onHeadersReceived为例,进行深入分析。

如何绑定header监听

还记得我们的目标吗?想要去掉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表示添加一个接收响应头的监听。以上代码中的关键参数或属性,下面逐一讲解。

  • callback,即事件触发时的回调,该回调默认传入一个参数(details),details就是请求的详情。
  • filter,Object类型,限制事件回调callback触发的过滤器。filter有四个属性可以指定,分别为①urls(包含指定url的数组)、②types(请求的类型,共8种)、③tabId(标签页id)、④windowId(窗口id)。
  • extraInfoSpec,数组类型,指的是额外的选项列表。对于headersReceived事件而言,包含”blocking”,意味着要求请求同步,基于此才可以修改响应头;包含”responseHeaders”意味着事件回调的默认参数details中将包含responseHeaders字段,该字段指向响应头列表。

既然有了添加监听的方法,自然,还会有移除监听的方法。

chrome.webRequest.onHeadersReceived.removeListener(listener);

除此之外,为了避免重复监听,还可以判断监听是否已经存在。

var bool = chrome.webRequest.onHeadersReceived.hasListener(listener);

为了保证更好的理清以上属性、方法或参数的逻辑关系,请看如下脑图:

扩展状态管理

监听器的状态管理

知道了如何绑定监听器,仅仅是第一步。监听器需要在合适的时机绑定,也需要在合适的时机解绑。为了不影响Chrome的访问速度,我们只在需要的标签页创建新的监听器,因此监听器需要依赖filter来区分不同的tabId,考虑到用户可能只需要监听一部分请求类型,types的区分也是不可避免的。又由于一个Tab里不同的时间段可能会加载不同的页面,一个监听器在不同的页面下正常运行也是必须的(因此监听器的filter中不需要指定urls)。

寥寥数语,可能不足以描述出监听器状态管理的原貌,请看下图进一步帮助理解。

以上,一个请求将依次触发上述①②③④⑤五个事件回调,每个事件回调都对应着一个监听器,这些监听器分为两类(从颜色上也可看出端倪)。

  • ②③⑤监听器的主要功能是记录,用于监听页面上每一个Request的请求头和响应头,以及请求响应时间。
  • ①④监听器的主要功能是更新,用于增加、删除或修改指定Request的请求头和响应头。

若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图标状态管理

标签页控制器的状态需要由视觉体现出来,因此Page Action图标的管理也是不可避免的。通常,默认的icon可以在manifest.json中指定。

"page_action": {
  "default_icon": "res/images/lightning_default.png", // 默认图标
},

icon有如下3种状态(后两种状态可以互相切换)。

  • 默认状态,展示默认的icon。
  • 初始状态,展示扩展初始化后的icon。
  • 激活状态,展示扩展激活后的icon。

Chrome提供了chrome.pageAction的API供Page Action使用。目前chrome.pageAction拥有如下方法。

  • show,在指定的tab下展示Page Action。
  • hide,在指定的tab下隐藏Page Action。
  • setTitle,设置Page Action的标题(鼠标移动到该Page Action上时会出现设置好的标题提示)
  • getTitle,获取Page Action的标题。
  • setIcon,设置Page Action的图标。
  • setPopup,设置点击时弹出页面的URL。
  • getPopup,获取点击时弹出页面的URL。

以上,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();
  }
  TabControler.get = 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(部分页面会包含favIconUrltitle信息),如下所示。

随着状态管理的逐渐完善,那么,是时候进行消息通信了(不知道你注意到上述代码中出现的Message对象没有?它就是消息处理的对象)。

文章转载于:louis blog。作者:louis

原链接:http://louiszhai.github.io/2017/11/14/iheader/

上一篇:

下一篇:

相关文章