Как я могу изменить текст ответа XMLHttpRequest, полученный другой функцией?

Я пытаюсь изменить 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(){функции здесь})();


РЕДАКТИРОВАТЬ 4: Я изменил принятый ответ, потому что у AmmarCSE нет никаких проблем и сложности, связанных с ответом jfriend00.

Короче, лучший ответ объясняется так:

Прослушайте любой запрос, который вы хотите изменить (убедитесь, что ваш слушатель перехватит его до того, как это сделает исходное назначение функции, иначе нет смысла изменять его после того, как ответ уже был использован).

Сохраните исходный ответ (если вы хотите изменить его) во временной переменной

Измените свойство, которое вы хотите изменить, на "доступное для записи: true", оно будет стирать любое значение, которое у него было. В моем случае я использую

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

куда event это объект, возвращенный при прослушивании load или же readystatechange событие запроса xhr

Теперь вы можете установить все, что вы хотите для своего ответа, если все, что вам нужно, это изменить исходный ответ, то вы можете использовать эти данные из вашей временной переменной, а затем сохранить изменения в ответе.

9 ответов

Решение

Один очень простой обходной путь - изменить дескриптор свойства для 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);

демонстрация

Изменить: см. Второй вариант кода ниже (он проверен и работает). Первый имеет некоторые ограничения.


Поскольку вы не можете изменить ни одну из этих функций, похоже, вам нужно идти за прототипом 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, так как это уже должно быть установлено. Затем эта исправленная функция получает возможность увидеть responseText до вызова исходного обработчика onReadyStateChange, чтобы она могла присвоить ему другое значение.

И, наконец, потому что .responseText только для готовности, это заменяет фиктивный объект XMLHttpResponse перед вызовом onreadystatechange обработчик. Это не будет работать во всех случаях, но будет работать, если обработчик onreadystatechange использует this.responseText чтобы получить ответ.


И вот попытка переопределить объект XMLHttpRequest в качестве нашего собственного прокси-объекта. Поскольку это наш собственный прокси-объект, мы можем установить responseText собственность на все, что мы хотим. Для всех других свойств, кроме onreadystatechangeэтот объект просто перенаправляет вызов get, set или function в реальный объект 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. Попытка создания фиктивного объекта, который отправляется как this объект при звонке onreadystatechange, но большая часть кода не ссылается this, но, скорее, фактический объект сохранен в локальной переменной в замыкании - таким образом победив фиктивный объект.

Мне нужно было перехватить и изменить ответ на запрос, поэтому я придумал немного кода. Я также обнаружил, что некоторым веб-сайтам нравится использовать 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;
});

По запросу я привожу ниже примерный фрагмент, показывающий, как изменить ответ 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();

Вы можете обернуть геттер для responseText в прототипе с новой функцией и внесите изменения в вывод.

Вот простой пример, который добавляет комментарий 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);

Вышеуказанная функция изменит текст ответа на все запросы.

Если вы хотите внести изменения только в один запрос, не используйте вышеописанную функцию, а просто определите геттер для отдельного запроса:

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();

Я столкнулся с той же проблемой, когда создавал расширение для Chrome, чтобы разрешать вызовы API из разных источников. Это работало в Chrome. (Обновление: это не работает в последней версии Chrome).

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

Фрагмент запускается внутри непропатченного 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 с доступными для записи свойствами, но ему не разрешалось вызывать или открывать его. Я также попробовал это решение: /questions/12383872/perehvatyivaet-xmlhttprequest-i-izmenyaet-responsetext/12383892#12383892. К сожалению, в Safari возникла та же ошибка:

TypeError: Попытка настройки настраиваемого атрибута неконфигурируемого свойства.

Первоклассные функциональные переменные - замечательные вещи! 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 внутри).

С помощью Proxy API вы можете создать прокси для класса XMLHttpRequest и загрязнить глобальныйwindow.XMLHttpRequestобъект.

      "use strict"

window.XMLHttpRequest = class XMLHttpRequest {
  static _originalXMLHttpRequest = window.XMLHttpRequest

  constructor(...args) {
    this._XMLHttpRequestInstance = new XMLHttpRequest._originalXMLHttpRequest(...args)

    // If a return statement is used in a constructor with an object,
    // the object will be returned instead of `this`.
    // https://javascript.info/constructor-new#return-from-constructors
    return new Proxy(this, {
      get(instance, property) {
        if (property === "responseText") {
          // Modify the response string
          // `this` doesn't work inside an object, use `instance` instead
          return instance._XMLHttpRequestInstance.responseText.replace("some string", "another string")

          // Or return whatever you want
          return "whatever you wanted"
        }

        // Functions won't work without having `_XMLHttpRequestInstance` as `this`
        const value = instance._XMLHttpRequestInstance[property]
        return value instanceof Function ? value.bind(instance._XMLHttpRequestInstance) : value
      },
      set(instance, property, value) {
        // `this` doesn't work inside an object, use `instance` instead
        instance._XMLHttpRequestInstance[property] = value
        return true
      }
    })
  }
}

Вы также можете изменить addEventListener или другие методы в оригинале.XMLHttpRequestсорт.

      return new Proxy(this, {
  get(instance, property) {
    // Modify listeners added by websites
    if (property === "addEventListener") {
      return (event, listener) => {
        if (event === "load") {
          instance._XMLHttpRequestInstance.addEventListener(event, () => {
            // Add your own code here
            // ...

            // In case you need to access the original listener
            listener()
          })
        } else {
          instance._XMLHttpRequestInstance.addEventListener(event, listener)
        }
      }
    }

    const value = instance._XMLHttpRequestInstance[property]
    return value instanceof Function ? value.bind(instance._XMLHttpRequestInstance) : value
  },
  set(instance, property, value) {
    // Remember to handle `xhr.onload = ...` too
    if (property === "onload") {
      instance._XMLHttpRequestInstance[property] = () => {
        // Add your own code here
        // ...

        // In case you need to access the original listener
        value()
      }
    } else {
      instance._XMLHttpRequestInstance[property] = value
    }

    return true
  }
})

См. также: /questions/60315697/dinamicheski-ustanavlivat-getteryi-i-setteryi-dlya-klassa/61108072#61108072

Я очень много ищу и сделал одно решение для решения проблемы

      const open_prototype = XMLHttpRequest.prototype.open,
    intercept_response = function (urlpattern, callback) {
        XMLHttpRequest.prototype.open = function () {
            arguments['1'].includes(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('SOME_PART_OF_YOUR_API', (response) => {
    const new_response = response.replace('apple', 'orange');
    return new_response;
})

и теперь все яблоки заменены на апельсины

Другие вопросы по тегам