Как мне обещать родной XHR?

183

Я хочу использовать (родные) обещания в моем приложении для выполнения запроса XHR, но без всякой дурачества массивной платформы.

Я хочу , чтобы мой XHR вернуть обещание , но это не работает (давая мне: Uncaught TypeError: Promise resolver undefined is not a function)

function makeXHRRequest (method, url, done) {
  var xhr = new XMLHttpRequest();
  xhr.open(method, url);
  xhr.onload = function() { return new Promise().resolve(); };
  xhr.onerror = function() { return new Promise().reject(); };
  xhr.send();
}

makeXHRRequest('GET', 'http://example.com')
.then(function (datums) {
  console.log(datums);
});
SomeKittens
источник

Ответы:

369

Я предполагаю, что вы знаете, как сделать собственный запрос XHR (вы можете освежить здесь и здесь )

Поскольку любой браузер, который поддерживает собственные обещания , также будет поддерживать xhr.onload, мы можем пропустить все onReadyStateChangeдурачества. Давайте сделаем шаг назад и начнем с базовой функции запроса XHR, используя обратные вызовы:

function makeRequest (method, url, done) {
  var xhr = new XMLHttpRequest();
  xhr.open(method, url);
  xhr.onload = function () {
    done(null, xhr.response);
  };
  xhr.onerror = function () {
    done(xhr.response);
  };
  xhr.send();
}

// And we'd call it as such:

makeRequest('GET', 'http://example.com', function (err, datums) {
  if (err) { throw err; }
  console.log(datums);
});

Ура! В этом нет ничего ужасно сложного (например, пользовательских заголовков или данных POST), но этого достаточно, чтобы продвинуться вперед.

Конструктор обещаний

Мы можем построить обещание так:

new Promise(function (resolve, reject) {
  // Do some Async stuff
  // call resolve if it succeeded
  // reject if it failed
});

Конструктор обещаний принимает функцию, которой будут переданы два аргумента (назовем их resolveи reject). Вы можете думать об этом как о обратных вызовах, один для успеха и один для отказа. Примеры потрясающие, давайте обновимся makeRequestэтим конструктором:

function makeRequest (method, url) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.open(method, url);
    xhr.onload = function () {
      if (this.status >= 200 && this.status < 300) {
        resolve(xhr.response);
      } else {
        reject({
          status: this.status,
          statusText: xhr.statusText
        });
      }
    };
    xhr.onerror = function () {
      reject({
        status: this.status,
        statusText: xhr.statusText
      });
    };
    xhr.send();
  });
}

// Example:

makeRequest('GET', 'http://example.com')
.then(function (datums) {
  console.log(datums);
})
.catch(function (err) {
  console.error('Augh, there was an error!', err.statusText);
});

Теперь мы можем использовать силу обещаний, объединяя несколько вызовов XHR (и при .catchкаждом вызове будет вызывать ошибку):

makeRequest('GET', 'http://example.com')
.then(function (datums) {
  return makeRequest('GET', datums.url);
})
.then(function (moreDatums) {
  console.log(moreDatums);
})
.catch(function (err) {
  console.error('Augh, there was an error!', err.statusText);
});

Мы можем улучшить это еще больше, добавив как параметры POST / PUT, так и пользовательские заголовки. Давайте использовать объект параметров вместо нескольких аргументов с подписью:

{
  method: String,
  url: String,
  params: String | Object,
  headers: Object
}

makeRequest теперь выглядит примерно так:

function makeRequest (opts) {
  return new Promise(function (resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.open(opts.method, opts.url);
    xhr.onload = function () {
      if (this.status >= 200 && this.status < 300) {
        resolve(xhr.response);
      } else {
        reject({
          status: this.status,
          statusText: xhr.statusText
        });
      }
    };
    xhr.onerror = function () {
      reject({
        status: this.status,
        statusText: xhr.statusText
      });
    };
    if (opts.headers) {
      Object.keys(opts.headers).forEach(function (key) {
        xhr.setRequestHeader(key, opts.headers[key]);
      });
    }
    var params = opts.params;
    // We'll need to stringify if we've been given an object
    // If we have a string, this is skipped.
    if (params && typeof params === 'object') {
      params = Object.keys(params).map(function (key) {
        return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
      }).join('&');
    }
    xhr.send(params);
  });
}

