【问题标题】:How can I modify the XMLHttpRequest responsetext received by another function?如何修改另一个函数接收到的 XMLHttpRequest 响应文本?
【发布时间】:2014-10-19 04:34:09
【问题描述】:

我正在尝试修改我无法修改的函数接收到的 responseText。此函数创建一个我可以附加到的 XMLHttpRequest,但我无法以一种允许我在原始函数接收到内容之前修改内容的方式“包装”responseText。

这是完整的原始功能:

function Mj(a, b, c, d, e) {
    function k() {
        4 == (m && 'readyState' in m ? m.readyState : 0) && b && ff(b) (m)
    }
    var m = new XMLHttpRequest;
    'onloadend' in m ? m.addEventListener('loadend', k, !1)  : m.onreadystatechange = k;
    c = ('GET').toUpperCase();
    d = d || '';
    m.open(c, a, !0);
    m.send(d);
    return m
}
function ff(a) {
    return a && window ? function () {
        try {
            return a.apply(this, arguments)
        } catch(b) {
            throw jf(b),
                b;
        }
    } : a
}

我也尝试过操纵接收函数 k();试图达到我的目标,但由于它不依赖于传递给函数的任何数据(例如 k(a.responseText);)我没有成功。

有什么方法可以实现吗?我不希望使用 js 库(如 jQuery);


编辑:我知道我无法直接更改 .responseText,因为它是只读的,但我正在尝试找到一种方法来更改响应和接收函数之间的内容。


EDIT2:在我尝试拦截和更改的方法之一下方添加了已从此处添加的 .responseText:Monkey patch XMLHTTPRequest.onreadystatechange

(function (open) {
XMLHttpRequest.prototype.open = function (method, url, async, user, pass) {
    if(/results/.test(url)) {
      console.log(this.onreadystatechange);
        this.addEventListener("readystatechange", function () {
            console.log('readystate: ' + this.readyState);
            if(this.responseText !== '') {
                this.responseText = this.responseText.split('&')[0];
            }
        }, false);
    }
    open.call(this, method, url, async, user, pass);
};
})(XMLHttpRequest.prototype.open);

EDIT3:我忘了包括函数 Mj 和 ff 不是全局可用的,它们都包含在一个匿名函数中 (function(){functions are here})();


EDIT4:我更改了接受的答案,因为 AmmarCSE 没有任何与 jfriend00 的答案相关的问题和复杂性。

简而言之,最佳答案如下:

侦听您要修改的任何请求(确保您的侦听器会在原始函数目标之前拦截它,否则在响应已经使用后修改它没有意义)。

将原始响应(如果要修改)保存在临时变量中

将要修改的属性更改为“可写:真”,它将擦除它拥有的任何值。就我而言,我使用

Object.defineProperty(event, 'responseText', {
    writable: true
});

其中event是监听xhr请求的loadreadystatechange事件返回的对象

现在您可以为响应设置任何您想要的内容,如果您只想修改原始响应,那么您可以使用临时变量中的数据,然后将修改保存在响应中。

【问题讨论】:

  • 正如this meta thread 所说,答案不应该是问题的一部分,而应该是他们自己的答案。我已将您的答案移至 to a community wiki answer(因此我无法从中获得代表)并对其进行了编辑。如果您想改写帖子,可以编辑社区 wiki 答案。
  • @MT0 感谢帮助和信息,社区 wiki 的回答听起来不错。

标签: javascript ajax xmlhttprequest monkeypatching


【解决方案1】:

编辑:请参阅下面的第二个代码选项(它已经过测试并且可以工作)。第一个有一些限制。


由于您无法修改任何这些函数,看来您必须使用 XMLHttpRequest 原型。这是一个想法(未经测试,但您可以看到方向):

(function() {
    var open = XMLHttpRequest.prototype.open;

    XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
        var oldReady;
        if (async) {   
            oldReady = this.onreadystatechange;
            // override onReadyStateChange
            this.onreadystatechange = function() {
                if (this.readyState == 4) {
                    // this.responseText is the ajax result
                    // create a dummay ajax object so we can modify responseText
                    var self = this;
                    var dummy = {};
                    ["statusText", "status", "readyState", "responseType"].forEach(function(item) {
                        dummy[item] = self[item];
                    });
                    dummy.responseText = '{"msg": "Hello"}';
                    return oldReady.call(dummy);
                } else {
                    // call original onreadystatechange handler
                    return oldReady.apply(this, arguments);
                }
            }
        } 
        // call original open method
        return open.apply(this, arguments);
    }

})();

