有时候需要拦截网页的某个请求,收集响应数据,单个请求直接操作就好了;如果是多个相同 API 的请求,如果有个批处理数据的工具岂不美哉。于是,它来了。本来打算弄成一个油猴插件,但是浏览器总是会把插件的打印日志给 “吞掉”,那就先直接在控制台运行脚本,更简单明了。

介绍

用到了一个插件 ajaxhook,官方是这样介绍的:

XMLHttpRequest 对象发起请求之前、收到响应内容之后以及发生错误时获得处理权,通过它你可以提前对请求、响应以及错误进行一些预处理。

要想在网站上用其他的脚本,首先需要将它引入,这里是通过动态创建 script 标签的方式,将这个标签插入到当前页面中,然后浏览器会自动获取这个 JS 资源。

const script = document.createElement("script")
script.setAttribute("type", "text/javascript")
script.setAttribute(
  "src",
  "https://unpkg.com/ajax-hook@2.1.3/dist/ajaxhook.min.js"
)
document.documentElement.appendChild(script)

创建完标签还不行,还要检测当前是否已经引入完成了。引入完成后,ajaxhook 会在全局定义一个名叫 ah 的变量,那么就可以根据这个变量来判断。

通过在一个 Promise 实例里,定时检测是否 ah !== undefined 来判断。虽然看起来很笨拙,但是很实用 🤭。

new Promise((resolve, reject) => {
  try {
    var ajaxHookTimer = setInterval(() => {
      if (ah !== undefined) {
        clearInterval(ajaxHookTimer)
        resolve()
      }
    }, 500)
  } catch (e) {
    reject(e)
  }
})

脚本其余部分都是按照官网案例,可以自行查阅。

脚本

完整的脚本代码如下:

;(function () {
  /* --- 收集数据 Start --- */

  // 用来收集数据的数组对象
  const resData = []

  // 需要拦截的请求地址
  const url = ""

  // 自定义的数据格式化方法
  function formatData(data) {
    /* 处理 data */
    console.log(resData)
  }

  /* --- 收集数据 End --- */

  injectAjaxHook().then(() => {
    console.log("开启拦截")
    openIntercept()
  })

  // 注入第三方插件
  function injectAjaxHook() {
    console.log("拦截准备中...")
    const script = document.createElement("script")
    script.setAttribute("type", "text/javascript")
    script.setAttribute(
      "src",
      "https://unpkg.com/ajax-hook@2.1.3/dist/ajaxhook.min.js"
    )
    document.documentElement.appendChild(script)
    return new Promise((resolve, reject) => {
      try {
        var ajaxHookTimer = setInterval(() => {
          if (ah !== undefined) {
            clearInterval(ajaxHookTimer)
            resolve()
          }
        }, 500)
      } catch (e) {
        reject(e)
      }
    })
  }

  // 开启数据拦截
  function openIntercept() {
    ah.proxy(
      {
        // 请求发起前进入
        onRequest: (config, handler) => {
          handler.next(config)
        },
        // 请求发生错误时进入,比如超时;注意,不包括http状态码错误,如404仍然会认为请求成功
        onError: (err, handler) => {
          console.log(err.type)
          handler.next(err)
        },
        // 请求成功后进入
        onResponse: (res, handler) => {
          try {
            const url = res.config.url
            if (isUsefulUrl(url)) {
              const data = JSON.parse(res.response)
              formatData(data)
            }
            handler.next(res)
          } catch (e) {
            console.log(e)
          }
        }
      },
      window
    )

    // 是否是有效链接
    function isUsefulUrl(testUrl) {
      let path = testUrl.match(/(\.\w+)?(\/\w+)+(?=(\?|$))/)
      if (path) {
        path = path[0]
      }
      return testUrl.indexOf(url) > -1 || url.indexOf(path) > -1
    }
  }
})()

示例

以百度图片搜索为例,每次切换图片时,会发起这个请求。

假如需要收集响应数据中的 bfe_log_id 字段。

那么在收集数据部分就可以写成下面这样:

/* --- 收集数据 Start --- */

// 用来收集数据的数组对象
const resData = []

// 需要拦截的请求地址
const url = "https://image.baidu.com/simple/simplesearch"

// 自定义的数据格式化方法
function formatData(data) {
  resData.push(data.bfe_log_id)
  console.log(resData)
}

/* --- 收集数据 End --- */

打开控制台,粘贴脚本代码,回车。当控制台出现【开启拦截】时,说明拦截器已经加载完毕,就可以发请求了。

大功告成,快去试试吧。