Як я обіцяю рідний 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);
});

Відповіді:


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 ).


3
Ви також можете додати параметри для responseTypeавтентифікації, облікових даних timeout… І paramsоб’єкти повинні підтримувати краплі / буферні перегляди та FormDataекземпляри
Бергі,

4
Було б краще повернути нову помилку при відхиленні?
прасантв

1
Крім того, не має сенсу повертатися xhr.statusі xhr.statusTextпомилятися, оскільки в такому випадку вони порожні.
dqd

2
Здається, цей код працює як рекламований, за винятком однієї речі. Я очікував, що правильний спосіб передавати параметри разом із запитом GET - через xhr.send (парами). Однак GET запити ігнорують будь-які значення, надіслані методу send (). Натомість їм просто потрібно запитувати параметри рядків у самій URL-адресі. Отже, для вищевказаного методу, якщо ви хочете, щоб аргумент "парами" застосовувався до GET-запиту, процедуру потрібно змінити, щоб розпізнати GET проти POST, а потім умовно додати ці значення до URL-адреси, переданої xhr .відчинено().
hairbo

1
Слід використовувати resolve(xhr.response | xhr.responseText);У більшості браузерів тим часом repsonse є responseText.
heinob

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 Я думаю, що це tl; dr версія його. Це дає ОП відповідь на їх питання і тільки це.
Пелег

куди йде тіло запиту POST?
кают

1
@crl так само, як у звичайному XHR:xhr.send(requestBody)
Peleg

так, але чому ви цього не допустили у своєму коді? (оскільки ви параметризували метод)
каб

6
Мені подобається ця відповідь, оскільки вона забезпечує дуже простий код, з яким можна відразу працювати, який відповідає на питання.
Стів Чамайлард

12

Для всіх, хто зараз шукає це, ви можете скористатися функцією вибору . Він має досить гарну підтримку .

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

Я спершу використав відповідь @ SomeKittens, але потім виявив, fetchщо це робить для мене поза скринькою :)


2
Старі браузери не підтримують цю fetchфункцію, але GitHub опублікував поліфайл .
бдешам

1
Я б не рекомендував, fetchоскільки він ще не підтримує скасування.
Джеймс Данне

2
Специфікація API Fetch тепер передбачає скасування. Підтримка поки що постачається в 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 bugs.chromium.org/p/chromium/isissue/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+ та Edge 16+
sideshowbarker

1
@ microo8 Було б непогано мати простий приклад з використанням файлу, і ось, здається, лежачи є гарним місцем для цього.
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);
});

5

На мій погляд, відповідь jpmc26 досить близька до ідеальної. Однак він має деякі недоліки:

  1. Він виставляє запит xhr лише до останнього моменту. Це не дозволяєPOST -просить задати тіло запиту.
  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: І звичайно, якщо орієнтуватися на сучасні браузери, використовуйте вибір!

PPS: У коментарях було зазначено, що цей метод змінює стандартний API, що може заплутати. Для більшої ясності можна обклеїти інший метод на об’єкт xhr sendAndGetPromise().


Я уникаю мавпових латок, тому що це дивно. Більшість розробників очікує, що стандартні назви функцій API викликають стандартну функцію API. Цей код все ще приховує фактичний sendвиклик, але також може бентежити читачів, які знають, що sendне має зворотного значення. Використання більш чітких дзвінків дозволяє зрозуміти, що додаткова логіка викликана. Мою відповідь потрібно коригувати для обробки аргументів send; однак, мабуть, краще використовувати fetchзараз.
jpmc26

Я думаю, це залежить. Якщо ви повертаєтесь / виставляєте запит xhr (який все одно здається сумнівним), ви абсолютно праві. Однак я не бачу, чому б не зробити цього в модулі і викласти лише отримані обіцянки.
t.animal

Я спеціально звертаюсь до тих, хто повинен підтримувати код, який ви робите.
jpmc26

Як я вже сказав: Це залежить. Якщо ваш модуль настільки величезний, що функція розпізнавання втрачається між рештою коду, у вас, ймовірно, є інші проблеми. Якщо у вас є модуль, де ви просто хочете зателефонувати в кінцеві точки та повернути обіцянки, я не бачу проблеми.
t.animal

Я не погоджуюся з тим, що це залежить від розміру вашої кодової бази. Бажано бачити, як стандартна функція API робить щось інше, ніж це стандартна поведінка.
jpmc26
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.