ReactXP wurde vom Skype-Team bei Microsoft entwickelt, um die Agilität und Effizienz der Entwicklung zu verbessern. In diesem Artikel werde ich mehr über die Architektur der neuen Skype-App sprechen.

Stores mit ReSub implementieren
Wir haben zunächst versucht, Flux zu verwenden, ein von Facebook-Ingenieuren entwickeltes Architekturmuster. Wir mochten einige seiner Eigenschaften, fanden es aber umständlich, da es uns zwang, eine Reihe von Hilfsklassen (Dispatcher, Aktionen, Aktionsersteller) zu implementieren. Die Zustandsverwaltung wurde in unseren komplexeren Komponenten auch schwer zu handhaben. Aus diesen Gründen entwickelten wir einen neuen Mechanismus, den wir ReSub nennen, kurz für „React Subscriptions“. ReSub bietet eine grob granulare Datenbindung zwischen Komponenten und Stores und automatisiert den Prozess des Abonnierens und Abmeldens. Weitere Details und Beispielcode finden Sie auf der ReSub GitHub-Seite.
Einige Stores innerhalb der App sind Singleton-Objekte und werden beim Start zugewiesen – und vielleicht sogar gefüllt. Andere werden bei Bedarf zugewiesen und haben eine klar definierte Lebensdauer, die einer Benutzerinteraktion oder einem Modus entspricht.
Daten lokal cachen
Stores sind für die Pflege von In-Memory-Datenrepräsentationen verantwortlich. Wir hatten auch die Notwendigkeit, Daten strukturiert zu speichern. Das lokale Speichern von Daten ermöglicht es der App, im „Offline“-Modus zu laufen. Es ermöglicht auch einen schnellen Start, da wir nicht darauf warten müssen, dass Daten über das Netzwerk heruntergeladen werden.
Für die lokale Speicherung haben wir eine plattformübergreifende NoSQL-Datenbankabstraktion entwickelt. Sie verwendet die native Datenbankimplementierung für jede Plattform (SQLite für iOS, IndexDB für einige Browser usw.). Die Abstraktion ermöglicht es uns, mehrere Tabellen zu erstellen und abzufragen. Jede Tabelle kann mehrere Indizes haben, einschließlich zusammengesetzter (Multi-Key) Indizes. Sie unterstützt auch Transaktionen und String-Indizierung für Volltextsuche.
Services & Startverwaltung
Hintergrundaufgaben wie das Abrufen neuer Nachrichten werden von Modulen behandelt, die wir als „Services“ bezeichnen. Dies sind Singleton-Objekte, die beim Start der App instanziiert werden. Einige Services sind für die Aktualisierung von Stores und das Speichern von Informationen in der lokalen Datenbank verantwortlich. Andere sind dafür verantwortlich, einen oder mehrere andere Stores zu überwachen und Informationen aus diesen Stores zu synthetisieren (z. B. Benachrichtigungen, die für eingehende Nachrichten generiert werden, die die sofortige Aufmerksamkeit des Benutzers erfordern).
In einigen Fällen war ein Service so eng an den Betrieb eines bestimmten Stores gebunden, dass wir ihre Funktionalität zu einem einzigen Modul zusammengefasst haben. Zum Beispiel haben wir einen ConfigurationStore erstellt, um appweite Konfigurationseinstellungen zu verfolgen (z. B. welche Funktionen für einen bestimmten Benutzer aktiviert sind). Wir hätten einen entsprechenden ConfigurationService implementieren können, der Konfigurationsaktualisierungen abruft, aber wir haben uns aus Pragmatismus dafür entschieden, diese Funktionalität innerhalb des ConfigurationStore zu implementieren.
Beim Start muss die App alle ihre Singleton-Stores und Services instanziieren, von denen einige Abhängigkeiten von anderen haben. Um diesen Startvorgang zu erleichtern, haben wir einen Startup-Manager erstellt. Jeder Store oder Service, der gestartet werden soll, muss eine Schnittstelle namens „IStartupable“ implementieren, die eine „startup“-Methode enthält, die ein Promise zurückgibt. Module registrieren sich beim Startup-Manager und geben an, von welchen anderen Modulen (falls vorhanden) sie abhängen. Dadurch kann der Startup-Manager Startroutinen parallel ausführen. Sobald ein Start-Promise aufgelöst ist, entsperrt es den Start aller abhängigen Module. Dies geht weiter, bis alle registrierten Module gestartet wurden.
Hier ist eine Startroutine, die ihren Store mit Daten aus der Datenbank füllt. Beachten Sie, dass die Startroutine ein Promise zurückgibt, das erst aufgelöst wird, nachdem der asynchrone Datenbankzugriff abgeschlossen ist.
startup(): SyncTasks.Promise<void> {
return ClientDatabase.getRecentConversations().then(conversations => {
this._conversations = conversations;
});
}
Kommunikation mit dem REST der Welt
Skype basiert auf über einem Dutzend verschiedener Microservices, die auf Azure laufen. Ein Microservice kümmert sich beispielsweise um die Nachrichtenübermittlung, ein anderer um die Speicherung und den Abruf von Fotos und Videos, und ein weiterer liefert dynamische Updates für Emoticon-Pakete. Jeder Microservice stellt seine Funktionalität über eine einfache REST-API bereit. Für jeden Dienst implementieren wir einen REST-Client, der die API für den Rest der App bereitstellt. Jeder REST-Client ist eine Unterklasse des Simple REST Client, der die Wiederholungslogik, die Authentifizierung und das Festlegen von HTTP-Headerwerten übernimmt.
Responsives Verhalten
Die Skype-App läuft auf einer Vielzahl von Geräten, von Telefonen bis hin zu Desktop-PCs mit großen Bildschirmen. Sie kann zur Laufzeit an Bildschirmgrößen- (und Orientierungs-) Änderungen angepasst werden. Dies ist hauptsächlich die Verantwortung von Komponenten in den oberen Ebenen der View-Hierarchie, die ihr Verhalten basierend auf der verfügbaren Bildschirmbreite ändern. Sie abonnieren einen Store, den wir „ResponsiveWidthStore“ nennen. Trotz seines Namens verfolgt dieser Store auch die Bildschirm- (oder Fenster-) Höhe und die Geräteausrichtung (Quer- vs. Hochformat).
Wie bei den meisten responsiven Websites haben wir mehrere „Breakpoints“-Breiten definiert. In unserem Fall haben wir drei solche Breakpoints gewählt, was bedeutet, dass unsere App in einem von vier verschiedenen responsiven „Modi“ funktioniert.

