ReactXP 是由微软 Skype 团队开发的,旨在提高开发敏捷性和效率。在本文中,我将详细介绍新 Skype 应用程序的架构。

使用 ReSub 实现 Stores
我们最初尝试使用 Flux,这是由 Facebook 工程师创建的一种架构模式。我们喜欢它的一些特性,但我们发现它很笨重,因为它需要我们实现一堆辅助类(调度器、操作、操作创建器)。在更复杂的组件中,状态管理也变得难以处理。出于这些原因,我们开发了一种新的机制,我们称之为 ReSub,即“React Subscriptions”的缩写。ReSub 在组件和 stores 之间提供粗粒度数据绑定,并自动化订阅和取消订阅的过程。更多详细信息和示例代码可在 ReSub 的 GitHub 站点上找到。
应用程序中的一些 stores 是单例对象,它们在启动时分配,甚至可能填充。其他 stores 按需分配,并具有明确的生命周期,与用户交互或模式相对应。
本地缓存数据
stores 负责维护内存中的数据表示。我们还需要以结构化的方式持久化数据。在本地存储数据允许应用程序在“离线”模式下运行。它还可以实现快速启动,因为我们不需要等待数据通过网络下载。
对于本地存储,我们开发了一个跨平台 No-SQL 数据库抽象。它使用每个平台的原生数据库实现(iOS 为 sqlite,某些浏览器为 indexDB 等)。该抽象允许我们创建和查询多个表。每个表可以有多个索引,包括复合(多键)索引。它还支持事务和字符串索引以进行全文搜索。
服务与启动管理
后台任务,例如获取新消息,由我们称为“服务”的模块处理。这些是应用程序启动时实例化的单例对象。一些服务负责更新 stores 并将信息保存到本地数据库。其他服务负责监听一个或多个其他 stores 并综合这些 stores 中的信息(例如,为需要用户立即关注的传入消息生成的通知)。
在某些情况下,服务与特定 store 的操作紧密绑定,以至于我们将它们的功能合并到一个模块中。例如,我们创建了一个 ConfigurationStore 来跟踪应用程序级别的配置设置(例如,为特定用户启用了哪些功能)。我们本可以实现一个相应的 ConfigurationService 来获取配置更新,但出于实用性考虑,我们选择在 ConfigurationStore 中实现此功能。
在启动时,应用程序需要实例化所有单例 stores 和服务,其中一些依赖于其他。为了促进这个启动过程,我们创建了一个启动管理器。每个想要启动的 store 或服务都必须实现一个名为“IStartupable”的接口,其中包括一个返回 promise 的“startup”方法。模块向启动管理器注册自己,并指定它们依赖于哪些其他模块(如果有)。这允许启动管理器并行运行启动例程。一旦启动 promise 得到解决,它就会解除任何依赖模块的启动。这会一直持续到所有注册模块都已启动。
这是一个启动例程,它用数据库中的数据填充其 store。请注意,启动例程返回一个 promise,该 promise 直到异步数据库访问完成后才会被解析。
startup(): SyncTasks.Promise<void> {
return ClientDatabase.getRecentConversations().then(conversations => {
this._conversations = conversations;
});
}
与外界的 REST 通信
Skype 建立在 Azure 上运行的十几个不同的微服务之上。例如,一个微服务处理消息传递,另一个处理照片和视频的存储和检索,还有一个提供表情符号包的动态更新。每个微服务都通过简单的 REST API 公开其功能。对于每个服务,我们都实现了一个 REST 客户端,将 API 公开给应用程序的其余部分。每个 REST 客户端都是 Simple REST 客户端的子类,它处理重试逻辑、身份验证和 HTTP 标头值的设置。
响应式行为
Skype 应用程序可在各种设备上运行,从手机到带有大屏幕的台式电脑。它能够在运行时适应屏幕尺寸(和方向)的变化。这主要是视图层次结构上层组件的责任,它们根据可用的屏幕宽度改变其行为。它们订阅了一个我们称为“ResponsiveWidthStore”的 store。尽管有其名称,该 store 还跟踪屏幕(或窗口)高度和设备方向(横向与纵向)。
与大多数响应式网站一样,我们定义了几个“断点”宽度。在我们的例子中,我们选择了三个这样的断点,这意味着我们的应用程序在四种不同的响应式“模式”之一中工作。

在最窄的模式下,应用程序使用“堆栈导航”模式,其中 UI 面板一个堆叠在另一个上面。这是手机的典型导航模式。对于更宽的模式,应用程序使用“复合导航”模式,其中面板彼此相邻放置,从而更好地利用扩展的屏幕空间。
导航
应用程序通过使用 NavigationStateStore 协调导航更改。组件可以订阅此 store 以确定应用程序当前是处于“堆栈导航”模式还是“复合导航”模式。在堆栈导航模式下,此 store 记录堆栈的内容。在复合导航模式下,它记录当前显示哪些面板和子面板(在某些情况下,它们处于哪种模式)。这是通过 NavigationContext 对象跟踪的。视图层次结构中响应导航更改的部分都具有相应的 NavigationContext。某些上下文具有对其他子上下文的引用,反映了 UI 的层次结构性质。当用户执行导致导航更改的操作时,NavigationAction 模块负责更新 NavigationContext 并将其写回 NavigationStateStore。这反过来又会导致 UI 更新。
以下是一些演示典型流程的代码。我们从按钮中的事件处理程序开始。
private _onClickConversationButton() {
// Navigate to the conversation.
NavigationActions.navigateToConversation(this.props.conversationId);
}
NavigationActions 模块然后更新当前导航上下文。它需要处理堆栈和复合两种情况。
navigateToConversation(conversationId: string) {
let convContext = this.createConversationNavContext(conversationId);
if (NavigationStateStore.isUsingStackNav()) {
NavigationStateStore.pushNewStackContext(convContext);
} else {
NavigationStateStore.updateRightPanel(convContext);
}
}
这会导致 NavigationStateStore 更新其内部状态并触发更改,从而通知任何订阅者。
pushNewStackContext(context: NavigationContext) {
this._navStack.push(context);
// Tell subscribers that the nav context changed.
this.trigger();
}
NavigationStateStore 的主要订阅者是一个名为 RootNavigationView 的组件。它负责渲染 RootStackNavigationView 或 RootCompositeNavigationView。
protected _buildState(/* params omitted */): RootNavigationViewState {
return {
isStackNav: NavigationStateStore.isUsingStackNav(),
compositeNavContext: NavigationStateStore.getCompositeNavContext(),
stackNavContext: NavigationStateStore.getStackNavContext()
};
}
render() {
if (this.state.isStackNav) {
return (
<RootStackNavigationView navContext={ this.state.stackNavContext } />
);
} else {
return (
<RootCompositeNavigationView navContext={ this.state.compositeNavContext } />
);
}
}