这会为 XMLHttpRequest open() 方法做一个猴子补丁,然后当为异步请求调用它时,它会为 onReadyStateChange 处理程序做一个猴子补丁,因为它应该已经设置了。然后,在调用原始 onReadyStateChange 处理程序之前,该修补函数会看到 responseText,因此它可以为其分配不同的值。

而且,最后因为.responseText 是仅就绪的,这会在调用onreadystatechange 处理程序之前替换一个虚拟的XMLHttpResponse 对象。这并非在所有情况下都有效,但如果 onreadystatechange 处理程序使用 this.responseText 来获取响应,则将有效。


而且,这里尝试将 XMLHttpRequest 对象重新定义为我们自己的代理对象。因为它是我们自己的代理对象,所以我们可以将responseText 属性设置为我们想要的任何值。对于除onreadystatechange 之外的所有其他属性,此对象只是将get、set 或函数调用转发给真正的XMLHttpRequest 对象。

(function() {
    // create XMLHttpRequest proxy object
    var oldXMLHttpRequest = XMLHttpRequest;

    // define constructor for my proxy object
    XMLHttpRequest = function() {
        var actual = new oldXMLHttpRequest();
        var self = this;

        this.onreadystatechange = null;

        // this is the actual handler on the real XMLHttpRequest object
        actual.onreadystatechange = function() {
            if (this.readyState == 4) {
                // actual.responseText is the ajax result

                // add your own code here to read the real ajax result
                // from actual.responseText and then put whatever result you want
                // the caller to see in self.responseText
                // this next line of code is a dummy line to be replaced
                self.responseText = '{"msg": "Hello"}';
            }
            if (self.onreadystatechange) {
                return self.onreadystatechange();
            }
        };

        // add all proxy getters
        ["status", "statusText", "responseType", "response",
         "readyState", "responseXML", "upload"].forEach(function(item) {
            Object.defineProperty(self, item, {
                get: function() {return actual[item];}
            });
        });

        // add all proxy getters/setters
        ["ontimeout, timeout", "withCredentials", "onload", "onerror", "onprogress"].forEach(function(item) {
            Object.defineProperty(self, item, {
                get: function() {return actual[item];},
                set: function(val) {actual[item] = val;}
            });
        });

        // add all pure proxy pass-through methods
        ["addEventListener", "send", "open", "abort", "getAllResponseHeaders",
         "getResponseHeader", "overrideMimeType", "setRequestHeader"].forEach(function(item) {
            Object.defineProperty(self, item, {
                value: function() {return actual[item].apply(actual, arguments);}
            });
        });
    }
})();

工作演示:http://jsfiddle.net/jfriend00/jws6g691/

我在最新版本的 IE、Firefox 和 Chrome 中进行了尝试,它可以处理一个简单的 ajax 请求。

注意:我还没有研究 Ajax 的所有高级方式(如二进制数据、上传等),以查看该代理是否足够彻底以使所有这些工作(我猜它可能还不是没有进一步的工作,但它适用于基本请求,所以看起来这个概念是有能力的)。


其他失败的尝试:

  1. 尝试从 XMLHttpRequest 对象派生,然后用我自己的构造函数替换,但这不起作用,因为真正的 XMLHttpRequest 函数不允许您将其作为函数调用来初始化我的派生对象。

  2. 尝试覆盖 onreadystatechange 处理程序并更改 .responseText,但该字段是只读的,因此您无法更改。

  3. 尝试创建一个虚拟对象,在调用onreadystatechange 时作为this 对象发送,但很多代码没有引用this,而是将实际对象保存在局部变量中在闭包中 - 从而击败了虚拟对象。

【讨论】:

  • 但我无法为 responseText 分配新值,因为它是只读的,否则我以前的方法(与您共享的方法非常相似)会起作用。尽管如此,我还是尝试了您的代码,并且 responseText 值按预期保持不变。
  • @Shadow - 我正在努力。差不多好了。我收到后会给你评论。
  • @Shadow - 在我的回答中查看我的第二个代码块。我创建了一个代理对象,用我自己的代理对象构造函数替换 XMLHttpRequest 构造函数,然后我可以修改.responseText
  • @Shadow - 这是一个更安全的版本(特别是供将来使用),它代理 XMLHttpRequest 对象上的所有属性(通过迭代它们)而不是仅列出特定的属性列表:jsfiddle.net/jfriend00/banxvu2c
  • @Shadow - 你会注意到我添加了对 .onload.addEventListener("load", ...) 的支持,这是查看 xhr 请求何时完成的另外两种方法。你的代码没有使用这些,但我想我会让它更完整。
【解决方案2】:

一个非常简单的解决方法是更改​​responseText 本身的属性描述符

Object.defineProperty(wrapped, 'responseText', {
     writable: true
});

所以,你可以像XMLHttpRequest一样扩展