// Headers and params are optional
makeRequest({
  method: 'GET',
  url: 'http://example.com'
})
.then(function (datums) {
  return makeRequest({
    method: 'POST',
    url: datums.url,
    params: {
      score: 9001
    },
    headers: {
      'X-Subliminal-Message': 'Upvote-this-answer'
    }
  });
})
.catch(function (err) {
  console.error('Augh, there was an error!', err.statusText);
});

Более комплексный подход можно найти в MDN .

Кроме того, вы можете использовать API выборки ( polyfill ).

SomeKittens
источник
3
Возможно, вы также захотите добавить опции для responseTypeаутентификации, учетных данных timeoutи т. Д. И paramsобъекты должны поддерживать BLOB-объекты / просмотры буферов и FormDataэкземпляры
Bergi
4
Будет ли лучше вернуть новую ошибку при отклонении?
prasanthv
1
Кроме того, нет смысла возвращаться xhr.statusи xhr.statusTextпри ошибке, так как они пусты в этом случае.
DQD
2
Этот код, кажется, работает как рекламируется, за исключением одной вещи. Я ожидал, что правильный путь для передачи параметров в запрос GET был через xhr.send (params). Однако запросы GET игнорируют любые значения, отправленные методу send (). Вместо этого они просто должны быть параметрами строки запроса в самом URL. Таким образом, для вышеуказанного метода, если вы хотите, чтобы аргумент «params» применялся к GET-запросу, необходимо изменить подпрограмму, чтобы распознать GET и POST, а затем условно добавить эти значения в URL-адрес, передаваемый xhr .открыто().
Hairbo
1
resolve(xhr.response | xhr.responseText);В большинстве браузеров следует использовать repsonse в формате responseText.
Хейноб
50

Это может быть так же просто, как следующий код.

Имейте в виду, что этот код будет вызывать rejectобратный вызов только при onerrorвызове ( только сетевые ошибки), а не тогда, когда код состояния HTTP означает ошибку. Это также исключит все другие исключения. Обработка этих вопросов должна решать вам, ИМО.

Кроме того, рекомендуется вызывать rejectобратный вызов с экземпляром, Errorа не с самим событием, но для простоты я оставил как есть.

function request(method, url) {
    return new Promise(function (resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.open(method, url);
        xhr.onload = resolve;
        xhr.onerror = reject;
        xhr.send();
    });
}

И вызывать это можно так:

request('GET', 'http://google.com')
    .then(function (e) {
        console.log(e.target.response);
    }, function (e) {
        // handle errors
    });
Пелегом
источник
14
@MadaraUchiha Полагаю, это его версия. Это дает ОП ответ на их вопрос и только это.
Пелег
куда идет тело запроса POST?
caub
1
@crl, как в обычном XHR:xhr.send(requestBody)
Пелег
да, но почему вы не допустили этого в своем коде? (так как вы параметризовали метод)
совещание
6
Мне нравится этот ответ, так как он предоставляет очень простой код для немедленной работы, который отвечает на вопрос.
Стив Шамайяр
12

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

fetch('http://example.com/movies.json')
  .then(response => response.json())
  .then(data => console.log(data));

Сначала я использовал ответ @ SomeKittens, но потом обнаружил, fetchчто он делает это для меня из коробки :)

