Bagaimana cara saya menjanjikan XHR asli?

183

Saya ingin menggunakan janji (asli) di aplikasi frontend saya untuk melakukan permintaan XHR tetapi tanpa semua kesalahan kerangka kerja yang masif.

Saya ingin xhr saya untuk kembali janji tapi ini tidak berhasil (memberi saya: 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);
});
Anak kucing
sumber

Jawaban:

369

Saya berasumsi Anda tahu cara membuat permintaan XHR asli (Anda dapat memoles di sini dan di sini )

Karena browser apa pun yang mendukung janji asli juga akan mendukung xhr.onload, kami dapat melewati semua onReadyStateChangetindakan buruk tersebut. Mari kita mundur selangkah dan mulai dengan fungsi permintaan XHR dasar menggunakan callback:

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

Hore! Ini tidak melibatkan sesuatu yang sangat rumit (seperti header khusus atau data POST) tetapi cukup untuk membuat kita bergerak maju.

Konstruktor janji

Kita bisa membuat janji seperti ini:

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

Konstruktor janji mengambil fungsi yang akan melewati dua argumen (sebut saja resolvedan reject). Anda dapat menganggap ini sebagai panggilan balik, satu untuk kesuksesan dan satu untuk kegagalan. Contohnya luar biasa, mari perbarui makeRequestdengan konstruktor ini:

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

Sekarang kita dapat memanfaatkan kekuatan janji, merantai beberapa panggilan XHR (dan keinginan .catchakan memicu kesalahan pada salah satu panggilan):

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

Kami dapat meningkatkan ini lebih jauh, menambahkan params POST / PUT dan header kustom. Mari kita gunakan objek opsi alih-alih beberapa argumen, dengan tanda tangan:

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

makeRequest sekarang terlihat seperti ini:

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

Pendekatan yang lebih komprehensif dapat ditemukan di MDN .

Atau, Anda bisa menggunakan API pengambilan ( polyfill ).

Anak kucing
sumber
3
Anda mungkin juga ingin menambahkan opsi untuk responseType, otentikasi, kredensial, timeout... Dan paramsobjek harus mendukung gumpalan / bufferviews dan FormDatainstance
Bergi
4
Apakah akan lebih baik untuk mengembalikan Kesalahan baru saat ditolak?
prasanthv
1
Selain itu, tidak masuk akal untuk kembali xhr.statusdan xhr.statusTextkesalahan, karena mereka kosong dalam kasus itu.
dqd
2
Kode ini tampaknya berfungsi seperti yang diiklankan, kecuali untuk satu hal. Saya berharap bahwa cara yang tepat untuk mengirimkan params ke permintaan GET adalah melalui xhr.send (params). Namun, permintaan GET mengabaikan nilai yang dikirim ke metode kirim (). Sebaliknya, mereka hanya perlu params string kueri di URL itu sendiri. Jadi, untuk metode di atas, jika Anda ingin argumen "params" diterapkan pada permintaan GET, rutin perlu dimodifikasi untuk mengenali GET vs POST, dan kemudian menambahkan nilai-nilai tersebut ke URL yang diserahkan ke xhr secara kondisional. .Buka().
hairbo
1
Seseorang harus menggunakan resolve(xhr.response | xhr.responseText);Di sebagian besar browser repsonse berada di responseText sementara itu.
heinob
50

Ini bisa sesederhana kode berikut.

Perlu diingat bahwa kode ini hanya akan memecat rejectpanggilan balik ketika onerrordipanggil ( hanya kesalahan jaringan ) dan bukan ketika kode status HTTP menandakan kesalahan. Ini juga akan mengecualikan semua pengecualian lainnya. Menangani itu harus terserah Anda, IMO.

Selain itu, disarankan untuk memanggil rejectcallback dengan contoh Errordan bukan acara itu sendiri, tetapi demi kesederhanaan, saya pergi apa adanya.

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

Dan memohon bisa jadi ini:

request('GET', 'http://google.com')
    .then(function (e) {
        console.log(e.target.response);
    }, function (e) {
        // handle errors
    });
Peleg
sumber
14
@ MadaraUchiha Saya kira itu adalah versi tl; dr itu. Ini memberi OP jawaban untuk pertanyaan mereka dan hanya itu.
Peleg
kemana tubuh permintaan POST itu?
bahas
1
@crl seperti di XHR biasa:xhr.send(requestBody)
Peleg
ya tapi mengapa Anda tidak mengizinkannya dalam kode Anda? (Karena Anda parametrized metode)
caub
6
Saya suka jawaban ini karena menyediakan kode yang sangat sederhana untuk bekerja dengan segera yang menjawab pertanyaan.
Steve Chamaillard
12

Bagi siapa saja yang mencari ini sekarang, Anda dapat menggunakan fungsi ambil . Ini memiliki beberapa dukungan yang cukup bagus .

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

Saya pertama kali menggunakan jawaban @ SomeKittens, tetapi kemudian menemukan fetchyang melakukannya untuk saya di luar kotak :)

