ฉันจะแนะนำ XHR ดั้งเดิมได้อย่างไร


183

ฉันต้องการใช้สัญญา (native) ในแอปส่วนหน้าของฉันเพื่อดำเนินการตามคำขอ 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);
});

คำตอบ:


369

ฉันสมมติว่าคุณรู้วิธีสร้างคำขอ XHR แบบดั้งเดิม (คุณสามารถแปรงที่นี่และที่นี่ )

เนื่องจากเบราว์เซอร์ใด ๆ ที่สนับสนุนคำสัญญาดั้งเดิมจะรองรับ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) แต่ก็เพียงพอที่จะทำให้เราก้าวไปข้างหน้า

คอนสตรัคสัญญา

เราสามารถสร้างสัญญาเช่นนั้นได้:

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

ตอนนี้เราสามารถแตะที่พลังแห่งสัญญาผูกสาย 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

หรือคุณสามารถใช้API การดึงข้อมูล ( polyfill )


3
คุณอาจต้องการเพิ่มตัวเลือกสำหรับการresponseTypeรับรองความถูกต้องข้อมูลประจำตัวtimeout... และparamsวัตถุควรสนับสนุน blobs / bufferviews และFormDataอินสแตนซ์
Bergi

4
การคืนข้อผิดพลาดใหม่เมื่อถูกปฏิเสธจะดีกว่าไหม
prasanthv

1
นอกจากนี้มันไม่สมเหตุสมผลที่จะส่งคืนxhr.statusและเกิดxhr.statusTextข้อผิดพลาดเนื่องจากไม่มีในกรณีนั้น
dqd

2
รหัสนี้ดูเหมือนว่าจะทำงานตามที่โฆษณาไว้ยกเว้นสิ่งหนึ่ง ฉันคาดว่าวิธีที่ถูกต้องในการส่งต่อ params ไปยังคำขอ GET คือผ่าน xhr.send (params) อย่างไรก็ตามคำขอ GET จะไม่สนใจค่าใด ๆ ที่ส่งไปยังวิธีการ send () แต่พวกเขาเพียงแค่ต้องเป็นพารามิเตอร์สตริงแบบสอบถามใน URL เอง ดังนั้นสำหรับวิธีการข้างต้นหากคุณต้องการให้อาร์กิวเมนต์ "params" ถูกนำไปใช้กับคำขอ GET จำเป็นต้องแก้ไขรูทีนเพื่อรับ GET vs. POST จากนั้นผนวกค่าเหล่านั้นเข้ากับ URL ที่ส่งไปยัง xhr ตามเงื่อนไข .เปิด().
hairbo

1
หนึ่งควรใช้resolve(xhr.response | xhr.responseText);ในเบราว์เซอร์ส่วนใหญ่ repsonse อยู่ใน responseText ในระหว่างนี้
heinob

50

นี่อาจจะง่ายเหมือนรหัสต่อไปนี้

โปรดทราบว่ารหัสนี้จะใช้rejectเรียกกลับเมื่อonerrorมีการโทรเท่านั้น( ข้อผิดพลาดเครือข่ายเท่านั้น) และไม่ใช่เมื่อรหัสสถานะ 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
    });

14
@MadaraUchiha ฉันคิดว่ามันเป็นเวอร์ชั่น TL ของมัน มันให้คำตอบ OP กับคำถามของพวกเขาและแค่นั้น
Peleg

เนื้อหาของคำขอ POST ไปที่ใด
caub

1
@crl เหมือนใน XHR ปกติ:xhr.send(requestBody)
Peleg

ใช่ แต่ทำไมคุณไม่อนุญาตให้ใช้ในรหัสของคุณ (เนื่องจากคุณกำหนดค่าพารามิเตอร์ด้วยวิธีนี้)
caub

6
ฉันชอบคำตอบนี้เพราะมันให้รหัสที่ง่ายมากที่จะทำงานทันทีซึ่งตอบคำถาม
Steve Chamaillard

12

สำหรับผู้ที่ค้นหาสิ่งนี้ในตอนนี้คุณสามารถใช้ฟังก์ชั่นดึงข้อมูลได้ มันมีการสนับสนุนที่ดีพอสมควร

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

ฉันได้ใช้คำตอบของ @ SomeKittens ก่อน แต่หลังจากนั้นก็พบfetchว่ามันทำให้ฉันออกจากกล่อง :)


2
เบราว์เซอร์รุ่นเก่าไม่รองรับfetchฟังก์ชั่นนี้ แต่GitHub ได้เผยแพร่โพลีฟิล
bdesham

1
ฉันไม่แนะนำfetchเพราะมันยังไม่รองรับการยกเลิก
James Dunne

2
ข้อมูลจำเพาะสำหรับ Fetch API ตอนนี้มีให้สำหรับการยกเลิก การสนับสนุนได้ส่งมอบแล้วใน Firefox 57 bugzilla.mozilla.org/show_bug.cgi?id=1378342และ Edge 16 การสาธิต: fetch-abort-demo-edge.glitch.me & mdn.github.io/dom-examples/abort -api และมีเปิด Chrome และ Webkit ข้อบกพร่องคุณลักษณะbugs.chromium.org/p/chromium/issues/detail?id=750599 & bugs.webkit.org/show_bug.cgi?id=174980 วิธีการ: developers.google.com/web/updates/2017/09/abortable-fetch & developer.mozilla.org/en-US/docs/Web/API/AbortSignal# ตัวอย่าง
sideshowbarker

