Chrome 请求过滤扩展实现

799 阅读6分钟

引子

接着 Chrome 扩展 : 入门,接下来开始实现一开始自己的想法:网络请求过滤。简单的说就是监听某个网站的所有请求,把想要的请求在扩展插件中展示出来。扩展名为 Capture Request 。

需求具体化

上面的想法比较模糊,为了达到这个目的,结合文档的示例,要做的有:

  1. 扩展要有对应的图标及提示。
  2. 点击工具栏扩展图标,打开一个新的 Tab 页面,用来展示请求的相关信息。
  3. 扩展监听处于激活 Tab 的网站请求,可以配置过滤监听的网址。
  4. 对监听的请求,支持根据 url 筛选并导出。

有些功能不方便直接在文档找到,这个时候,建议在 Chrome 商店找一个开源扩展,根据效果看里面用的一些 API ,然后找到对应文档。这里参考了 FeHelper 里面的一些实现。需要注意到是 FeHelper 开发基于 manifest_version 版本为 2 ,以下开发扩展基于的版本是推荐的版本为 3 ,完整代码见 Capture Request

实现

图标相关信息配置

按照入门里面介绍的信息,图标可能出现的地方有工具栏、扩展管理页、权限警告和 favicon 上,在 manifest.json 中配置的下面相关字段:

{
  "name": "Capture Request",
  "description": "Capture Request",
  "version": "1.0",
  "manifest_version": 3,
  "action": {
    "default_icon": {
      "16": "xxx.png",
      "32": "xxx.png",
    }
  },
  "icons":{
    "16": "xxx.png",
    "32": "xxx.png",
  }
}

什么尺寸图标用在什么地方的详细说明在这里,文档上推荐用 PNG 的图片格式。按照这个来,发现有的图标太小了会看起来明显模糊,也可以用比较大的尺寸,Chrome 会自己把图片压缩到需要的尺寸。

点击扩展打开新 Tab 页面

入门里面点击扩展的展现形式是打开了一个弹窗,在文档 Design the user interface 中介绍的形式有 Popup 、Tooltip、Omnibox、Context menu、Override pages ,最有可能就是 Override pages ,但试了一下发现没有效果,于是去看别人的实现,发现可以通过监听点击图标事件打开新 Tab 。

但看似可以直接用的 API ,实际上还有下面几点要考虑:

  1. 在哪里监听这事件?
  2. 什么时候监听这个事件?
  3. 怎么打开新 Tab ?
  4. 是否需要权限,如果需要,涉及那些权限?

入门里面添加功能是通过后台脚本,文档开头说的一句个人觉得很重要:

扩展是通过基于事件编程来改变或增强 Chrome 浏览体验。

从文档中可以发现,在后台脚本中可以解决上面提的第 1、2 两个问题,需要的权限是 scripting

打开 Tab 使用的 API 是 chrome.tabs ,需要的权限是 tabs

主要做法是在 manifest.json 中添加下面配置:

{
  ...
+ "permissions": [
+   "scripting",
+   "tabs",
+ ],
+ "background": {
+   "service_worker": "background.js"
+ },
  ...
}

然后新建后台脚本文件 background.js ,并添加下面主要逻辑代码:

chrome.action.onClicked.addListener(() => {
  chrome.tabs.create({
    url: 'page.html'
  });
});

监听请求及配置

要把处于激活 Tab 网站上的请求显示到打开的扩展页面上,主要需要考虑的点有:

  1. 怎么找到激活的 Tab ?
  2. 怎么截获网页请求?
  3. 截获的请求怎么同步到扩展自定义页面上?

通过上面打开 Tab 的效果实现,可以联想到相关的 API 应该也在 chrome.tabs 中,发现提供了 query 方法可以解决第 1 个问题。

截获请求的方法通过网上搜索,发现文档 chrome.webRequest ,里面详细的介绍了扩展中请求的生命周期及触发的事件,经过对比思考,个人最后决定监听 onResponseStarted 事件,需要的权限是 webRequest 。这样就解决了第 2 个问题。