microo8
sumber
2
Browser lama tidak mendukung fetchfungsi ini, tetapi GitHub telah menerbitkan polyfill .
bdesham
1
Saya tidak akan merekomendasikan fetchkarena belum mendukung pembatalan.
James Dunne
2
Spesifikasi untuk API Ambil sekarang menyediakan untuk pembatalan. Dukungan sejauh ini telah dikirim di Firefox 57 bugzilla.mozilla.org/show_bug.cgi?id=1378342 dan Edge 16. Demo: fetch-abort-demo-edge.glitch.me & mdn.github.io/dom-examples/abort -api . Dan ada bug terbuka Chrome & Webkit fitur bugs.chromium.org/p/chromium/issues/detail?id=750599 & bugs.webkit.org/show_bug.cgi?id=174980 . How-to: developers.google.com/web/updates/2017/09/abortable-fetch & developer.mozilla.org/en-US/docs/Web/API/AbortSignal#Contoh
sontonbarker
Jawabannya di stackoverflow.com/questions/31061838/… memiliki contoh kode yang dapat dibatalkan yang sejauh ini sudah berfungsi di Firefox 57+ dan Edge 16+
sontonbarker
1
@ microo8 Akan menyenangkan untuk memiliki contoh sederhana menggunakan fetch, dan di sini sepertinya tempat yang bagus untuk meletakkannya.
jpaugh
8

Saya pikir kita bisa membuat jawaban atas jauh lebih fleksibel dan dapat digunakan kembali dengan tidak membuatnya membuat XMLHttpRequestobjek. Satu-satunya keuntungan dari melakukannya adalah bahwa kita tidak harus menulis sendiri 2 atau 3 baris kode untuk melakukannya, dan itu memiliki kelemahan besar dalam menghilangkan akses kita ke banyak fitur API, seperti mengatur header. Ini juga menyembunyikan properti objek asli dari kode yang seharusnya menangani respons (untuk keberhasilan dan kesalahan). Jadi kita dapat membuat fungsi yang lebih fleksibel, lebih luas diterapkan dengan hanya menerima XMLHttpRequestobjek sebagai input dan meneruskannya sebagai hasilnya .

Fungsi ini mengubah XMLHttpRequestobjek sewenang-wenang menjadi janji, memperlakukan kode status non-200 sebagai kesalahan secara default:

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

Fungsi ini sangat cocok dengan rantai Promises, tanpa mengorbankan fleksibilitas 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);
});

catchdihilangkan di atas untuk menjaga kode sampel tetap sederhana. Anda harus selalu memilikinya, dan tentu saja kami dapat:

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

Dan menonaktifkan penanganan kode status HTTP tidak memerlukan banyak perubahan dalam kode:

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

Kode panggilan kami lebih panjang, tetapi secara konsep, masih sederhana untuk memahami apa yang terjadi. Dan kami tidak perlu membangun kembali seluruh API permintaan web hanya untuk mendukung fitur-fiturnya.

Kami dapat menambahkan beberapa fungsi kenyamanan untuk merapikan kode kami, juga:

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

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

Kemudian kode kita menjadi:

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

Jawaban jpmc26 cukup dekat dengan sempurna menurut saya. Ini memiliki beberapa kelemahan, meskipun:

  1. Itu memperlihatkan permintaan xhr hanya sampai saat terakhir. Ini tidak mengizinkan POST-requests untuk mengatur badan permintaan.
  2. Lebih sulit untuk dibaca karena sendpanggilan- krusial tersembunyi di dalam suatu fungsi.
  3. Ini memperkenalkan sedikit boilerplate ketika benar-benar membuat permintaan.

Monyet menambal objek xhr menangani masalah ini:

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

Sekarang penggunaannya sesederhana:

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

Tentu saja, ini memperkenalkan kelemahan yang berbeda: Penambalan monyet tidak merusak kinerja. Namun ini seharusnya tidak menjadi masalah dengan asumsi bahwa pengguna menunggu terutama untuk hasil xhr, bahwa permintaan itu sendiri membutuhkan pesanan lebih besar daripada mengatur panggilan dan permintaan xhr tidak sering dikirim.

PS: Dan tentu saja jika menargetkan browser modern, gunakan fetch!

PPS: Telah ditunjukkan dalam komentar bahwa metode ini mengubah API standar yang dapat membingungkan. Untuk kejelasan yang lebih baik, seseorang dapat menambal metode yang berbeda ke objek xhr sendAndGetPromise().

t.hewan
sumber
Saya menghindari tambalan monyet karena itu mengejutkan. Sebagian besar pengembang berharap bahwa nama fungsi API standar memanggil fungsi API standar. Kode ini masih menyembunyikan sendpanggilan aktual tetapi juga dapat membingungkan pembaca yang tahu bahwa sendtidak memiliki nilai balik. Menggunakan panggilan yang lebih eksplisit membuatnya lebih jelas bahwa logika tambahan telah dipanggil. Jawaban saya perlu disesuaikan untuk menangani argumen send; namun, mungkin lebih baik digunakan fetchsekarang.
jpmc26
Saya kira itu tergantung. Jika Anda mengembalikan / mengekspos permintaan xhr (yang tampaknya meragukan bagaimanapun juga) Anda benar sekali. Namun saya tidak melihat mengapa seseorang tidak akan melakukan ini dalam modul dan hanya mengekspos janji yang dihasilkan.
t.animal
Saya merujuk secara khusus kepada siapa pun yang harus menjaga kode yang Anda
gunakan
Seperti saya katakan: Itu tergantung. Jika modul Anda sangat besar sehingga fungsi promisify hilang di antara sisa kode Anda mungkin punya masalah lain. Jika Anda punya modul di mana Anda hanya ingin memanggil beberapa titik akhir dan mengembalikan janji saya tidak melihat masalah.
t.animal
Saya tidak setuju bahwa itu tergantung pada ukuran basis kode Anda. Sangat membingungkan melihat fungsi API standar melakukan sesuatu selain dari itu perilaku standar.
jpmc26