คำตอบที่stackoverflow.com/questions/31061838/…มีตัวอย่างโค้ดที่สามารถยกเลิกได้ซึ่งตอนนี้ได้ทำงานใน Firefox 57+ และ Edge 16+ แล้ว
sideshowbarker

1
@ microo8 มันคงจะดีถ้ามีตัวอย่างง่ายๆที่ใช้การดึงข้อมูลและที่นี่ดูเหมือนจะเป็นที่ที่เหมาะสำหรับใส่มัน
jpaugh

8

ฉันคิดว่าเราสามารถทำให้คำตอบด้านบนมีความยืดหยุ่นและสามารถนำกลับมาใช้ใหม่ได้โดยไม่ต้องสร้างXMLHttpRequestวัตถุ ประโยชน์เพียงอย่างเดียวของการทำเช่นนั้นคือเราไม่ต้องเขียนโค้ดสองถึงสามบรรทัดและมีข้อเสียอย่างใหญ่หลวงที่ทำให้เราไม่สามารถเข้าถึงคุณลักษณะต่างๆของ 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();
    });
}

ฟังก์ชั่นนี้เหมาะสมอย่างเป็นธรรมชาติในห่วงโซ่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);
});

รหัสการโทรของเรานั้นยาวกว่า แต่ตามแนวคิดแล้วมันยังง่ายที่จะเข้าใจว่าเกิดอะไรขึ้น และเราไม่จำเป็นต้องสร้าง 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 นั้นใกล้เคียงกับความคิดของฉันมาก มันมีข้อบกพร่องบางอย่างแม้ว่า:

  1. มันเปิดเผยการร้องขอ xhr เท่านั้นจนถึงช่วงเวลาสุดท้าย สิ่งนี้ไม่อนุญาตให้ - POSTร้องขอการตั้งค่าเนื้อความคำขอ
  2. มันยากที่จะอ่านเนื่องจากมีsendสายที่สำคัญซ่อนอยู่ภายในฟังก์ชั่น
  3. มันแนะนำค่อนข้างสำเร็จรูปเมื่อทำการร้องขอ

Monkey patching the 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: และแน่นอนถ้ากำหนดเป้าหมายเบราว์เซอร์สมัยใหม่ให้ใช้การดึง!

PPS: มีการชี้ให้เห็นในความคิดเห็นว่าวิธีนี้เปลี่ยน API มาตรฐานซึ่งอาจทำให้เกิดความสับสน sendAndGetPromise()สำหรับดีขึ้นความคมชัดใครสามารถแก้ไขวิธีการที่แตกต่างกันบนวัตถุ XHR


ฉันหลีกเลี่ยงการปะแก้ลิงเพราะมันน่าประหลาดใจ นักพัฒนาส่วนใหญ่คาดว่าชื่อฟังก์ชัน API มาตรฐานจะเรียกใช้ฟังก์ชัน API มาตรฐาน รหัสนี้ยังคงซ่อนการsendโทรจริงแต่ยังสามารถสร้างความสับสนให้ผู้อ่านที่รู้ว่าsendไม่มีค่าตอบแทน การใช้การโทรที่ชัดเจนยิ่งขึ้นทำให้ชัดเจนว่ามีการเรียกตรรกะเพิ่มเติม คำตอบของฉันไม่จำเป็นต้องปรับเพื่อจัดการกับข้อโต้แย้งsend; อย่างไรก็ตามมันน่าจะดีกว่าที่จะใช้fetchตอนนี้
jpmc26

ฉันคิดว่ามันขึ้นอยู่กับ หากคุณส่งคืน / แสดงคำขอ xhr (ซึ่งดูเหมือนว่าน่าสงสัย แต่อย่างใด) คุณพูดถูก อย่างไรก็ตามฉันไม่เห็นว่าทำไมไม่มีใครทำเช่นนี้ในโมดูลและเปิดเผยเฉพาะสัญญาที่เกิดขึ้น
t.animal

ฉันหมายถึงคนที่ต้องรักษารหัสที่คุณทำไว้เป็นพิเศษ
jpmc26

อย่างที่ฉันพูด: มันขึ้นอยู่กับ หากโมดูลของคุณมีขนาดใหญ่จนฟังก์ชั่น promisify หายไประหว่างส่วนที่เหลือของรหัสคุณอาจมีปัญหาอื่น ๆ หากคุณมีโมดูลที่คุณต้องการโทรหาจุดปลายและส่งสัญญาฉันไม่เห็นปัญหา
t.animal

ฉันไม่เห็นด้วยว่าขึ้นอยู่กับขนาดของรหัสฐานของคุณ การดูฟังก์ชั่น API มาตรฐานนั้นทำให้เกิดความสับสน
jpmc26
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.