Làm cách nào để tôi quảng cáo XHR bản địa?


183

Tôi muốn sử dụng các lời hứa (bản địa) trong ứng dụng lối vào của mình để thực hiện yêu cầu XHR nhưng không có tất cả các tính năng tomfoolery của một khung lớn.

Tôi muốn xhr của tôi trả lại một lời hứa nhưng điều này không hiệu quả (cho tôi 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);
});

Câu trả lời:


369

Tôi giả sử bạn biết cách tạo một yêu cầu XHR riêng (bạn có thể xem ở đâyđây )

bất kỳ trình duyệt nào hỗ trợ lời hứa riêng cũng sẽ hỗ trợ xhr.onload, chúng tôi có thể bỏ qua tất cả các onReadyStateChangetomfoolery. Hãy lùi lại một bước và bắt đầu với chức năng yêu cầu XHR cơ bản bằng cách sử dụng các cuộc gọi lại:

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

Tiếng hoan hô! Điều này không liên quan đến bất kỳ điều gì phức tạp khủng khiếp (như tiêu đề tùy chỉnh hoặc dữ liệu POST) nhưng đủ để khiến chúng tôi tiến lên.

Người xây dựng lời hứa

Chúng ta có thể xây dựng một lời hứa như vậy:

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

Hàm tạo hứa hẹn có một hàm sẽ được truyền hai đối số (hãy gọi chúng resolvereject). Bạn có thể nghĩ về những điều này như cuộc gọi lại, một cho thành công và một cho thất bại. Các ví dụ thật tuyệt vời, hãy cập nhật makeRequestvới hàm tạo này:

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

Bây giờ chúng ta có thể khai thác sức mạnh của lời hứa, thực hiện nhiều cuộc gọi XHR (và .catchsẽ kích hoạt lỗi cho một trong hai cuộc gọi):

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

Chúng tôi có thể cải thiện điều này hơn nữa, thêm cả thông số POST / PUT và tiêu đề tùy chỉnh. Chúng ta hãy sử dụng một đối tượng tùy chọn thay vì nhiều đối số, với chữ ký:

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

makeRequest bây giờ trông giống như thế này:

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

Một cách tiếp cận toàn diện hơn có thể được tìm thấy tại MDN .

Ngoài ra, bạn có thể sử dụng API tìm nạp ( polyfill ).


3
Bạn cũng có thể muốn thêm các tùy chọn cho responseType, xác thực, thông tin xác thực, các đối tượng timeoutparamshỗ trợ các blobs / bufferview và các FormDatatrường hợp
Bergi

4
Sẽ tốt hơn nếu trả lại một Lỗi mới khi từ chối?
prasanthv

1
Ngoài ra, không có nghĩa là trả lại xhr.statusvà có xhr.statusTextlỗi, vì chúng trống trong trường hợp đó.
dqd

2
Mã này dường như hoạt động như quảng cáo, ngoại trừ một điều. Tôi hy vọng rằng cách đúng để truyền tham số cho yêu cầu GET là thông qua xhr.send (params). Tuy nhiên, các yêu cầu GET bỏ qua mọi giá trị được gửi đến phương thức send (). Thay vào đó, họ chỉ cần là các tham số chuỗi truy vấn trên chính URL. Vì vậy, đối với phương pháp trên, nếu bạn muốn áp dụng đối số "params" cho yêu cầu GET, thường trình cần phải được sửa đổi để nhận ra GET so với POST, sau đó bổ sung một cách có điều kiện các giá trị đó vào URL được trao cho xhr .mở().
Hairbo

1
resolve(xhr.response | xhr.responseText);Trong thời gian chờ đợi, người ta nên sử dụng trong hầu hết các trình duyệt.
heinob

50

Điều này có thể đơn giản như mã sau đây.

Hãy nhớ rằng mã này sẽ chỉ thực hiện rejectcuộc gọi lại khi onerrorđược gọi ( mạng chỉ lỗi ) chứ không phải khi mã trạng thái HTTP biểu thị một lỗi. Điều này cũng sẽ loại trừ tất cả các ngoại lệ khác. Xử lý những người nên tùy thuộc vào bạn, IMO.