(function(proxied) {
    XMLHttpRequest = function() {
        //cannot use apply directly since we want a 'new' version
        var wrapped = new(Function.prototype.bind.apply(proxied, arguments));

        Object.defineProperty(wrapped, 'responseText', {
            writable: true
        });

        return wrapped;
    };
})(XMLHttpRequest);

Demo

【讨论】:

  • 我一直想知道这种方法是否可行并且可能更容易,但是每当我尝试通过 defineProperty 更改它时,我都会以无穷无尽的递归循环结束(responseText 更改 responseText 从而更改 responseText 等.)。我一定会试一试,它的简单性非常适合预期用途。
  • 此选项似乎不合适,因为它完全清空了 responseText 返回的任何内容,结果是 responseText 始终返回“未定义”,尽管能够为其添加值。目的是修改 responseText,而不是删除它并用新值完全替换。
  • 我终于想出了如何使这项工作。在拦截期间,特定的 XMLHttpRequest 必须覆盖其属性,就在需要修改属性之前。如果在此之前完成,则 responseText 值会丢失。
  • @Shadow 你有一个 sn-p 来展示你的最终解决方案是如何完成的吗?
  • @Crwth 我在 EDIT5 中的问题末尾包含了一个示例 sn-p
【解决方案3】:

我需要拦截和修改请求响应,所以我想出了一点代码。我还发现有些网站喜欢使用 response 和 responseText,这就是我的代码同时修改两者的原因。

守则

var open_prototype = XMLHttpRequest.prototype.open,
intercept_response = function(urlpattern, callback) {
   XMLHttpRequest.prototype.open = function() {
      arguments['1'].match(urlpattern) && this.addEventListener('readystatechange', function(event) {
         if ( this.readyState === 4 ) {
            var response = callback(event.target.responseText);
            Object.defineProperty(this, 'response',     {writable: true});
            Object.defineProperty(this, 'responseText', {writable: true});
            this.response = this.responseText = response;
         }
      });
      return open_prototype.apply(this, arguments);
   };
};

intercept_response 函数的第一个参数是匹配请求 url 的正则表达式,第二个参数是用于修改响应的函数。

使用示例

intercept_response(/fruit\.json/i, function(response) {
   var new_response = response.replace('banana', 'apple');
   return new_response;
});

