添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

浅谈H5 HTTP请求拦截

背景

在前端开发中,经常会遇到针对全局HTTP请求拦截的需求,比如底层统一检测第三方代码发送的请求是否合规,对不合规请求进行拦截;底层统一进行接口加固的改造,需要对请求添加额外参数;进行统一埋点统计或者性能监控等场景。

前端发起HTTP请求的方式

要拦截HTTP请求,首先就要搞清楚有哪些发送HTTP请求的方式。翻阅资料了解到前端大致有以下几种方式可以发起HTTP请求:

  • XMLHttpRequest
  • fetch
  • jQuery.ajax(底层基于 XMLHttpRequest 进行封装)
  • axios(H5端底层基 XMLHttpRequest fetch 进行封装)
  • Taro.request(H5端底层基于 fetch 进行封装)
  • JSONP(通过 <script> 标签方式获取数据) 以上几种请求方式,只有 XMLHttpRequest fetch JSONP 三种方式是最底层的发起请求的方法,像jQuery.ajax,axios和Taro.request基本上都是通过对上面某种方式的二次封装。
  • HTTP请求拦截

    通过以上分析,我们知道只需要对 XMLHttpRequest fetch JSONP 这三种底层的方式进行请求拦截即可满足大部分的场景。 我们可以在以下三个阶段调用一些钩子函数以获得对请求处理权:

  • onRequest 发送请求之前
  • onResponse 收到响应之后
  • onError 发生错误时
  • 当然像 jquery axios Taro.request 这些库本身也都提供了一些类似的拦截器设计,他们大致可以分为发送请求之前和响应返回之后(包括成功和失败),但是他们属于上层库的实现,无法从全局对所有HTTP请求进行拦截。

    XMLHttpRequest拦截

    XMLHttpRequest使用示例

    var xhr = new XMLHttpRequest();
    xhr.open("POST", "https://example.com/api/data", true);
    xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4 && xhr.status === 200) {
        var response = JSON.parse(xhr.responseText);
        console.log(response);
    var params = "username=test&password=12345"; // 参数字符串
    xhr.send(params);
     * 检测是否需要触发请求
     * @param {*} thisObj 
     * @param {*} onRequest 
     * @param {*} config 
     * @returns 
    const checkNeedRequest = (thisObj, onRequest, config) => {
        return new Promise((resolve) => {
            let needRequest = true
            if (typeof onRequest === "function") {
                // sendHookResult=false才会阻止发送请求,否则都发送请求
                let sendHookResult = onRequest.apply(thisObj, [config])
                if (sendHookResult instanceof Promise) {
                    sendHookResult.then((realResult) => {
                        needRequest = realResult
                        resolve(needRequest)
                        .catch((error) => {
                            resolve(needRequest)
                else {
                    needRequest = sendHookResult
                    resolve(needRequest)
            else {
                resolve(needRequest)
     * 拦截XMLHttpRequest
     * @param window
     * @param {*} option
    function XMLHttpRequestInterceptor(window, option) {
        if (window.XMLHttpRequest) {
            const { onRequest } = option
            // 保存原始函数
            const originalSend = window.XMLHttpRequest.prototype.send
            const originalOpen = window.XMLHttpRequest.prototype.open
            const originalAbort = window.XMLHttpRequest.prototype.abort
            window.XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
                this.method = method
                this.url = url
                this.async = async
                this.user = user
                this.password = password
                originalOpen.apply(this, arguments)
            window.XMLHttpRequest.prototype.abort = function () {
                // 已经调用了abort方法的标识
                this._abort = true
                originalAbort.apply(this, arguments)
            // 重新发送方法
            window.XMLHttpRequest.prototype.send = function (body) {
                console.log("XMLHttpRequest url:", this.url)
                const xhr = this
                xhr.body = body
                xhr.addEventListener('abort', (e) => {
                    console.log("e:", e)
                    // 已经收到请求终止事件的标识,可能由多种愿意触发,比如调用了abort()方法,获取其他方式
                    xhr._aborted = true
                const config = { type: "XMLHttpRequest", url: xhr.url, body, method: xhr.method, async: xhr.async, user: xhr.user, password: xhr.password }
                checkNeedRequest(xhr, onRequest, config).then((needRequest) => {
                    xhr._needSend = needRequest
                    if (needRequest) {
                        // 是否调用了send()方法
                        xhr._send = true
                        originalSend.apply(xhr, arguments)
        else {
            console.error("window.XMLHttpRequest is null")
    

    以上代码展示了一个基本的拦截方案。 拦截XMLHttpRequest请求的整体思路是实现一个XMLHttpRequest的代理对象,然后覆盖全局的XMLHttpRequest,这样一但上层调用new XMLHttpRequest这样的代码时,其实创建的是我们代理对象实例,就可以在发送请求之前,以及返回后进行一些预处理。 其实对于XMLHttpRequest的拦截,业内有一些比较优秀的实现方案,比如:Ajax-hook,这个库已经被多个大厂使用,相关代码也已经达到商用的标准。实现细节和详细原理也可以参考文档Ajax-hook原理解析。 需要注意的是:因为xhr的responseText属性并不是writeable的,这也就意味着你无法直接更改xhr.responseText的值,而Ajax-hook最新版放弃了直接重写XMLHttpRequest.prototype的实现方式,转而代理XMLHttpRequest的实例就不会有这个问题。

    fetch拦截

    fetch使用示例

    fetch('http://example.com/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json', body: JSON.stringify({ name: 'John' }), .then(response => response.json()) .then(data => { // 请求成功的处理逻辑 console.log(data); .catch(error => { // 请求失败的处理逻辑 console.error(error);
    // fetch-proxy.js
    var prototype = 'prototype'
    export default function fetchProxy(proxy, win) {
      win = win || window;
      return proxyFetch(proxy, win)
    function Handler(resolve, reject) {
      this._resolve = resolve
      this._reject = reject
    Handler[prototype] = Object.create({
      resolve: function resolve(res) {
        this._resolve && this._resolve(res.response)
      reject: function reject(err) {
        this._reject && this._reject(err.error)
    function makeHandler(next) {
      function sub(resolve, reject) {
        Handler.call(this, resolve, reject)
      sub[prototype] = Object.create(Handler[prototype])
      sub[prototype].next = next
      return sub
    function proxyFetch(proxy, win) {
      const onRequest = proxy.onRequest
      const onResponse = proxy.onResponse
      const onError = proxy.onError
      // 保存原始函数
      const originalFetch = win.fetch
      if (!originalFetch) {
        console.error("fetch is null")
        return {}
       * @param input url字符串或者Request对象
       * @param init 配置项
      win.fetch = function (input, init) {
        let config = { input, init }
        return new Promise((resolve, reject) => {
          let RequestHandler = makeHandler(function (cfg) {
            cfg = cfg || config;
            callFetch(cfg)
          let ResponseHandler = makeHandler(function (res) {
            this.resolve(res)
          let ErrorHandler = makeHandler(function (err) {
            this.reject(err)
          // 调用真实请求接口
          const callFetch = (config) => {
            originalFetch.apply(this, [config.input, config.init]).then((response) => {
              let handler = new ResponseHandler(resolve, reject)
              let ret = {
                config: config,
                response: response,
              if (!onResponse) {
                return handler.resolve(ret)
              onResponse(ret, handler)
            }).catch((error) => {
              let handler = new ErrorHandler(resolve, reject)
              let err = { config: config, error: error }
              if (onError) {
                onError(err, handler)
              } else {
                handler.next(err)
          if (onRequest) {
            onRequest(config, new RequestHandler(resolve, reject))
          else {
            callFetch(config)
      function unProxy() {
        win.fetch = originalFetch
        originalFetch = undefined
      return {
        originalFetch,
        unProxy
    

    以下是使用fetchProxy的示例

    // fetchProxy使用展示
    const {originalFetch, unProxy} = fetchProxy({
      //请求发起前进入
      onRequest: (config, handler) => {
        setTimeout(() => {
          handler.next(config)
        }, 1000)
      //请求发生错误时进入
      onError: (err, handler) => {
        handler.next(err)
      //请求成功后进入
      onResponse: (response, handler) => {
        handler.next(response)
    

    拦截fetch请求的整体思路是实现一个fetch的代理对象,然后覆盖全局的fetch,这样一但上层调用fetch()这样的代码时,其实调用的是我们代理函数,就可以在发送请求之前,以及返回后进行一些预处理。

    JSONP拦截

    JSONP实现基本原理

    JSONP实现跨域请求的原理简单的说,就是动态创建<script>标签,然后利用<script>src不受同源策略约束来跨域获取数据。

    1、动态方式

    function jsonpCallback(data) {
      // 在这里处理响应数据
    var scriptElement = document.createElement('script');
    scriptElement.src = "//api.domain.com/getData?callback=jsonpCallback";
    document.body.appendChild(scriptElement);
    document.body.removeChild(scriptElement); // 请求发送后立即删除 <script> 元素
    // 服务器将会返回类似于以下的响应:
    jsonpCallback({ "name": "John", "age": 30 })
    

    以上代码展示了通过动态创建<script>标签的方式实现JSONP的请求。由于浏览器加载<script>标签返回的数据默认会按照javascript代码执行,所以返回的字符串中的jsonpCallback会被当做函数执行,因此我们可以在全局定义的函数jsonpCallback中获取到后端传递过来的数据data

    根据以上实现方式,整个加载流程分为以下几个阶段:

  • 创建<script>标签:document.createElement
  • 赋值src属性
  • 添加子节点到dom树的方法:appendChildinsertBeforeinsertAdjacentElement
  • 加载完成事件onload
  • 加载报错事件onerror
  • 可以发现,对于动态创建<script>标签的JSONP实现方案,我们唯一能做的就只能是创建和赋值src阶段进行拦截,当加载完成时回调函数的代码会自动执行,我们无法干涉。

    // jsonp-proxy.js 拦截JSONP请求示例
    (function () {
      var originalCreateElement = document.createElement
      function proxy(dom) {
        var src
        Object.defineProperty(dom, 'src', {
          get: function () {
            return src
          set: function (newVal) {
            src = newVal
            dom.setAttribute('src', newVal)
        var originalSetAttribute = dom.setAttribute
        dom.setAttribute = function () {
          var args = Array.prototype.slice.call(arguments)
          var key = args[0]
          var val = args[1]
          // 根据val即url地址上是否存在callback=回调函数字样粗略的判断,是否请求的是JSONP的调用,但是此方法不是很可靠,因为像jquery里面的JSONP实现是可以指定callback的命名的。
          if (key === 'src' && val.includes('callback=')) {
            let url = val
            console.log('请求地址:', url)
            // 在调用之前做一些事情,比如修改url
            originalSetAttribute.apply(dom, [key, url])
          else {
            originalSetAttribute.apply(dom, args)
      // 重写创建节点函数
      document.createElement = function (tagName) {
        var dom = originalCreateElement.call(document, tagName)
        dom.onload = function (evt) {
          console.log("onload:", evt)
        dom.onerror = function (err) {
          console.log("onerror:", err)
        if(tagName.toLowerCase() === 'script'){
          proxy(dom)
        return dom
    

    2、静态方式

    不排除也可以通过非动态创建的<script>标签实现JSONP的调用,示例如下:

    <script type="text/javascript">
        function jsonpCallback(data) {
          // 在这里处理响应数据
    </script>
    <script type="text/javascript" src="//api.domain.com/getData?callback=jsonpCallback"></script>
    // 服务器将会返回类似于以下的响应:
    jsonpCallback({ "name": "John", "age": 30 })
    

    由于<script>标签已经提前放在了HTML页面上,所以我们很难干涉这样的JSONP的请求,目前暂无办法进行拦截。

    由以上分析可以知道,目前在XMLHttpRequestfetch的拦截都可以做到onRequest发送请求之前、onResponse收到响应之后和onError发送错误时三个阶段的完全拦截进行预处理。但是针对JSONP的请求,目前只能对动态方式进行onRequest发送请求之前的部分拦截。 不过综合以上三种拦截方式的实现,已经可以满足绝大部分场景的拦截需求了。

    重写底层请求组件可能带来的问题:前端上层应用埋点或性能监控引入的库本身就会重写,二者容易冲突,复写大概率也不怎么健壮,因此使用时需要考虑到兼容性问题。

    全部评论
    空 还没有回复哦~

    相关推荐

    淘天转正给我挂了,意料之中,但还是很伤心,我很怀疑自己&nbsp;你很优秀,可惜我们今年最不缺的就是优秀的人&nbsp;我是呢种很容易自我伤害的人,如果我的朋友背叛了我,我第一反应不是愤怒,而是自责,责备自己认人不清&nbsp;陌生的城市很大,大到没有容纳自己的地方&nbsp;出门的路口拐过去也不知道会到哪里去&nbsp;这一切就像一个巨大的他妈的袋鼠,狠狠的给了我一拳&nbsp;我觉得这样不好,我这样的人不适合呆在互联网,我的一个同学就很适合,我原以为她不适合的&nbsp;有一天面试,面试官问我,如果明天项目就要上线了,今天出现了一个重大严重bug,加班也解决不了,你怎么办?&nbsp;我叹了口气,想了想说,准备一下简历,晚上加班投一波&nbsp;最近秋招运气很差,和...
    榨出爱国基因: 很简单,我进厂不就是了,说完,他的气息不再掩饰,显露而出,再回流水线,竟是大专巅峰修为!一瞬间,流水线再次一寂,只见他挥手间就飞出三只蛊虫,一转苦力蛊,二转牛马蛊,三转吗喽蛊! 有趣,真有趣,精彩,实在是精彩。 我乃大专巅峰!谁敢叼我 谁能叼我! 又听他低吟: 电子厂中寒风吹 流水线上大神归 无休倒班万人退,本科悔而我不悔! 他牢牢占据工位,转身低眉道:不过是些许夜班罢了
    点赞 评论 收藏
    分享
    头像
    10-16 09:14
    已编辑
    门头沟学院 C++
    影石 音视频渲染 n*15
    这offer是俺拾嘞: 华为可以
    点赞 评论 收藏
    分享
    牛客775778651号: 我导师已经快一年没找我说话了,我也没找他说话,他是不是忘了还有我这个学生
    点赞 评论 收藏
    分享
    Skywalker_lgy: 这种啥毕还回他干啥,头像都暗示你拉黑了
    点赞 评论 收藏
    分享
    点赞 收藏 评论
    分享

    全站热榜