기본 XHR을 어떻게 약속합니까?


183

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, 우리는 모든 건너 뛸 수 있습니다 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 (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객체 모양 / bufferviews하고 지원해야 FormData인스턴스
— BERGI

4
거부시 새로운 오류를 반환하는 것이 더 좋습니까?
— prasanthv

1
또한, 그것은 반환에 이해가되지 않습니다 xhr.status그리고 xhr.statusText그들은 그 경우에 비어 있기 때문에, 에러.
— dqd

2
이 코드는 한 가지를 제외하고는 광고 된 것처럼 작동합니다. GET 요청과 함께 매개 변수를 전달하는 올바른 방법은 xhr.send (params)를 통한 것이라고 예상했습니다. 그러나 GET 요청은 send () 메소드로 전송 된 모든 값을 무시합니다. 대신 URL 자체에서 쿼리 문자열 매개 변수이면됩니다. 따라서 위의 방법에서 "params"인수를 GET 요청에 적용하려면 GET vs. POST를 인식하도록 루틴을 수정 한 다음 해당 값을 xhr로 전달되는 URL에 조건부로 추가해야합니다. .열다().
— hairbo

1
하나는 사용해야 resolve(xhr.response | xhr.responseText);repsonse 그 동안에서 responseText에 대부분의 브라우저에서.
— heinob

50

이것은 다음 코드처럼 간단 할 수 있습니다.

이 코드는 HTTP 상태 코드가 오류를 나타내는 경우가 아니라 호출 된 경우 ( 네트워크 오류 만) reject콜백을 발생시킵니다. 다른 모든 예외도 제외됩니다. 그 취급은 당신에게 달려 있습니다, IMO.onerror

또한 이벤트 자체가 아닌 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; dr 버전이라고 생각한다. OP에게 질문에 대한 답변을 제공합니다.
— Peleg

본문은 POST 요청입니까?
— caub

1
@crl은 일반적인 XHR과 마찬가지로 :xhr.send(requestBody)
— Peleg

예, 왜 코드에서 허용하지 않습니까? (방법을 매개 변수화
— 했으므로

6
이 답변은 질문에 대한 답변을 즉시 처리 할 수있는 매우 간단한 코드를 제공하므로 좋아합니다.
— Steve Chamaillard 2016 년

12

지금 이것을 찾는 사람이라면 누구나 인출 기능을 사용할 수 있습니다 . 꽤 좋은 지원이 있습니다.

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

나는 처음 @SomeKittens의 답변을 사용했지만 그 fetch즉시 상자에서 나를 위해 그것을 발견 했습니다 :)


2
이전 브라우저는이 fetch기능을 지원하지 않지만 GitHub는 polyfill을 게시했습니다 .
— bdesham

1
fetch아직 취소를 지원하지 않으므로 권장 하지 않습니다.
— James Dunne


stackoverflow.com/questions/31061838/… 의 답변 은 취소 가능 페치 코드 예제를 가지고 있으며 Firefox 57 이상 및 Edge 16 이상에서 작동합니다.
— sideshowbarker 1

1
@ microo8 fetch를 사용하여 간단한 예제를 작성하는 것이 좋을 것입니다.
— jpaugh

8

나는 우리가 객체를 만들지 않아도 훨씬 유연하고 재사용 가능한 최상위 답변을 만들 수 있다고 생각 XMLHttpRequest합니다. 그렇게하는 것의 유일한 이점은 우리가 직접 2 ~ 3 줄의 코드를 작성할 필요가 없으며 헤더 설정과 같은 많은 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();
    });
}

이 함수 Promise는 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-requests가 요청 본문을 설정 하도록 허용하지 않습니다 .
  2. 중요한 send호출이 함수 안에 숨겨져 있으므로 읽기가 더 어렵습니다 .
  3. 실제로 요청할 때 꽤 많은 상용구를 소개합니다.

xhr 객체를 패치하는 원숭이는 다음과 같은 문제를 해결합니다.

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의 결과를 기다리고 있다고 가정하고, 요청 자체가 호출 설정보다 x 더 긴 시간이 걸리고 xhr 요청이 자주 전송되지 않는다고 가정하면 문제가되지 않습니다.

추신 : 물론 최신 브라우저를 대상으로하는 경우 가져 오기를 사용하십시오!

PPS :이 방법은 혼란 스러울 수있는 표준 API를 변경한다는 의견에서 지적되었습니다. 명확성을 높이기 위해 xhr 객체에 다른 메소드를 패치 할 수 sendAndGetPromise()있습니다.


놀랍기 때문에 원숭이 패치를 피합니다. 대부분의 개발자는 표준 API 함수 이름이 표준 API 함수를 호출 할 것으로 기대합니다. 이 코드는 여전히 실제 send호출을 숨기지 만 send반환 값이 없다는 것을 알고있는 독자에게는 혼란을 줄 수 있습니다 . 보다 명시적인 호출을 사용하면 추가 논리가 호출 된 것이 더 명확 해집니다. 내 답변은 인수를 처리하도록 조정해야합니다 send. 그러나 fetch지금 사용하는 것이 좋습니다 .
— jpmc26

나는 그것이 달려 있다고 생각합니다. xhr 요청을 반환 / 노출하면 (어쨌든 모호한 것처럼 보입니다) 절대적으로 옳습니다. 그러나 왜 모듈 내 에서이 작업을 수행하지 않으며 결과 약속 만 노출하는지 알 수 없습니다.
— t.animal

나는 여러분이 코드를 관리해야하는 사람을 특별히 언급하고 있습니다.
— jpmc26

내가 말했듯이 : 그것은 달려 있습니다. 모듈이 너무 커서 약속의 기능이 나머지 코드 사이에서 손실되면 다른 문제가있을 수 있습니다. 일부 엔드 포인트를 호출하고 약속을 반환하려는 모듈이 있다면 문제가 없습니다.
— t.animal

코드베이스의 크기에 따라 다릅니다. 표준 API 함수가 표준 동작 이외의 작업을 수행하는 것은 혼란 스럽습니다.
— jpmc26
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.