我如何Promise原生 XHR?

IT技术 javascript xmlhttprequest promise
2021-01-21 14:59:23

我想在我的前端应用程序中使用(本机)promise 来执行 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);
});
6个回答

我假设您知道如何发出本机 XHR 请求(您可以在这里这里复习

由于任何支持原生 promises 的浏览器也将支持xhr.onload,我们可以跳过所有的onReadyStateChangetomfoolery。让我们退后一步,从使用回调的基本 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 数据),但足以让我们继续前进。

Promise构造函数

我们可以像这样构造一个 promise:

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

promise 构造函数接受一个函数,该函数将传递两个参数(让我们称它们为resolvereject)。您可以将这些视为回调,一个表示成功,一个表示失败。例子很棒,让我们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);
});

现在我们可以利用 Promise 的力量,链接多个 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 上可以找到更全面的方法

或者,您可以使用fetch API ( polyfill )。

此外,返回xhr.statusxhr.statusText出错是没有意义的,因为在这种情况下它们是空的。
2021-03-11 14:59:23
您可能还想为responseType、身份验证、凭据等添加选项timeout……并且params对象应该支持 blob/bufferviews 和FormData实例
2021-04-01 14:59:23
在拒绝时返回一个新的错误会更好吗?
2021-04-05 14:59:23
除了一件事,这段代码似乎像宣传的那样工作。我希望将参数传递给 GET 请求的正确方法是通过 xhr.send(params)。但是,GET 请求会忽略发送到 send() 方法的任何值。相反,它们只需要是 URL 本身的查询字符串参数。因此,对于上述方法,如果您希望将“params”参数应用于 GET 请求,则需要修改例程以识别 GET 与 POST,然后有条件地将这些值附加到传递给 xhr 的 URL 。打开()。
2021-04-09 14:59:23
应该使用resolve(xhr.response | xhr.responseText);在大多数浏览器中,响应同时在 responseText 中。
2021-04-09 14:59:23

这可能像以下代码一样简单。

请记住,此代码只会rejectonerror被调用时(仅限网络错误)触发回调,而不会在 HTTP 状态代码表示错误触发这也将排除所有其他例外。处理这些应该取决于你,IMO。

此外,建议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
    });
@MadaraUchiha 我想这是它的 tl; dr 版本。它为 OP 提供了他们问题的答案,仅此而已。
2021-03-11 14:59:23
是的,但你为什么不允许在你的代码中这样做?(因为你参数化了方法)
2021-03-20 14:59:23
我喜欢这个答案,因为它提供了非常简单的代码,可以立即回答问题。
2021-03-21 14:59:23
@crl 就像在常规 XHR 中一样: xhr.send(requestBody)
2021-03-26 14:59:23
POST 请求的正文在哪里?
2021-03-30 14:59:23

对于现在搜索此内容的任何人,您都可以使用fetch功能。它有一些很好的支持

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

我首先使用了@SomeKittens 的答案,但后来发现fetch它对我来说是开箱即用的 :)

@microo8 有一个使用 fetch 的简单示例会很好,这里似乎是放置它的好地方。
2021-03-15 14:59:23
较旧的浏览器不支持该fetch功能,但GitHub 发布了一个 polyfill
2021-03-18 14:59:23
stackoverflow.com/questions/31061838/...的答案有可取消提取的代码示例,到目前为止已经在 Firefox 57+ 和 Edge 16+ 中工作
2021-03-27 14:59:23
我不推荐,fetch因为它还不支持取消。
2021-04-05 14:59:23

我认为我们可以通过不让它创建对象来使最佳答案更加灵活和可重用XMLHttpRequest这样做的唯一好处是我们不必自己编写 2 或 3 行代码来完成它,而且它的巨大缺点是剥夺了我们对许多 API 功能的访问权限,例如设置标头。它还从应该处理响应(成功和错误)的代码中隐藏原始对象的属性。所以我们可以通过只接受XMLHttpRequest对象作为输入并将其作为结果传递来制作一个更灵活、更广泛适用的函数

此函数将任意XMLHttpRequest对象转换为Promise,默认情况下将非 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);
});

我们的调用代码较长,但从概念上讲,理解发生了什么仍然很简单。而且我们不必为了支持其功能而重新构建整个 Web 请求 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);
});
这应该是一个最高投票的答案,因为它使代码干净
2021-03-20 14:59:23

在我看来,jpmc26 的答案非常接近完美。不过,它也有一些缺点:

  1. 它只在最后一刻公开 xhr 请求。这不允许POST-requests 设置请求正文。
  2. 由于关键的send-call 隐藏在函数中,因此更难阅读
  3. 它在实际发出请求时引入了相当多的样板文件。

猴子修补 xhr-object 解决了这些问题:

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

当然,这引入了一个不同的缺点:Monkey-patching 确实会影响性能。然而,假设用户主要等待 xhr 的结果,那么这应该不是问题,请求本身比设置呼叫花费的时间长几个数量级,并且 xhr 请求不经常发送。

PS:当然,如果针对现代浏览器,请使用 fetch!

PPS:评论中已指出此方法更改了标准 API,这可能会令人困惑。为了更清晰,可以将不同的方法修补到 xhr 对象上sendAndGetPromise()

就像我说的:这取决于。如果您的module太大以至于 promisify 函数在其余代码之间丢失,那么您可能会遇到其他问题。如果你有一个module,你只想调用一些端点并返回Promise,我看不出有什么问题。
2021-03-19 14:59:23
我避免猴子修补,因为它令人惊讶。大多数开发人员期望标准 API 函数名称调用标准 API 函数。这段代码仍然隐藏了实际的send调用,但也会让知道send没有返回值的读者感到困惑使用更明确的调用可以更清楚地表明已经调用了额外的逻辑。我的答案确实需要调整以处理send; 但是,fetch现在使用可能更好
2021-03-27 14:59:23
我不同意这取决于您的代码库的大小。看到标准 API 函数执行其标准行为以外的其他操作令人困惑。
2021-03-30 14:59:23
我想这取决于。如果您返回/公开 xhr 请求(无论如何这似乎是可疑的),那么您是绝对正确的。但是,我不明白为什么不在module中执行此操作并仅公开结果Promise。
2021-04-01 14:59:23
我特别指的是必须维护您执行代码的任何人。
2021-04-07 14:59:23