【讨论】:

    【解决方案4】:

    根据请求,我在下面提供了一个示例 sn-p,展示了如何在原始函数接收之前修改 XMLHttpRequest 的响应。

    // In this example the sample response should be
    // {"data_sample":"data has not been modified"}
    // and we will change it into
    // {"data_sample":"woops! All data has gone!"}
    
    /*---BEGIN HACK---------------------------------------------------------------*/
    
    // here we will modify the response
    function modifyResponse(response) {
    
        var original_response, modified_response;
    
        if (this.readyState === 4) {
    
            // we need to store the original response before any modifications
            // because the next step will erase everything it had
            original_response = response.target.responseText;
    
            // here we "kill" the response property of this request
            // and we set it to writable
            Object.defineProperty(this, "responseText", {writable: true});
    
            // now we can make our modifications and save them in our new property
            modified_response = JSON.parse(original_response);
            modified_response.data_sample = "woops! All data has gone!";
            this.responseText = JSON.stringify(modified_response);
    
        }
    }
    
    // here we listen to all requests being opened
    function openBypass(original_function) {
    
        return function(method, url, async) {
    
            // here we listen to the same request the "original" code made
            // before it can listen to it, this guarantees that
            // any response it receives will pass through our modifier
            // function before reaching the "original" code
            this.addEventListener("readystatechange", modifyResponse);
    
            // here we return everything original_function might
            // return so nothing breaks
            return original_function.apply(this, arguments);
    
        };
    
    }
    
    // here we override the default .open method so that
    // we can listen and modify the request before the original function get its
    XMLHttpRequest.prototype.open = openBypass(XMLHttpRequest.prototype.open);
    // to see the original response just remove/comment the line above
    
    /*---END HACK-----------------------------------------------------------------*/
    
    // here we have the "original" code receiving the responses
    // that we want to modify
    function logResponse(response) {
    
        if (this.readyState === 4) {
    
            document.write(response.target.responseText);
    
        }
    
    }
    
    // here is a common request
    var _request = new XMLHttpRequest();
    _request.open("GET", "https://gist.githubusercontent.com/anonymous/c655b533b340791c5d49f67c373f53d2/raw/cb6159a19dca9b55a6c97d3a35a32979ee298085/data.json", true);
    _request.addEventListener("readystatechange", logResponse);
    _request.send();

    【讨论】:

      【解决方案5】:

      您可以使用新函数将 responseText 的 getter 包装在原型中,并在那里对输出进行更改。

      这是一个简单的示例,它将 html 注释 <!-- TEST --> 附加到响应文本:

      (function(http){
        var get = Object.getOwnPropertyDescriptor(
          http.prototype,
          'responseText'
        ).get;
      
        Object.defineProperty(
          http.prototype,
          "responseText",
          {
            get: function(){ return get.apply( this, arguments ) + "<!-- TEST -->"; }
          }
        );
      })(self.XMLHttpRequest);
      

      上述函数将更改所有请求的响应文本。

      如果您只想更改一个请求,请不要使用上面的函数,而只需在单个请求上定义 getter:

      var req = new XMLHttpRequest();
      var get = Object.getOwnPropertyDescriptor(
        XMLHttpRequest.prototype,
        'responseText'
      ).get;
      Object.defineProperty(
        req,
        "responseText", {
          get: function() {
            return get.apply(this, arguments) + "<!-- TEST -->";
          }
        }
      );
      var url = '/';
      req.open('GET', url);
      req.addEventListener(
        "load",
         function(){
           console.log(req.responseText);
         }
      );
      req.send();
      

      【讨论】:

        【解决方案6】:

        我在制作 Chrome 扩展程序以允许跨源 API 调用时遇到了同样的问题。这在 Chrome 中有效。 (更新:它在最新的 Chrome 版本中不起作用)。

        delete _this.responseText;
        _this.responseText = "Anything you want";
        

        sn-p 在一个经过猴子补丁的 XMLHttpRequest.prototype.send 中运行,该 XMLHttpRequest.prototype.send 将请求重定向到扩展后台脚本并替换响应中的所有属性。像这样:

        // Delete removes the read only restriction
        delete _this.response;
        _this.response = event.data.response.xhr.response;
        delete _this.responseText;
        _this.responseText = event.data.response.xhr.responseText;
        delete _this.status;
        _this.status = event.data.response.xhr.status;
        delete _this.statusText;
        _this.statusText = event.data.response.xhr.statusText;
        delete _this.readyState;
        _this.readyState = event.data.response.xhr.readyState;
        

        这在 Firefox 中不起作用,但我找到了一个有效的解决方案:

        var test = new XMLHttpRequest();
        Object.defineProperty(test, 'responseText', {
          configurable: true,
          writable: true,
        });
        
        test.responseText = "Hey";
        

        这在 Chrome 中不起作用,但在 Chrome 和 Firefox 中都可以:

        var test = new XMLHttpRequest();
        var aValue;
        Object.defineProperty(test, 'responseText', {
          get: function() { return aValue; },
          set: function(newValue) { aValue = newValue; },
          enumerable: true,
          configurable: true
        });
        
        test.responseText = "Hey";
        

        最后是从https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty复制过去

        这些解决方案均不适用于 Safari。我尝试使用可写属性创建一个新的 XMLHttpRequest,但不允许调用 open 或从中发送。我也试过这个解决方案:https://stackoverflow.com/a/28513219/3717718。不幸的是,它在 Safari 中产生了同样的错误:

        TypeError: Attempting to configurable property of unconfigurable property.

        【讨论】:

        • 删除方法在最新的 Chrome 版本中不起作用。
        【解决方案7】:

        一流的函数变量是美妙的东西! function f() {a; b; c; }var f = function () {a; b; c; } 完全相同,这意味着您可以根据需要重新定义函数。您想包装函数Mj 以返回修改后的对象吗?没问题。 responseText 字段是只读的这一事实令人痛苦,但如果这是您需要的唯一字段...

        var Mj_backup = Mj; // Keep a copy of it, unless you want to re-implement it (which you could do)
        Mj = function (a, b, c, d, e) { // To wrap the old Mj function, we need its args
            var retval = Mj_backup(a,b,c,d,e); // Call the original function, and store its ret value
            var retmod; // This is the value you'll actually return. Not a true XHR, just mimics one
            retmod.responseText = retval.responseText; // Repeat for any other required properties
            return retmod;
        }
        

        现在,当您的页面代码调用 Mj() 时,它将改为调用您的包装器(当然,它仍会在内部调用原始的 Mj)。

        【讨论】:

        • 要是这么简单就好了,但我忘了说明这些功能在全球范围内不可用,这是我的错误,对此我深表歉意。
        • 您无权访问定义它们的范围吗? var Mj_backup = A.Fn.Mj; A.Fn.Mj = function... 也可以。我猜有一些原因不起作用,或者你已经做到了......
        • 它们都包含在一个匿名函数中 (function(){})();就像我说的,如果只是像扭曲函数本身而不是 XMLHttprequest 那样简单。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-04-27
        • 2017-08-02
        • 2023-01-11
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多