参照入门里面改变颜色的方式,类似的可以把请求缓存到 chrome.storage ,然后在扩展页面获取,需要的权限是 storage 。关于数据同步,可以通过监听 chrome.storage.onChanged 事件拿到变动的最新数据。这样就解决了第 3 个问题。

在调试的过程中,发现存在本地的数据使用 chrome.storage.sync 时,请求达到一定量后,会报错。看了文档发现这种方式的最大值有一定的限制,不太适合存储大量请求数据的场景,使用 chrome.storage.local 更加合适。

配置过滤请求的方式可直接按照入门里面的配置方式处理,但有一点需要注意的是,每当配置更新的时候,需要重新监听 onResponseStarted 事件。

主要做法是在 manifest.jsonpermissions 字段中添加 webRequeststorage

background.js 中添加主要代码:

// 储存请求数据默认值
let requestList = []
// 网址过滤的默认值
let urlPattern = '<all_urls>'

// 监听请求事件的处理程序
const handlerResponseStarted = (details) => {
  // 找到处于激活状态的 Tab
  chrome.tabs.query({ active: true }, (tab) => {
    requestList.unshift(details)
    chrome.storage.local.set({ requestList });
    return { cancel: true };
  })
}

// 监听 storage 改变事件
chrome.storage.onChanged.addListener((changeObj, areaName) => {
  const { urlPattern } = changeObj
  // 由于在 page.html 里面也监听了,所以要判断是不是 urlPattern 变动了
  if (areaName !== 'local' || !urlPattern) {
    console.warn('urlPattern does not change')
    return;
  }
  const { newValue } = urlPattern
  const hasAddListen = chrome.webRequest.onResponseStarted.hasListener(handlerResponseStarted)
  if (hasAddListen) {
    chrome.webRequest.onResponseStarted.removeListener(handlerResponseStarted);
  }
  chrome.webRequest.onResponseStarted.addListener(
    handlerResponseStarted,
    { urls: [newValue] },
  );
})

为扩展页面 page.html 添加脚本文件 pages.js ,添加关键逻辑:

  chrome.storage.onChanged.addListener((changeObj, areaName) => {
    const { requestList } = changeObj
    // 由于在 background.js 里面也监听了,所以要判断是不是 requestList 变动了
    if (areaName !== 'local' || !requestList) {
      console.warn('requestList does not change')
      return;
    }
    const { newValue } = requestList || { newValue: [] }
    const newItem = newValue[0] || null
    if (!newItem) {
      console.warn('no data')
      return;
    }
    // 显示数据的逻辑
    showData(newItem)
  })

导出数据

截获了想要的数据,有需要导出到本地使用的场景,参考 FeHelper 里面的实现,找到了文档 chrome.downloads ,需要的权限是 downloads

主要做法是在 manifest.jsonpermissions 字段中添加 downloads

pages.js 添加关键逻辑:

  let localFilterList = []; // 页面筛选后的数据
  // 点击导出的按钮
  const exportEle = document.querySelector('#operate-export')
  exportEle.addEventListener('click', () => {
    if (!localFilterList.length) {
      alert('无有效数据')
      return;
    }
    const txt = JSON.stringify(localFilterList)
    let blob = new Blob([txt], { type: 'application/octet-stream' });
    // 文件名称获取时间的秒数,可按照自己喜好定义
    let dt = (new Date()).getSeconds();
    chrome.downloads.download({
      url: URL.createObjectURL(blob),
      saveAs: true,
      conflictAction: 'overwrite',
      filename: dt + '.json'
    });
  })

参考资料

wastebasket

最近看了下电视剧《围城》,虽然年代很早,但真是有趣,据说是完全按照原著拍的,没有任何改编。

在小说的最后一段才讲的是婚姻围城,不知道什么时候开始起,我一直以为整本书讲的是关于结婚的围城。

83-poster