Ngoài ra, nên gọi rejectlại cuộc gọi với một ví dụ Errorvà không phải là sự kiện, nhưng để đơn giản, tôi đã để nguyên như vậy.

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

Và gọi nó có thể là thế này:

request('GET', 'http://google.com')
    .then(function (e) {
        console.log(e.target.response);
    }, function (e) {
        // handle errors
    });

14
@MadaraUchiha Tôi đoán đó là phiên bản tl; dr của nó. Nó cung cấp cho OP một câu trả lời cho câu hỏi của họ và chỉ có thế.
Peleg

cơ thể của nó là một yêu cầu POST?
caub

1
@crl giống như trong XHR thông thường:xhr.send(requestBody)
Peleg

có nhưng tại sao bạn không cho phép điều đó trong mã của bạn? (kể từ khi bạn tham số hóa phương thức)
caub

6
Tôi thích câu trả lời này vì nó cung cấp mã rất đơn giản để làm việc ngay lập tức mà trả lời câu hỏi.
Steve Chamaillard

12

Đối với bất kỳ ai tìm kiếm cái này bây giờ, bạn có thể sử dụng chức năng tìm nạp . Nó có một số hỗ trợ khá tốt .

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

Lần đầu tiên tôi đã sử dụng câu trả lời của @ someKittens, nhưng sau đó phát hiện ra fetchđiều đó giúp tôi hiểu điều đó :)


2
Các trình duyệt cũ hơn không hỗ trợ fetchchức năng, nhưng GitHub đã xuất bản một polyfill .
bdesham

1
Tôi sẽ không đề xuất fetchvì nó chưa hỗ trợ hủy bỏ.
James Dunne

2
Thông số kỹ thuật cho API Fetch hiện cung cấp cho việc hủy bỏ. Cho đến nay, bộ phận hỗ trợ đã được chuyển đến trong Firefox 57 bugzilla.mozilla.org/show_orms.cgi?id=1378342 và Edge 16. Trình diễn: fetch-abort-demo-edge.glitch.me & mdn.github.io/dom-examples/abort -api . Và có Chrome & Webkit mở tính năng lỗi bug.chromium.org/p/chromium/issues/detail?id=750599 & bug.webkit.org/show_orms.cgi?id=174980 . Làm thế nào để: developers.google.com/web/updates/2017/09/abortable-fetch & developer.mozilla.org/en-US/docs/Web/API/AbortSignal#Examples
sideshowbarker

Câu trả lời tại stackoverflow.com/questions/31061838/ đã có ví dụ về mã có thể hủy bỏ mà cho đến nay đã hoạt động trong Firefox 57+ và Edge 16+
sIDIAbarker

1
@ microo8 Thật tuyệt khi có một ví dụ đơn giản sử dụng tìm nạp và ở đây có vẻ như đây là một nơi tốt để đặt nó.
jpaugh

8

Tôi nghĩ rằng chúng ta có thể làm cho câu trả lời hàng đầu linh hoạt hơn và có thể tái sử dụng bằng cách không tạo ra XMLHttpRequestđối tượng. Lợi ích duy nhất của việc này là chúng ta không phải tự viết 2 hoặc 3 dòng mã để làm điều đó và nó có một nhược điểm rất lớn là lấy đi quyền truy cập của chúng ta vào nhiều tính năng của API, như đặt tiêu đề. Nó cũng ẩn các thuộc tính của đối tượng ban đầu khỏi mã được cho là để xử lý phản hồi (cho cả thành công và lỗi). Vì vậy, chúng ta có thể tạo ra một chức năng linh hoạt hơn, có thể áp dụng rộng rãi hơn bằng cách chỉ chấp nhận XMLHttpRequestđối tượng làm đầu vào và chuyển nó làm kết quả .

Hàm này chuyển đổi một XMLHttpRequestđối tượng tùy ý thành một lời hứa, mặc định coi các mã trạng thái không phải là 200 là lỗi:

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

Hàm này khớp rất tự nhiên vào một chuỗi Promises, mà không làm mất tính linh hoạt của 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đã được bỏ qua ở trên để giữ cho mã mẫu đơn giản hơn. Bạn nên luôn luôn có một, và tất nhiên chúng ta có thể:

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

