Verwendung asynchroner Windows-APIs
Architekturbewertung erforderlich: Diese Dokumentation wurde zur Unterstützung der Entwicklung gegen die "alte" oder "Legacy"-Architektur von React Native geschrieben. Sie ist möglicherweise nicht direkt auf die Entwicklung mit der neuen Architektur anwendbar und muss überprüft und möglicherweise aktualisiert werden. Informationen zu React Native-Architekturen in React Native Windows finden Sie unter Neu vs. Alt Architektur.
Die neuesten Informationen zur nativen Entwicklung unter Windows finden Sie unter Native Plattform: Übersicht.
Diese Dokumentation und der zugrunde liegende Plattformcode sind im Entstehen begriffen.
Ein gängiges Szenario für Native Modules ist der Aufruf einer oder mehrerer nativer asynchroner Methoden aus einer JS-asynchronen Methode. Es ist jedoch möglicherweise nicht sofort ersichtlich, wie beide asynchronen Welten ordnungsgemäß überbrückt werden können, was zu instabilem, schwer zu debuggendem Code führen kann.
Dieses Dokument schlägt einige Best Practices vor, die bei der Überbrückung asynchroner Methoden von JS zu nativem Code für React Native Windows befolgt werden sollten. Es wird davon ausgegangen, dass Sie bereits mit den Grundlagen der Einrichtung und dem Schreiben von Native Modules vertraut sind.
Der vollständige Quellcode für die folgenden Beispiele ist im Native Module Sample in
microsoft/react-native-windows-samplesenthalten.
Schreiben von Native Modules, die asynchrone Windows-APIs aufrufen
Schreiben wir ein natives Modul, das asynchrone Windows-APIs verwendet, um eine einfache HTTP-Anfrage durchzuführen. Wir nennen es SimpleHttpModule und es benötigt eine einzelne, promise-basierte Methode namens GetHttpResponse, die eine URI-Zeichenkette als Parameter nimmt und im Erfolgsfall ein Objekt mit dem HTTP-Statuscode und dem Textinhalt zurückgibt.
Am Ende möchten wir die Methode wie folgt aus JS aufrufen
NativeModules.SimpleHttpModule.GetHttpResponse('https://msdocs.de/react-native-windows/')
.then(result => console.log(result))
.catch(error => console.log(error));
SimpleHttpModule in C#
Die native Modulunterstützung für C# unterstützt die gängigen asynchronen Programmiermuster in C#, die mit async, await und Task<T> etabliert wurden.
Um das Modul für JavaScript verfügbar zu machen, müssen Sie eine C#-Klasse deklarieren. Um anzuzeigen, dass es für JavaScript verfügbar gemacht werden soll, annotieren Sie es mit einem [ReactModule] Attribut wie
namespace NativeModuleSample
{
[ReactModule]
class SimpleHttpModule
{
// Methods go here.
}
}
Dies macht ein Objekt über den Ausdruck NativeModules.SimpleHttpModule für JavaScript verfügbar. Standardmäßig stimmt der JavaScript-Name mit dem C#-Klassennamen überein. Wenn der Klassenname nicht mit dem Namen in JavaScript übereinstimmen soll, d. h. Sie möchten auf das Modul über den Ausdruck NativeModules.CustomModule zugreifen, können Sie einen benutzerdefinierten Namen übergeben, wie
[ReactModule("CustomModule")]
class SimpleHttpModule
{
...
}
Nun möchten wir die Methode verfügbar machen, die die HTTP-Anfrage durchführt. Es wird empfohlen, und es ist Standard, diese Funktionen asynchron zu schreiben. Das Schreiben von asynchronem Code in C# ist mit den Schlüsselwörtern async und await sowie den Task<T>-Typen ziemlich unkompliziert und intuitiv.
Wenn Sie mit dem Schreiben von asynchronem C#-Code nicht vertraut sind, lesen Sie Asynchrone APIs in C# oder Visual Basic aufrufen und Asynchrone Programmierung, die Ihnen die Konzepte vermitteln, wenn Sie noch nicht vertraut sind.
Die Funktionssignatur für eine typische Webanfrage, annotiert mit dem [ReactMethod]-Attribut, sieht so aus:
[ReactMethod]
public async Task<string> GetHttpResponseAsync(string uri) {
...
}
Jetzt können Sie die Logik wie folgt ausfüllen:
// Create an HttpClient object
var httpClient = new HttpClient();
// Send the GET request asynchronously
var httpResponseMessage = await httpClient.GetAsync(new Uri(uri));
var content = await httpResponseMessage.Content.ReadAsStringAsync();
return content;
Der Code führt die folgenden Schritte aus:
- Erstellt einen
HttpClient. - Ruft asynchron die Methode
GetAsyncauf, um eine HTTP-Anfrage für die URI zu stellen. - Parst den Statuscode aus dem zurückgegebenen
HttpResponseMessage-Objekt. - Parst asynchron den Inhalt aus dem zurückgegebenen
HttpResponseMessage-Objekt. - Gibt den Inhalt zurück.
Dieser Code gibt nur einen String zurück. Möglicherweise möchten Sie ein komplexeres Objekt zurückgeben, das sowohl den Inhalt als auch den Statuscode enthält. Dafür können Sie einfach eine C#-struct deklarieren, die nach JavaScript überführt wird, wie
internal struct Result {
public int statusCode { get; set; }
public string content { get; set; }
}
Es wird empfohlen, hier die JavaScript-Namenskonventionen zu befolgen, da es derzeit keine automatische Zuordnung von Namen zwischen den gängigen Stilrichtlinien von C# und JS gibt.
Um den Wert zurückzugeben, müssen Sie natürlich die Signatur der Methode von der Rückgabe eines string zu Result aktualisieren
public async Task<Result> GetHttpResponseAsync(string uri) {
sowie den Statuscode speichern und die Rückgabeanweisung von return content; zu
var statusCode = httpResponseMessage.StatusCode;
return new Result()
{
statusCode = (int)statusCode,
content = content,
};
Aber warten Sie, wir haben bisher nur den Erfolgsfall besprochen. Was passiert, wenn GetHttpResponse nicht erfolgreich ist? Wir behandeln in diesem Beispiel keine Ausnahmen. Wenn eine Ausnahme ausgelöst wird, wie überführen wir einen Fehler zurück nach JavaScript? Das wird tatsächlich vom Framework für Sie übernommen: Jede Ausnahme in der Aufgabe wird auf der JavaScript-Seite als JavaScript-Ausnahme überführt.
Das ist alles! Wenn Sie das vollständige SimpleHttpModule sehen möchten, siehe AsyncMethodExamples.cs.
SimpleHttpModule in C++/WinRT
Beginnen wir mit der asynchronen nativen Methode, die die HTTP-Anfrage durchführt
static winrt::Windows::Foundation::IAsyncAction GetHttpResponseAsync(std::wstring uri) noexcept
{
// Create an HttpClient object
auto httpClient = winrt::Windows::Web::Http::HttpClient();
// Send the GET request asynchronously
auto httpResponseMessage = co_await httpClient.GetAsync(winrt::Windows::Foundation::Uri(uri));
// Parse response
auto statusCode = httpResponseMessage.StatusCode();
auto content = co_await httpResponseMessage.Content().ReadAsStringAsync();
// TODO: How to return the result?
}
Die Methode GetHttpResponseAsync ist zu diesem Zeitpunkt ziemlich einfach. Sie nimmt eine wstring URI und "gibt" eine IAsyncAction zurück (d. h. die Methode ist asynchron und gibt keinen tatsächlichen Wert zurück, wenn sie fertig ist).
Wenn Sie mit dem Schreiben von asynchronem C++/WinRT-Code nicht vertraut sind, lesen Sie Concurrency und asynchrone Operationen mit C++/WinRT.
Innerhalb von GetHttpResponseAsync sehen wir, dass sie
- Erstellt einen
HttpClient. - Ruft asynchron die Methode
GetAsyncauf, um eine HTTP-Anfrage für die URI zu stellen. - Parst den Statuscode aus dem zurückgegebenen
HttpResponseMessage-Objekt. - Parst asynchron den Inhalt aus dem zurückgegebenen
HttpResponseMessage-Objekt.
Jetzt haben wir statusCode und content, aber was machen wir damit? Wie rufen wir diese Methode von JS auf und wie bekommen wir das Ergebnis zurück nach JS?
Lassen Sie uns das kurz unterbrechen und mit dem Aufbau unseres nativen Moduls beginnen.
namespace NativeModuleSample
{
REACT_MODULE(SimpleHttpModule);
struct SimpleHttpModule
{
REACT_METHOD(GetHttpResponse);
void GetHttpResponse(std::wstring uri,
winrt::Microsoft::ReactNative::ReactPromise<winrt::Microsoft::ReactNative::JSValueObject> promise) noexcept
{
}
};
}
Hier definieren wir einfach SimpleHttpModule mit einer leeren GetHttpResponse-Methode.
Beachten Sie, dass die Methode selbst void ist und der letzte Parameter in der Signatur vom Typ ReactPromise<JSValueObject> ist. Dies zeigt React Native Windows an, dass wir eine promise-basierte Methode in JS wünschen und dass der erwartete Rückgabewert bei Erfolg vom Typ JSValueObject ist.
Alle Methodenparameter vor diesem finalen Promise sind die Eingabeparameter, die wir von JS überführt erwarten. In diesem Fall möchten wir eine einzelne Zeichenkette für die anzufordernde URI.
Das promise-Objekt ist unsere Schnittstelle zur Verarbeitung des Promises und zur Überführung eines Ergebnisses nach JS. Dazu rufen wir einfach promise.Resolve() mit dem Ergebnisobjekt auf (wenn die Operation erfolgreich war) oder promise.Reject() mit einem Fehler (wenn die Operation fehlgeschlagen ist).
Nachdem wir nun wissen, wie Ergebnisse zurückgegeben werden, bereiten wir GetHttpResponseAsync darauf vor, einen ReactPromise<JSValueObject>-Parameter entgegenzunehmen und ihn zu verwenden.
static winrt::Windows::Foundation::IAsyncAction GetHttpResponseAsync(std::wstring uri,
winrt::Microsoft::ReactNative::ReactPromise<winrt::Microsoft::ReactNative::JSValueObject> promise) noexcept
{
auto capturedPromise = promise;
// Create an HttpClient object
auto httpClient = winrt::Windows::Web::Http::HttpClient();
// Send the GET request asynchronously
auto httpResponseMessage = co_await httpClient.GetAsync(winrt::Windows::Foundation::Uri(uri));
// Parse response
auto statusCode = httpResponseMessage.StatusCode();
auto content = co_await httpResponseMessage.Content().ReadAsStringAsync();
// Build result object
auto resultObject = winrt::Microsoft::ReactNative::JSValueObject();
resultObject["statusCode"] = static_cast<int>(statusCode);
resultObject["content"] = winrt::to_string(content);
capturedPromise.Resolve(resultObject);
}
Was haben wir hier getan? Zunächst haben wir das promise lokal innerhalb der asynchronen Methode "erfasst", indem wir es in capturedPromise kopiert haben. Wir tun dies, weil es sich um eine asynchrone Methode handelt, die andere asynchrone Methoden aufruft, und andernfalls riskieren wir, dass das ReactPromise-Objekt von React Native Windows vorzeitig gelöscht wird.
Wichtig: Unser einziger Eingabeparameter in diesem Beispiel ist ein
wstring, aber wenn Ihre MethodeJSValue,JSValueArrayoderJSValueObjectParametertypen verwendet, müssen Sie diese ebenfalls mit einer Kopie "erfassen". Beispielstatic winrt::Windows::Foundation::IAsyncAction MethodAsync(winrt::Microsoft::ReactNative::JSValueObject options) noexcept { auto captureOptions = options.Copy(); ... }
Am Ende der Methode bauen wir einfach das Ergebnisobjekt auf, das an JS zurückgegeben werden soll, und übergeben es an capturedPromise.Resolve(). Das war's für GetHttpResponseAsync – wenn die Methodenausführung ohne Probleme bis zum Ende gelangt, wird das Promise aufgelöst, was das Ergebnis zurück nach JS überführt.
Nachdem GetHttpResponseAsync erledigt ist, überbrücken wir die Lücke zwischen ihr und unserer neuen nativen Modulmethode GetHttpResponse.
REACT_METHOD(GetHttpResponse);
void GetHttpResponse(std::wstring uri,
winrt::Microsoft::ReactNative::ReactPromise<winrt::Microsoft::ReactNative::JSValueObject> promise) noexcept
{
auto asyncOp = GetHttpResponseAsync(uri, promise);
}
Sieht einfach genug aus, oder? Wir rufen GetHttpResponseAsync mit den Parametern uri und promise auf und erhalten ein IAsyncAction-Objekt zurück, das wir in asyncOp speichern. Wenn dies ausgeführt wird, gibt GetHttpResponseAsync die Kontrolle zurück, wenn es seinen ersten co_await erreicht, was wiederum die Kontrolle zurückgibt, damit der JS-Code weiter ausgeführt werden kann. Wenn alles in GetHttpResponseAsync erfolgreich ist, ist es selbst dafür verantwortlich, das Promise mit dem Ergebnis aufzulösen.
Aber warten Sie, was passiert, wenn GetHttpResponseAsync nicht erfolgreich ist? Wir behandeln in diesem Beispiel keine Ausnahmen. Wenn eine Ausnahme ausgelöst wird, wie überführen wir einen Fehler zurück nach JS? Wir müssen noch eine Sache tun, und das ist die Überprüfung auf nicht behandelte Ausnahmen.
REACT_METHOD(GetHttpResponse);
void GetHttpResponse(std::wstring uri,
winrt::Microsoft::ReactNative::ReactPromise<winrt::Microsoft::ReactNative::JSValueObject> promise) noexcept
{
auto asyncOp = GetHttpResponseAsync(uri, promise);
asyncOp.Completed([promise](auto action, auto status)
{
if (status == winrt::Windows::Foundation::AsyncStatus::Error)
{
std::stringstream errorCode;
errorCode << "0x" << std::hex << action.ErrorCode() << std::endl;
auto error = winrt::Microsoft::ReactNative::ReactError();
error.Message = "HRESULT " + errorCode.str() + ": " + std::system_category().message(action.ErrorCode());
promise.Reject(error);
}
});
}
Wir haben einen AsyncActionCompletedHandler Lambda definiert und ihn so eingestellt, dass er ausgeführt wird, wenn asyncOp abgeschlossen ist. Hier prüfen wir, ob die Aktion fehlgeschlagen ist (d. h. status == AsyncStatus::Error) und bauen in diesem Fall ein ReactError-Objekt, dessen Nachricht sowohl den Fehlercode (ein Windows HRESULT) als auch die Fehlermeldung für diesen Code enthält. Dann übergeben wir diesen Fehler an promise.Reject() und überführen damit den Fehler zurück nach JS.
Wichtig: Dieses Beispiel zeigt den Minimalfall, bei dem Sie keine Fehler innerhalb von
GetHttpResponseAsyncbehandeln, aber Sie sind darauf nicht beschränkt. Sie können jederzeit Fehlerbedingungen in Ihrem Code erkennen undcapturedPromise.Reject()selbst mit (nützlicheren) Fehlermeldungen aufrufen. Sie sollten jedoch *immer* diesen abschließenden Handler einschließen, um unerwartete und unbehandelte Ausnahmen aufzufangen, die auftreten können, insbesondere beim Aufruf von Windows-APIs. Stellen Sie einfach sicher, dass SieReject()nur einmal aufrufen und danach nichts mehr ausgeführt wird.
Das ist alles! Wenn Sie das vollständige SimpleHttpModule sehen möchten, siehe AsyncMethodExamples.h.
Ausführen von API-Aufrufen auf dem UI-Thread
Seit Version 0.64 werden Aufrufe an native Module nicht mehr auf dem UI-Thread ausgeführt. Das bedeutet, dass jeder Aufruf von APIs, die auf dem UI-Thread ausgeführt werden müssen, jetzt explizit übergeben werden muss.
Dazu sollte der UIDispatcher verwendet werden.
Dieser Abschnitt behandelt das grundlegende Anwendungsszenario des UIDispatcher und seiner Post()-Methode mit dem WinRT FileOpenPicker (eine Beschreibung des Öffnens von Dateien und Ordnern mit einem Picker auf UWP finden Sie im Öffnen von Dateien und Ordnern mit einem Picker).
Verwendung von UIDispatcher mit C#
Nehmen wir an, wir haben ein natives Modul, das eine Datei mit FileOpenPicker öffnet.
Dem offiziellen Beispiel folgend würde die Methode des nativen Moduls, die den Picker startet, so aussehen:
[ReactMethod("openFile")]
public async void OpenFile()
{
var picker = new Windows.Storage.Pickers.FileOpenPicker();
// Other initialization code
Windows.Storage.StorageFile file = await picker.PickSingleFileAsync();
if (file != null)
{
// File opened successfully
}
else
{
// Error while opening the file
}
}
Ab React Native Windows 0.64 würde diese Methode jedoch mit System.Exception: Invalid window handle enden. Da die FileOpenPicker-API auf dem UI-Thread ausgeführt werden muss, müssen wir diesen Aufruf mit der Methode UIDispatcher.Post umschließen.
[ReactMethod("openFile")]
public void OpenFile()
{
context.Handle.UIDispatcher.Post(async () => {
var picker = new Windows.Storage.Pickers.FileOpenPicker();
// Other initialization code
Windows.Storage.StorageFile file = await picker.PickSingleFileAsync();
if (file != null)
{
// File opened successfully
}
else
{
// Error while opening the file
}
});
}
Hinweis:
UIDispatcherist über denReactContextverfügbar, den wir über eine alsReactInitializermarkierte Methode injizieren können.[ReactInitializer] public void Initialize(ReactContext reactContext) { context = reactContext; }
Wenn wir nun die Methode openFile in unserem JS-Code aufrufen, öffnet sich das Fenster des Dateipickers.
Verwendung von UIDispatcher mit C++/WinRT
Nehmen wir an, wir haben das native Modul, das die Datei mit FileOpenPicker öffnet und lädt.
Dem offiziellen Beispiel folgend würde die Methode des nativen Moduls, die den Picker startet, so aussehen:
REACT_METHOD(OpenFile, L"openFile");
winrt::fire_and_forget OpenFile() noexcept
{
winrt::Windows::Storage::Pickers::FileOpenPicker openPicker;
// Other initialization code
winrt::Windows::Storage::StorageFile file = co_await openPicker.PickSingleFileAsync();
if (file != nullptr)
{
// File opened successfully
}
else
{
// Error while opening the file
}
}
Ab React Native Windows 0.64 würde diese Methode jedoch mit ERROR_INVALID_WINDOW_HANDLE enden. Da die FileOpenPicker-API auf dem UI-Thread ausgeführt werden muss, müssen wir diesen Aufruf mit der Methode UIDispatcher.Post umschließen.
REACT_METHOD(OpenFile, L"openFile");
void OpenFile() noexcept
{
context.UIDispatcher().Post([]()->winrt::fire_and_forget {
winrt::Windows::Storage::Pickers::FileOpenPicker openPicker;
// Other initialization code
winrt::Windows::Storage::StorageFile file = co_await openPicker.PickSingleFileAsync();
if (file != nullptr)
{
// File opened successfully
}
else
{
// Error while opening the file
}
});
}
Hinweis:
UIDispatcherist über denReactContextverfügbar, den wir über eine alsREACT_INITmarkierte Methode injizieren können.REACT_INIT(Initialize); void Initialize(const winrt::Microsoft::ReactNative::ReactContext& reactContext) noexcept { context = reactContext; }
Wenn wir nun die Methode openFile in unserem JS-Code aufrufen, öffnet sich das Fenster des Dateipickers.