ネイティブ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);
});

2
ジェネリックリファレンスも参照してください。既存のコールバックAPIをpromiseに変換するにはどうすればよいですか?
ベルギ

回答:


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データなど)を含みませんが、先に進むには十分です。

promiseコンストラクター

次のようにpromiseを作成できます。

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

promiseコンストラクターは、2つの引数が渡される関数を取ります(それらをresolveとと呼びましょうreject)。これらは、1つは成功、もう1つは失敗のコールバックと考えることができます。例は素晴らしいです、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);
});

これで、promiseの力を利用して、複数の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にあります。

または、フェッチAPIpolyfill)を使用することもできます。


3
あなたはまたのためのオプションを追加したい場合がありresponseType、認証、認証情報を、timeout...そしてparamsオブジェクトは、ブロブ/ bufferviewsとサポートしなければならないFormDataのインスタンスを
Bergi

4
拒否時に新しいエラーを返す方がよいでしょうか?
prasanthv

1
さらに、それはリターンに意味がないxhr.statusxhr.statusText、彼らはそのような場合には空なので、エラーに。
dqd

2
このコードは、1つの点を除いて、宣伝どおりに機能しているようです。GETリクエストにparamsを渡す正しい方法は、xhr.send(params)を介するものであると私は思っていました。ただし、GETリクエストはsend()メソッドに送信された値を無視します。代わりに、URL自体のクエリ文字列パラメータである必要があります。したがって、上記の方法で、「params」引数をGETリクエストに適用する場合は、GETとPOSTを認識するようにルーチンを変更し、xhrに渡されるURLにこれらの値を条件付きで追加する必要があります。 。開いた()。
ヘアボ2016年

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; drバージョンだと思います。それはOPに彼らの質問への答えを与えます、そしてそれだけです。
Peleg

POSTリクエストの本文はどこにありますか?
caub

1
@crlは通常のXHRと同じです:xhr.send(requestBody)
Peleg

はい、でもなぜそれをコードで許可しなかったのですか?(メソッドをパラメーター化したため)
caub 2015年

6
質問に答えるすぐに使用できる非常に単純なコードを提供するので、私はこの回答が好きです。
Steve Chamaillard 2017年

12

これを今検索する人は誰でも、フェッチ機能を使用できます。かなり良いサポートがあります。

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

私は最初に@SomeKittensの答えを使用しましたが、それからすぐにfetchそれが私のためにそれをすることを発見しました:)


2
古いブラウザはこのfetch機能をサポートしていませんが、GitHubはpolyfillを公開しています
bdesham 2017

1
fetchキャンセルをまだサポートしていないのでお勧めしません。
James Dunne

2
Fetch APIの仕様でキャンセルが提供されるようになりました。サポートは、これまでのFirefox 57で出荷していbugzilla.mozilla.org/show_bug.cgi?id=1378342とエッジ16デモ:fetch-abort-demo-edge.glitch.memdn.github.io/dom-examples/abort -api。そして、オープンクローム&Webkitの機能のバグがあるbugs.chromium.org/p/chromium/issues/detail?id=750599bugs.webkit.org/show_bug.cgi?id=174980。使い方:developers.google.com/web/updates/2017/09/abortable-fetchdeveloper.mozilla.org/en-US/docs/Web/API/AbortSignal#Examples
sideshowbarker

答えstackoverflow.com/questions/31061838/...は、これまでのところ、すでにFirefoxの57+と16+エッジで動作することを解約フェッチコード例を持っている
sideshowbarker

1
@ microo8 fetchを使用した簡単な例があるといいでしょう。
jpaugh

8

オブジェクトを作成しないようにすることで、トップアンサーをより柔軟で再利用可能にできると思いXMLHttpRequestます。そうすることの唯一の利点は、それを行うために自分で2行または3行のコードを記述する必要がないことです。また、ヘッダーの設定など、APIの多くの機能へのアクセスを奪うという大きな欠点があります。また、(成功とエラーの両方の)応答を処理することになっているコードから、元のオブジェクトのプロパティを隠します。したがって、XMLHttpRequestオブジェクトを入力として受け入れ、そのオブジェクトを結果として渡すだけで、より柔軟でより広く適用できる関数を作成できます。

この関数は任意のXMLHttpRequestオブジェクトをpromiseに変換し、デフォルトでは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);
});

呼び出しコードは長くなりますが、概念的には、何が起こっているのかを理解するのは簡単です。また、その機能をサポートするためだけにWebリクエスト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-callは関数内に隠されているため、読みにくいです。
  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の結果を待機していること、要求自体が呼び出しのセットアップよりも桁違いに長く、xhr要求が頻繁に送信されないことを前提とする問題ではありません。

PS:もちろん、最新のブラウザーをターゲットにする場合は、フェッチを使用してください!

PPS:コメントで指摘されているように、このメソッドは標準APIを変更し、混乱を招く可能性があります。より明確にするために、xhrオブジェクトに別のメソッドをパッチすることができますsendAndGetPromise()


サルのパッチングは意外なので避けます。ほとんどの開発者は、標準API関数名が標準API関数を呼び出すことを期待しています。このコードは実際のsend呼び出しを非表示にしますがsend、戻り値がないことを知っている読者を混乱させる可能性もあります。より明示的な呼び出しを使用すると、追加のロジックが呼び出されたことが明確になります。への引数を処理するには、私の答えを調整する必要がありsendます。ただし、おそらくfetch今の方が良いでしょう。
jpmc26

場合によります。(とにかく怪しげに見える)xhrリクエストを返す/公開するなら、あなたは絶対に正しいです。しかし、なぜこれをモジュール内で行わず、結果のプロミスのみを公開するのかわかりません。
t.animal

私はあなたがそれを行うコードを維持するために持つ人には、特別に言及しています。
jpmc26

私が言ったように:それは異なります。モジュールが非常に大きいため、残りのコード間でpromisify関数が失われる場合は、おそらく他の問題が発生しています。いくつかのエンドポイントを呼び出してpromiseを返すだけのモジュールがある場合、問題は発生しません。
t.animal

それはあなたのコードベースのサイズに依存することには同意しません。標準のAPI関数が標準の動作以外のことをするのを見るのは混乱します。
jpmc26
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.