使用异步 Windows API
需要进行架构审查:本文档旨在支持针对 React Native 的“旧”或“遗留”架构的开发。它可能适用也可能不适用于新架构的开发,需要审查并可能需要更新。有关 React Native Windows 中 React Native 架构的信息,请参阅新旧架构。
有关 Windows 上原生开发的最新信息,请参阅原生平台:概述。
本文档和底层平台代码正在开发中。
原生模块的一个常见场景是从 JS 异步方法调用一个或多个原生异步方法。然而,如何正确地桥接这两个异步世界可能并非显而易见的,这可能导致代码不稳定且难以调试。
本文档提出了一些在 React Native Windows 中将 JS 异步方法桥接到原生代码时应遵循的最佳模式。它假设您已经熟悉原生模块的设置和编写基础知识。
以下示例的完整源代码在
microsoft/react-native-windows-samples
中的原生模块示例中提供。
编写调用异步 Windows API 的原生模块
让我们编写一个使用异步 Windows API 执行简单 HTTP 请求的原生模块。我们将其命名为 SimpleHttpModule
,它需要一个名为 GetHttpResponse
的单一、基于 Promise 的方法,该方法将 URI 字符串作为参数,并在成功时返回一个包含 HTTP 状态码和文本内容的对象。
最后,我们希望从 JS 中调用该方法,如下所示:
NativeModules.SimpleHttpModule.GetHttpResponse('https://msdocs.cn/react-native-windows/')
.then(result => console.log(result))
.catch(error => console.log(error));
SimpleHttpModule
C# 中的 C# 的原生模块支持 C# 中使用 async
、await
和 Task<T>
建立的常见异步编程模式。
要将模块公开给 JavaScript,您需要声明一个 C# 类。要指示应将其公开给 JavaScript,请使用 [ReactModule]
属性进行注解,如下所示:
namespace NativeModuleSample
{
[ReactModule]
class SimpleHttpModule
{
// Methods go here.
}
}
这将通过表达式 NativeModules.SimpleHttpModule
使对象可用于 JavaScript。默认情况下,JavaScript 名称将与 C# 类名匹配。如果您不希望类名与 JavaScript 中的名称匹配,即您希望通过表达式 NativeModules.CustomModule
访问模块。您可以传递一个自定义名称,如下所示:
[ReactModule("CustomModule")]
class SimpleHttpModule
{
...
}
现在我们想公开执行 HTTP 请求的方法。建议(也是默认做法)将这些函数异步编写。在 C# 中使用 async
和 await
关键字以及 Task<T>
类型编写异步代码非常简单直观。
如果您不熟悉编写异步 C# 代码,请参阅 在 C# 或 Visual Basic 中调用异步 API 和 异步编程,如果您还不熟悉,它们将教您这些概念。
使用 [ReactMethod]
属性注释的典型 Web 请求函数签名将如下所示:
[ReactMethod]
public async Task<string> GetHttpResponseAsync(string uri) {
...
}
现在您可以自由地填充逻辑,如下所示:
// 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;
代码执行以下步骤:
- 创建一个
HttpClient
。 - 异步调用
GetAsync
方法,对 URI 发出 HTTP 请求。 - 从返回的
HttpResponseMessage
对象中解析状态码。 - 异步从返回的
HttpResponseMessage
对象中解析内容。 - 返回内容
此代码仅返回一个字符串。您可能希望返回一个更复杂的对象,其中包含内容和状态码。为此,您可以简单地声明一个 C# struct
,它将被封送为 JavaScript,如下所示:
internal struct Result {
public int statusCode { get; set; }
public string content { get; set; }
}
建议此处遵循 JavaScript 命名约定,因为目前 C# 和 JS 的常见风格指南之间没有名称的自动映射。
要返回值,您当然必须将方法的签名从返回 string
更新为 Result
:
public async Task<Result> GetHttpResponseAsync(string uri) {
以及存储状态码并将 return 语句从 return content;
更新为:
var statusCode = httpResponseMessage.StatusCode;
return new Result()
{
statusCode = (int)statusCode,
content = content,
};
但是等等,我们只讨论了成功路径,如果 GetHttpResponse
不成功会发生什么?在此示例中,我们不处理任何异常。如果抛出异常,我们如何将错误封送回 JavaScript?这实际上由框架为您处理:任务中的任何异常都将作为 JavaScript 异常封送回 JavaScript 端。
就是这样!如果您想查看完整的 SimpleHttpModule
,请参阅 AsyncMethodExamples.cs
。
SimpleHttpModule
C++/WinRT 中的 让我们从执行 HTTP 请求的异步原生方法开始:
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?
}
GetHttpResponseAsync
方法到目前为止非常简单,它接受一个 wstring
URI 并“返回”一个 IAsyncAction
(也就是说,该方法是异步的,并且在完成后实际上不返回值)。
如果您不熟悉编写异步 C++/WinRT 代码,请参阅 C++/WinRT 中的并发和异步操作。
在 GetHttpResponseAsync
内部,我们看到它
- 创建一个
HttpClient
。 - 异步调用
GetAsync
方法,对 URI 发出 HTTP 请求。 - 从返回的
HttpResponseMessage
对象中解析状态码。 - 异步从返回的
HttpResponseMessage
对象中解析内容。
现在我们有了 statusCode
和 content
,但我们如何处理它们?我们如何从 JS 调用此方法,以及如何将结果返回给 JS?
让我们暂停一下,开始构建我们的原生模块。
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
{
}
};
}
这里我们简单地定义了一个带有空 GetHttpResponse
方法的 SimpleHttpModule
。
请注意,方法本身是 void
,并且签名中的最后一个参数是 ReactPromise<JSValueObject>
类型。这向 React Native Windows 指示我们希望在 JS 中使用基于 Promise 的方法,并且成功的预期返回值是 JSValueObject
类型。
此最终 Promise 之前的所有方法参数都是我们期望从 JS 封送的输入参数。在本例中,我们希望使用一个字符串作为请求的 URI。
promise
对象是我们处理 Promise 并将结果封送回 JS 的接口。为此,我们只需在结果对象(如果操作成功)时调用 promise.Resolve()
,或在发生错误(如果操作失败)时调用 promise.Reject()
。
既然我们知道如何返回结果,让我们准备 GetHttpResponseAsync
来接受 ReactPromise<JSValueObject>
参数并使用它:
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);
}
我们在这里做了什么?首先,我们通过将 promise
复制到 capturedPromise
中,在异步方法中“捕获”了它。我们这样做是因为这是一个异步方法调用其他异步方法,否则我们可能会使 ReactPromise
对象被 React Native Windows 过早删除。
重要:此示例中我们唯一的输入参数是
wstring
,但如果您的方法使用JSValue
、JSValueArray
或JSValueObject
参数类型,您也需要通过复制来“捕获”它们。例如:static winrt::Windows::Foundation::IAsyncAction MethodAsync(winrt::Microsoft::ReactNative::JSValueObject options) noexcept { auto captureOptions = options.Copy(); ... }
在该方法的底部,我们只需构建要返回到 JS 的结果对象,并将其传递给 capturedPromise.Resolve()
。GetHttpResponseAsync
的工作就完成了——如果方法执行没有任何问题地到达末尾,它将解决 Promise,从而将结果封送回 JS。
现在 GetHttpResponseAsync
已经处理完毕,让我们连接它和我们的新 GetHttpResponse
原生模块方法之间的桥梁。
REACT_METHOD(GetHttpResponse);
void GetHttpResponse(std::wstring uri,
winrt::Microsoft::ReactNative::ReactPromise<winrt::Microsoft::ReactNative::JSValueObject> promise) noexcept
{
auto asyncOp = GetHttpResponseAsync(uri, promise);
}
看起来很简单,对吧?我们使用 uri
和 promise
参数调用 GetHttpResponseAsync
,并返回一个 IAsyncAction
对象,我们将其存储在 asyncOp
中。当它执行时,GetHttpResponseAsync
在遇到第一个 co_await
时将返回控制权,这反过来将返回控制权给 JS 代码继续运行。当 GetHttpResponseAsync
中的所有内容都成功时,它本身负责用结果解决 Promise。
但是等等,如果 GetHttpResponseAsync
不成功会发生什么?在此示例中,我们不处理任何异常,因此如果抛出异常,我们如何将错误封送回 JS?我们还有一件事要做,那就是检查未处理的异常:
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);
}
});
}
我们定义了一个 AsyncActionCompletedHandler
lambda,并将其设置为在 asyncOp
完成时运行。在这里,我们检查操作是否失败(即 status == AsyncStatus::Error
),如果失败,我们构建一个 ReactError
对象,其中消息包含错误代码(Windows HRESULT
)和该代码的错误消息。然后我们将该错误传递给 promise.Reject()
,从而将错误封送回 JS。
重要:此示例显示了最小情况,即您在
GetHttpResponseAsync
中不处理任何错误,但您不限于此。您可以随时在代码中检测错误条件,并使用(更有用的)错误消息自行调用capturedPromise.Reject()
。但是,您应该始终包含此最终处理程序,以捕获可能发生的任何意外和未处理的异常,尤其是在调用 Windows API 时。只需确保您只调用一次Reject()
并且之后没有执行任何其他操作。
就是这样!如果您想查看完整的 SimpleHttpModule
,请参阅 AsyncMethodExamples.h
。
在 UI 线程上执行 API 调用
从 0.64 版本开始,对原生模块的调用不再在 UI 线程上运行。这意味着现在必须显式调度每个必须在 UI 线程上执行的 API 调用。
为此,应使用 UIDispatcher
。
本节将介绍 UIDispatcher
及其 Post()
方法与 WinRT FileOpenPicker
的基本使用场景(有关在 UWP 上使用选取器打开文件和文件夹的说明,请参阅 使用选取器打开文件和文件夹)。
UIDispatcher
与 C# 一起使用
将 假设我们有一个使用 FileOpenPicker
打开文件的原生模块。
按照官方示例,启动选取器的原生模块方法将如下所示:
[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
}
}
然而,从 react-native-windows 0.64 开始,此方法将以 System.Exception: Invalid window handle
结束。由于 FileOpenPicker
API 需要在 UI 线程上运行,我们需要将此调用包装在 UIDispatcher.Post
方法中。
[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
}
});
}
注意:
UIDispatcher
可通过ReactContext
获得,我们可以通过标记为ReactInitializer
的方法注入ReactContext
。
[ReactInitializer] public void Initialize(ReactContext reactContext) { context = reactContext; }
现在,如果我们在 JS 代码中调用 openFile
方法,文件选取器窗口将打开。
UIDispatcher
与 C++/WinRT 一起使用
将 假设我们有一个使用 FileOpenPicker
打开和加载文件的原生模块。
按照官方示例,启动选取器的原生模块方法将如下所示:
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
}
}
然而,从 react-native-windows 0.64 开始,此方法将以 ERROR_INVALID_WINDOW_HANDLE
结束。由于 FileOpenPicker
API 需要在 UI 线程上运行,我们需要将此调用包装在 UIDispatcher.Post
方法中。
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
}
});
}
注意:
UIDispatcher
可通过ReactContext
获得,我们可以通过标记为REACT_INIT
的方法注入ReactContext
。REACT_INIT(Initialize); void Initialize(const winrt::Microsoft::ReactNative::ReactContext& reactContext) noexcept { context = reactContext; }
现在,如果我们在 JS 代码中调用 openFile
方法,文件选取器窗口将打开。