Im schmalsten Modus verwendet die App einen „Stack-Navigations“-Modus, bei dem UI-Panels übereinander gestapelt werden. Dies ist ein typisches Navigationsmuster für Telefone. In breiteren Modi verwendet die App einen „Composite-Navigations“-Modus, bei dem Panels nebeneinander positioniert sind, um den erweiterten Bildschirmbereich besser auszunutzen.
Navigation
Die App koordiniert Navigationsänderungen mithilfe eines NavigationStateStore. Komponenten können diesen Store abonnieren, um festzustellen, ob die App sich gerade im Modus „Stack-Navigation“ oder „Composite-Navigation“ befindet. Im Stack-Navigationsmodus zeichnet dieser Store den Inhalt des Stacks auf. Im Composite-Navigationsmodus zeichnet er auf, welche Panels und Sub-Panels derzeit angezeigt werden (und in einigen Fällen, in welchem Modus sie sich befinden). Dies wird über ein NavigationContext-Objekt verfolgt. Die Teile der View-Hierarchie, die auf Navigationsänderungen reagieren, haben jeweils einen entsprechenden NavigationContext. Einige Kontexte haben Verweise auf andere Kind-Kontexte, was die hierarchische Natur der UI widerspiegelt. Wenn der Benutzer eine Aktion ausführt, die zu einer Navigationsänderung führt, ist das NavigationAction-Modul für die Aktualisierung des NavigationContext und dessen Rückschreiben in den NavigationStateStore verantwortlich. Dies wiederum bewirkt, dass sich die UI aktualisiert.
Hier ist ein Code, der den typischen Ablauf demonstriert. Wir beginnen mit einem Event-Handler innerhalb eines Buttons.
private _onClickConversationButton() {
// Navigate to the conversation.
NavigationActions.navigateToConversation(this.props.conversationId);
}
Das NavigationActions-Modul aktualisiert dann den aktuellen Navigationskontext. Es muss sowohl die Stack- als auch die Composite-Fälle behandeln.
navigateToConversation(conversationId: string) {
let convContext = this.createConversationNavContext(conversationId);
if (NavigationStateStore.isUsingStackNav()) {
NavigationStateStore.pushNewStackContext(convContext);
} else {
NavigationStateStore.updateRightPanel(convContext);
}
}
Dies veranlasst den NavigationStateStore, seinen internen Zustand zu aktualisieren und eine Änderung auszulösen, die alle Abonnenten benachrichtigt.
pushNewStackContext(context: NavigationContext) {
this._navStack.push(context);
// Tell subscribers that the nav context changed.
this.trigger();
}
Der primäre Abonnent des NavigationStateStore ist eine Komponente namens RootNavigationView. Sie ist verantwortlich für das Rendern entweder eines RootStackNavigationView oder eines 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 } />
);
}
}