microo8
источник
2
Старые браузеры не поддерживают эту fetchфункцию, но GitHub опубликовал полифилл .
bdesham
1
Я бы не рекомендовал, так fetchкак он пока не поддерживает отмену.
Джеймс Данн
2
Спецификация для Fetch API теперь предусматривает отмену. Поддержка Firefox 57 до сих пор отправлялась в bugzilla.mozilla.org/show_bug.cgi?id=1378342 и в Edge 16. Демоверсии: fetch-abort-demo-edge.glitch.me & mdn.github.io/dom-examples/abort -апи . И есть открытые ошибки Chrome & Webkit bugs.chromium.org/p/chromium/issues/detail?id=750599 & bugs.webkit.org/show_bug.cgi?id=174980 . Как к: developers.google.com/web/updates/2017/09/abortable-fetch & developer.mozilla.org/en-US/docs/Web/API/AbortSignal#Examples
sideshowbarker
Ответ на stackoverflow.com/questions/31061838/... имеет отменяемый-принести пример кода , который до сих пор уже работает в Firefox 57+ и 16+ Крае
sideshowbarker
1
@ microo8 Было бы неплохо иметь простой пример, использующий fetch, и здесь, похоже, хорошее место для его размещения.
jpaugh
8

Я думаю, что мы можем сделать верхний ответ гораздо более гибким и пригодным для повторного использования, не создавая XMLHttpRequestобъект. Единственная выгода от этого заключается в том, что нам не нужно самостоятельно писать 2 или 3 строки кода, и это имеет огромный недостаток, заключающийся в лишении нашего доступа ко многим функциям API, таким как установка заголовков. Он также скрывает свойства исходного объекта от кода, который должен обрабатывать ответ (как для успехов, так и для ошибок). Таким образом, мы можем сделать более гибкую, более широко применимую функцию, просто принимая XMLHttpRequestобъект в качестве входных данных и передавая его в качестве результата .

Эта функция преобразует произвольный XMLHttpRequestобъект в обещание, обрабатывая коды состояния, отличные от 200, по умолчанию как ошибку:

function promiseResponse(xhr, failNon2xx = true) {
    return new Promise(function (resolve, reject) {
        // Note that when we call reject, we pass an object
        // with the request as a property. This makes it easy for
        // catch blocks to distinguish errors arising here
        // from errors arising elsewhere. Suggestions on a 
        // cleaner way to allow that are welcome.
        xhr.onload = function () {
            if (failNon2xx && (xhr.status < 200 || xhr.status >= 300)) {
                reject({request: xhr});
            } else {
                resolve(xhr);
            }
        };
        xhr.onerror = function () {
            reject({request: xhr});
        };
        xhr.send();
    });
}

Эта функция очень естественно вписывается в цепочку Promises, не жертвуя гибкостью XMLHttpRequestAPI:

Promise.resolve()
.then(function() {
    // We make this a separate function to avoid
    // polluting the calling scope.
    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://stackoverflow.com/');
    return xhr;
})
.then(promiseResponse)
.then(function(request) {
    console.log('Success');
    console.log(request.status + ' ' + request.statusText);
});

catchбыл опущен выше для упрощения примера кода. У вас всегда должен быть один, и, конечно, мы можем:

Promise.resolve()
.then(function() {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://stackoverflow.com/doesnotexist');
    return xhr;
})
.then(promiseResponse)
.catch(function(err) {
    console.log('Error');
    if (err.hasOwnProperty('request')) {
        console.error(err.request.status + ' ' + err.request.statusText);
    }
    else {
        console.error(err);
    }
});

А отключение обработки кода состояния HTTP не требует больших изменений в коде:

Promise.resolve()
.then(function() {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://stackoverflow.com/doesnotexist');
    return xhr;
})
.then(function(xhr) { return promiseResponse(xhr, false); })
.then(function(request) {
    console.log('Done');
    console.log(request.status + ' ' + request.statusText);
});

Наш код вызова длиннее, но концептуально все еще просто понять, что происходит. И нам не нужно перестраивать весь API веб-запросов только для поддержки его функций.

Мы также можем добавить несколько удобных функций, чтобы привести в порядок наш код:

function makeSimpleGet(url) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    return xhr;
}

function promiseResponseAnyCode(xhr) {
    return promiseResponse(xhr, false);
}

Тогда наш код становится:

Promise.resolve(makeSimpleGet('https://stackoverflow.com/doesnotexist'))
.then(promiseResponseAnyCode)
.then(function(request) {
    console.log('Done');
    console.log(request.status + ' ' + request.statusText);
});
jpmc26
источник
5

На мой взгляд, ответ jpmc26 довольно близок к идеальному. Однако у него есть некоторые недостатки:

  1. Он выставляет запрос xhr только до последнего момента. Это не позволяет POST-requests устанавливать тело запроса.
  2. Его сложнее читать, поскольку критический sendвызов скрыт внутри функции.
  3. Он вводит довольно много шаблонного, когда фактически делает запрос.

Обезьяна, исправляющая объект xhr, решает следующие проблемы:

function promisify(xhr, failNon2xx=true) {
    const oldSend = xhr.send;
    xhr.send = function() {
        const xhrArguments = arguments;
        return new Promise(function (resolve, reject) {
            // Note that when we call reject, we pass an object
            // with the request as a property. This makes it easy for
            // catch blocks to distinguish errors arising here
            // from errors arising elsewhere. Suggestions on a 
            // cleaner way to allow that are welcome.
            xhr.onload = function () {
                if (failNon2xx && (xhr.status < 200 || xhr.status >= 300)) {
                    reject({request: xhr});
                } else {
                    resolve(xhr);
                }
            };
            xhr.onerror = function () {
                reject({request: xhr});
            };
            oldSend.apply(xhr, xhrArguments);
        });
    }
}

Теперь использование так же просто, как:

let xhr = new XMLHttpRequest()
promisify(xhr);
xhr.open('POST', 'url')
xhr.setRequestHeader('Some-Header', 'Some-Value')

xhr.send(resource).
    then(() => alert('All done.'),
         () => alert('An error occured.'));

Конечно, это вносит другой недостаток: исправление обезьяны снижает производительность. Однако это не должно быть проблемой, если предположить, что пользователь ожидает в основном результата xhr, что сам запрос занимает на несколько порядков больше времени, чем настройка вызова, и запросы xhr отправляются не часто.

PS: И конечно, если вы ориентируетесь на современные браузеры, используйте fetch!

PPS: в комментариях было указано, что этот метод изменяет стандартный API, что может сбивать с толку. Для большей наглядности можно применить другой метод к объекту xhr sendAndGetPromise().

t.animal
источник
Я избегаю мартышек, потому что это удивительно. Большинство разработчиков ожидают, что стандартные имена функций API вызывают стандартную функцию API. Этот код все еще скрывает фактический sendвызов, но также может запутать читателей, которые знают, что sendне имеют возвращаемого значения. Использование более явных вызовов проясняет, что была вызвана дополнительная логика. Мой ответ должен быть скорректирован для обработки аргументов send; однако, вероятно, лучше использовать fetchсейчас.
jpmc26
Я думаю, это зависит Если вы возвращаете / выставляете запрос xhr (который в любом случае кажется сомнительным), вы абсолютно правы. Однако я не понимаю, почему нельзя было бы сделать это в модуле и выставить только полученные обещания.
t.animal
Я имею в виду специально для тех, кому нужно поддерживать код, в котором вы это делаете.
jpmc26
Как я уже сказал: это зависит. Если ваш модуль настолько велик, что функция promisify теряется между остальным кодом, вы, вероятно, столкнулись с другими проблемами. Если у вас есть модуль, в котором вы просто хотите вызвать некоторые конечные точки и вернуть обещания, я не вижу проблем.
t.animal
Я не согласен, что это зависит от размера вашей кодовой базы. Это сбивает с толку, когда стандартная функция API выполняет нечто иное, чем ее стандартное поведение.
jpmc26