Và việc vô hiệu hóa việc xử lý mã trạng thái HTTP không đòi hỏi nhiều thay đổi trong mã:

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

Mã gọi của chúng tôi dài hơn, nhưng về mặt khái niệm, vẫn đơn giản để hiểu chuyện gì đang xảy ra. Và chúng tôi không phải xây dựng lại toàn bộ API yêu cầu web chỉ để hỗ trợ các tính năng của nó.

Chúng tôi cũng có thể thêm một vài chức năng tiện lợi để dọn dẹp mã của mình:

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

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

Sau đó, mã của chúng tôi trở thành:

Promise.resolve(makeSimpleGet('https://stackoverflow.com/doesnotexist'))
.then(promiseResponseAnyCode)
.then(function(request) {
    console.log('Done');
    console.log(request.status + ' ' + request.statusText);
});

5

Theo tôi, câu trả lời của jpmc26 khá gần với hoàn hảo. Nó có một số nhược điểm, mặc dù:

  1. Nó chỉ hiển thị yêu cầu xhr cho đến giây phút cuối cùng. Điều này không cho phép POSTcác yêu cầu thiết lập cơ thể yêu cầu.
  2. Khó đọc hơn vì phần quan trọng sendđược ẩn bên trong một hàm.
  3. Nó giới thiệu khá nhiều nồi hơi khi thực sự yêu cầu.

Khỉ vá các đối tượng xhr đã khắc phục các vấn đề này:

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

Bây giờ cách sử dụng đơn giản như:

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

Tất nhiên, điều này giới thiệu một nhược điểm khác: Khỉ vá không làm giảm hiệu suất. Tuy nhiên, đây không phải là vấn đề khi giả sử rằng người dùng đang chờ đợi chủ yếu cho kết quả của xhr, rằng chính yêu cầu đó nhận các đơn đặt hàng lớn hơn so với thiết lập cuộc gọi và các yêu cầu xhr không được gửi thường xuyên.

PS: Và tất nhiên nếu nhắm mục tiêu các trình duyệt hiện đại, hãy sử dụng tìm nạp!

PPS: Nó đã được chỉ ra trong các ý kiến ​​rằng phương pháp này thay đổi API tiêu chuẩn có thể gây nhầm lẫn. Để rõ ràng hơn, người ta có thể vá một phương thức khác vào đối tượng xhr sendAndGetPromise().


Tôi tránh vá khỉ vì nó đáng ngạc nhiên. Hầu hết các nhà phát triển mong đợi rằng các tên hàm API tiêu chuẩn gọi hàm API tiêu chuẩn. Mã này vẫn ẩn sendcuộc gọi thực tế nhưng cũng có thể gây nhầm lẫn cho những người đọc biết rằng sendkhông có giá trị trả về. Sử dụng các cuộc gọi rõ ràng hơn làm cho rõ ràng hơn rằng logic bổ sung đã được gọi. Câu trả lời của tôi không cần phải điều chỉnh để xử lý các đối số send; tuy nhiên, fetchbây giờ có lẽ tốt hơn để sử dụng .
jpmc26

Tôi đoán nó phụ thuộc. Nếu bạn trả lại / phơi bày yêu cầu xhr (có vẻ như không rõ ràng dù sao đi nữa), bạn hoàn toàn đúng. Tuy nhiên tôi không thấy lý do tại sao một người sẽ không làm điều này trong một mô-đun và chỉ đưa ra những lời hứa kết quả.
t.animal

Tôi đặc biệt đề cập đến bất cứ ai phải duy trì mã mà bạn thực hiện.
jpmc26

Giống như tôi đã nói: Nó phụ thuộc. Nếu mô-đun của bạn quá lớn đến nỗi chức năng hứa hẹn sẽ bị mất giữa phần còn lại của mã, bạn có thể gặp các vấn đề khác. Nếu bạn có một mô-đun nơi bạn chỉ muốn gọi một số điểm cuối và trả lại lời hứa, tôi sẽ không gặp vấn đề gì.
t.animal

Tôi không đồng ý rằng nó phụ thuộc vào kích thước cơ sở mã của bạn. Thật khó hiểu khi thấy một hàm API tiêu chuẩn làm một cái gì đó khác với hành vi tiêu chuẩn của nó.
jpmc26
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.