Как я могу обещать родной XHR?
Я хочу использовать (родной) promises в моем приложении frontend для выполнения запроса 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);
});
Ответы
Ответ 1
Я предполагаю, что вы знаете, как создать собственный запрос XHR (вы можете обрезать здесь и здесь)
Так как любой браузер, поддерживающий собственный promises, также поддерживает xhr.onload
, мы можем пропустить все onReadyStateChange
tomfoolery. Давайте сделаем шаг назад и начнем с базовой функции запроса 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);
});
Теперь мы можем использовать мощность promises, связывая несколько вызовов 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 и пользовательские заголовки. Позвольте использовать объект options вместо нескольких аргументов, с сигнатурой:
{
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 fetch (polyfill).
Ответ 2
Это может быть так же просто, как следующий код.
Имейте в виду, что этот код вызывает только обратный вызов 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
});
Ответ 3
Для тех, кто ищет это сейчас, вы можете использовать функцию fetch.
У этого есть довольно хорошая поддержка.
Сначала я использовал @SomeKittens ответ, но затем обнаружил fetch
, который делает это для меня из коробки:)
Ответ 4
Я думаю, что мы можем сделать верхний ответ более гибким и многоразовым, не создав объект 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();
});
}
Эта функция очень естественно вписывается в цепочку Promise
s, не жертвуя гибкостью API XMLHttpRequest
:
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 довольно близок к идеальному. У него есть некоторые недостатки:
- Он выставляет запрос xhr только до последнего момента. Это не позволяет
POST
-requests устанавливать тело запроса. - Это труднее читать как решающий
send
-call скрыто внутри функции. - Он вводит довольно много шаблонного, когда фактически делает запрос.
Обезьяна, исправляющая объект 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, что может сбивать с толку. Для большей ясности можно sendAndGetPromise()
другой метод к объекту xhr sendAndGetPromise()
.