我想使用(本机)承诺在我的前端应用程序执行XHR请求,但没有一个庞大的框架的所有愚蠢。

我想我的xhr返回一个承诺,但这并不工作(给我:Uncaught TypeError:承诺解析器未定义不是一个函数)

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

当前回答

我假设你知道如何做出一个原生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
});

promise构造函数接受一个函数,该函数将传递两个参数(我们将它们称为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 (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.response);
      } else {
        reject({
          status: xhr.status,
          statusText: xhr.statusText
        });
      }
    };
    xhr.onerror = function () {
      reject({
        status: xhr.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 (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.response);
      } else {
        reject({
          status: xhr.status,
          statusText: xhr.statusText
        });
      }
    };
    xhr.onerror = function () {
      reject({
        status: xhr.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)。

其他回答

对于现在搜索这个的人,你可以使用fetch函数。 它有一些很好的支持。

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

我首先使用了@ somecats的答案,但后来发现fetch为我做了这件事:)

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

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

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

当然,这也带来了另一个缺点:猴子补丁确实会损害性能。但是,假设用户主要等待xhr的结果,请求本身花费的时间比设置调用的时间长几个数量级,并且xhr请求不经常发送,这应该不是问题。

PS:当然,如果目标是现代浏览器,使用fetch!

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

我假设你知道如何做出一个原生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
});

promise构造函数接受一个函数,该函数将传递两个参数(我们将它们称为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 (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.response);
      } else {
        reject({
          status: xhr.status,
          statusText: xhr.statusText
        });
      }
    };
    xhr.onerror = function () {
      reject({
        status: xhr.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 (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.response);
      } else {
        reject({
          status: xhr.status,
          statusText: xhr.statusText
        });
      }
    };
    xhr.onerror = function () {
      reject({
        status: xhr.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)。

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

请记住,这段代码只会在onerror被调用(仅限网络错误)时触发拒绝回调,而不会在HTTP状态代码表示错误时触发。这也将排除所有其他异常。在我看来,这些应该由你来处理。

此外,建议使用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
    });

如果你想让你的代码在旧的浏览器中工作,把这个放在你的HTML文档的<head>:

<script>
self.Promise||document.write("<script src=/path/to/promise/polyfill.js><\/script>");
</script>

将/path/to/promise/polyfill.js替换为promise polyfill的路径。这将创建一个Promise类,如果该类不是本地的,并允许您的代码在Internet Explorer等旧浏览器上运行。ie和其他老浏览器只占了一小部分市场份额,这看起来微不足道,但这仍然意味着数百万用户,所以我不建议完全抛弃这些用户。

请允许我推荐这个承诺填充:

https://github.com/stefanpenner/es6-promise/

现在您可以访问Promise类。

如果你想让你的代码在像IE 6-8这样的老浏览器中工作,你需要使用onreadystatechange而不是onload。这样做并没有什么坏处,因为onreadystatechange仍然在当前所有浏览器中使用,以实现向后兼容:

function send_request(xhr, data, timeout) {
    return new Promise(function (resolve, reject) {
        var s, p, i;
        if (data && data.constructor==Object) {// serialize object
            s = "_="+(new Date).getTime();
            for (p in data) if (data.hasOwnProperty(p)) {
                if (!data[p] || data[p].constructor!=Array) {
                    data[p] = [data[p]]
                }
                for (i=0; i<data[p].length; i++) {
                    s+= "&"+encodeuricomponent(p)+"="+encodeuricomponent(data[p][i]);
                }
            }
            data = s;
        }
        xhr.onreadystatechange = function() {
            if (xhr.readyState==4) {
                resolve(xhr);
            }
        }
        xhr.send(data);
        if (timeout) {
            settimeout(function() {
                reject("timeout");
                xhr.abort();
            }, timeout);// milliseconds until timeout
        }
    });
}

xhr = new XMLHttpRequest();
xhr.open("GET", "/some/file", true);
send_request(xhr).then(function(xhr) {
    if (xhr.status>=200 || xhr.status<400) {
        //success
        alert(xhr.responseText);
    }
    else {
        return Promise.reject(xhr.statusText? xhr.status+" "+xhr.statusText: "error");
    }
})

请记住,IE 6不支持XMLHttpRequest,所以你需要填充它,你可以用ActiveX来做。在文档<head>中像下面这样的代码可能会起作用:

<!--[if lt IE 7]>
<script>
// This is just an example. Use at your own risk.
function XMLHttpRequest() {
    try {
        return new ActiveXObject("Msxml2.XMLHTTP.6.0")
    }
    catch (e) {
        return new ActiveXObject("Msxml2.XMLHTTP.3.0")
    }
}
</script>
<![endif]-->