Neues Odnoklassniki-Frontend: React in Java starten. Teil II



Wir setzen die Geschichte fort, wie es uns in Odnoklassniki mithilfe von GraalVM gelungen ist, sich mit Java und JavaScript anzufreunden und auf ein riesiges System mit viel Legacy-Code zu migrieren.

Im zweiten Teil des Artikels werden wir ausführlich über den Start, die Zusammenstellung und die Integration von Anwendungen auf dem neuen Stack sprechen, auf die Besonderheiten ihrer Arbeit sowohl auf dem Client als auch auf dem Server eingehen sowie die auf unserem Weg aufgetretenen Schwierigkeiten erörtern und Lösungen beschreiben, die ihnen bei der Überwindung helfen .

Wenn Sie den ersten Teil nicht gelesen habenIch kann es nur empfehlen. Daraus lernen Sie die Geschichte des Frontends in Odnoklassniki kennen und lernen seine historischen Merkmale kennen, gehen den Weg, um eine Lösung für die Probleme zu finden, die sich in unseren 13 Projektjahren angesammelt haben, und tauchen am Ende in die technischen Merkmale der Serverimplementierung der von uns getroffenen Entscheidung ein.

UI-Konfiguration


Um den UI-Code zu schreiben, haben wir die fortschrittlichsten Tools ausgewählt: Reagieren Sie mit MobX, CSS-Modulen, ESLint, TypeScript, Lerna. All dies wird mit Webpack gesammelt.



Anwendungsarchitektur


Wie im vorherigen Teil dieses Artikels beschrieben, werden wir zur Realisierung einer schrittweisen Migration neue Komponenten in die Site in DOM-Elemente mit benutzerdefinierten Namen einfügen, die im neuen UI-Stapel funktionieren, während sie für den Rest der Site wie ein DOM-Element mit aussehen seine API. Der Inhalt dieser Elemente kann auf dem Server gerendert werden.

Was ist es? Im Inneren befindet sich eine coole, modische, moderne MVC-Anwendung, die auf React ausgeführt wird und die Standard-DOM-API nach außen bereitstellt: Attribute, Methoden für dieses DOM-Element und Ereignisse.



Um solche Komponenten zu betreiben, haben wir einen speziellen Mechanismus entwickelt. Was macht er? Zunächst wird die Anwendung gemäß ihrer Beschreibung initialisiert. Zweitens bindet es die Komponente an den spezifischen DOM-Knoten, in dem sie startet. Es gibt auch zwei Engines (für den Client und für den Server), die diese Komponenten finden und rendern können.



Warum wird das benötigt? Tatsache ist, dass, wenn die gesamte Site in React erstellt wird, normalerweise die Site-Komponente in das Stammelement der Seite gerendert wird und diese Komponente keine Rolle spielt, was sich außerhalb befindet, sondern nur, was sich innerhalb befindet.

In unserem Fall ist alles komplizierter: Eine Reihe von Anwendungen muss die Möglichkeit haben, unserer Seite auf der Website mitzuteilen, dass ich es bin und dass sich etwas an mir ändert. Beispielsweise muss der Kalender ein Ereignis auslösen, bei dem der Benutzer auf die Schaltfläche geklickt hat und das Datum geändert wurde, oder außerhalb müssen Sie die Möglichkeit haben, innerhalb des Kalenders das Datum zu ändern. Zu diesem Zweck implementiert die Application Engine Fassaden in die Grundfunktionalität der Anwendung.

Wenn Sie eine Komponente an einen Client liefern, muss die Engine des alten Standorts diese Komponente starten können. Zu diesem Zweck werden während des Builds die für den Start erforderlichen Informationen gesammelt.

{
    "events-calendar": {
        "bundleName": "events-calendar",
        "js": "events-calendar-h4h5m.js",
        "css": "events-calendar-h4h5m.css"
    }
}


Den Attributen des Komponenten-Tags werden spezielle Markierungen hinzugefügt, die besagen, dass es sich bei dieser Anwendung um einen neuen Typ handelt. Der Code kann einer bestimmten JS-Datei entnommen werden. Gleichzeitig verfügt es über eigene Attribute, die zum Initialisieren dieser Komponente erforderlich sind: Sie bilden den Anfangszustand der Komponente im Geschäft.

<events-calendar	data-module="react-loader"
			data-bundle="events-calendar.js"
			date=".."
			marks="[{..}]"
			…
/>


Für die Rehydratisierung wird nicht eine Umwandlung des Anwendungsstatus verwendet, sondern Attribute, mit denen Datenverkehr gespart werden kann. Sie liegen in normalisierter Form vor und sind in der Regel kleiner als der von der Anwendung erstellte Speicher. Gleichzeitig ist die Zeit zum Neuerstellen des Speichers aus den Attributen auf dem Client kurz, sodass sie normalerweise vernachlässigt werden können.

Für den Kalender haben die Attribute beispielsweise nur ein hervorgehobenes Datum, und das Geschäft verfügt bereits über eine Matrix mit vollständigen Informationen für den Monat. Offensichtlich ist es sinnlos, es vom Server zu übertragen.

Wie führe ich den Code aus?


Das Konzept wurde an einfachen Funktionen getestet, die entweder eine Zeile für den Server angeben oder innerHTML für den Client schreiben. Aber in echtem Code gibt es Module und TypeScript.

Es gibt Standardlösungen für den Client, z. B. das Sammeln von Code mit Webpack, das selbst alles mahlt und dem Client in Form eines Bündels von Bundles zur Verfügung stellt. Und was tun für den Server bei Verwendung von GraalVM?



Betrachten wir zwei Optionen. Die erste besteht darin, TypeScript in JavaScript einzugeben, wie dies bei Node.js der Fall ist. Diese Option funktioniert in unserer Konfiguration leider nicht, wenn JavaScript die Gastsprache in GraalVM ist. In diesem Fall verfügt JavaScript weder über ein modulares System noch über Asynchronität. Weil Modularität und Arbeit mit Asynchronität eine bestimmte Laufzeit bieten: NodeJS oder einen Browser. In unserem Fall verfügt der Server über JavaScript, das Code nur synchron ausführen kann.

Die zweite Option: Sie können den Servercode einfach aus denselben Dateien ausführen, die für den Client erfasst wurden. Und diese Option funktioniert. Es gibt jedoch ein Problem, dass der Server andere Implementierungen für eine Reihe von Methoden benötigt. Beispielsweise wird die Funktion renderToString () auf dem Server aufgerufen, um die Komponente zu rendern, und ReactDOM.render () auf dem Client. Oder ein anderes Beispiel aus dem vorherigen Artikel: Um Texte und Einstellungen auf dem Server abzurufen, wird die von Java bereitgestellte Funktion aufgerufen und auf dem Client eine Implementierung in JS.

Als Lösung für dieses Problem können Sie Aliase aus Webpack verwenden. Mit ihnen können Sie zwei Implementierungen der von uns benötigten Klasse erstellen: für den Client und den Server. Geben Sie dann in den Konfigurationsdateien für Client und Server die entsprechende Implementierung an.



Zwei Konfigurationsdateien sind jedoch zwei Assemblys. Jedes Mal ist es langwierig und schwierig, alles separat für den Server und den Client zu sammeln.

Sie müssen eine solche Konfiguration erstellen, damit alles auf einmal erfasst wird.

Webpack-Konfiguration zum Ausführen von JS auf Server und Client


Um eine Lösung für dieses Problem zu finden, sehen wir uns an, aus welchen Teilen das Projekt besteht:



Erstens verfügt das Projekt über eine Laufzeit von Drittanbietern (Anbietern), die für den Client und den Server gleich ist. Es ändert sich fast nie. Dem Benutzer kann Rantime zur Verfügung gestellt werden, und er wird auf dem Client zwischengespeichert, bis wir die Version der Bibliothek eines Drittanbieters aktualisieren.

Zweitens gibt es unsere Laufzeit (Core), die den Start der Anwendung sicherstellt. Es verfügt über Methoden mit unterschiedlichen Implementierungen für Client und Server. Zum Beispiel das Abrufen von Lokalisierungstexten, Einstellungen usw. Diese Laufzeit ändert sich ebenfalls selten.

Drittens gibt es einen Komponentencode. Dies gilt sowohl für den Client als auch für den Server. Dadurch können Sie den Anwendungscode im Browser debuggen, ohne den Server überhaupt zu starten. Wenn auf dem Client ein Fehler aufgetreten ist, können Sie die Fehler in der Browserkonsole anzeigen, sich an alles erinnern und sicherstellen, dass beim Starten auf dem Server keine Fehler auftreten.

Insgesamt werden drei Teile erhalten, die zusammengebaut werden müssen. Wir möchten:
  • Konfigurieren Sie die Baugruppe jedes Teils separat.
  • Schreiben Sie die Abhängigkeiten zwischen ihnen auf, damit nicht jedes Teil in das fällt, was sich im anderen befindet.
  • Sammeln Sie alles in einem Durchgang.


Wie kann man die Teile, aus denen die Baugruppe bestehen wird, separat beschreiben? In Webpack gibt es eine Multikonfiguration: Sie verschenken einfach eine Reihe von Exporten der in jedem Teil enthaltenen Module.

module.exports = [{
  entry: './vendors.js',
}, {
  entry: './core.js'
}, {
 entry: './app.js'
}];


Alles wäre in Ordnung, aber in jedem dieser Teile wird der Code der Module, von denen dieser Teil abhängt, dupliziert:



Glücklicherweise gibt es in den grundlegenden Webpack-Plugins DllPlugin , mit dem Sie eine Liste der darin enthaltenen Module für jedes zusammengebaute Teil erhalten können. Für den Anbieter können Sie beispielsweise herausfinden, welche spezifischen Module in diesem Teil enthalten sind.

Wenn Sie ein anderes Teil erstellen, z. B. Kernbibliotheken, können wir sagen, dass diese vom Herstellerteil abhängen.



Während der Webpack-Assemblierung erkennt DllPlugin die Abhängigkeit des Kerns von einer Bibliothek, die sich bereits im Hersteller befindet, und fügt ihn nicht dem Kern hinzu, sondern fügt einfach einen Link dazu hinzu.

Dadurch werden drei Teile gleichzeitig zusammengesetzt und hängen voneinander ab. Wenn die erste Anwendung auf den Client heruntergeladen wird, werden die Laufzeit- und Kernbibliotheken im Browser-Cache gespeichert. Und da Odnoklassniki eine Site ist, kommt es ziemlich selten vor, dass der Tab, mit dem der Benutzer "für immer" öffnen kann, verdrängt wird. In den meisten Fällen wird bei Versionen neuer Versionen der Site nur der Anwendungscode aktualisiert.

Ressourcenlieferung


Betrachten Sie das Problem am Beispiel der Arbeit mit lokalisierten Texten, die in einer separaten Datenbank gespeichert sind.

Wenn Sie früher irgendwo auf dem Server Text in der Komponente benötigt haben, können Sie die Funktion aufrufen, um den Text abzurufen.

const pkg = l10n('smiles');

<div>
    : { pkg.getText('title') }
</div>


Das Abrufen von Text auf dem Server ist nicht schwierig, da die Serveranwendung eine schnelle Anforderung an die Datenbank stellen oder sogar alle Texte im Speicher zwischenspeichern kann.

Wie erhalte ich Texte in Komponenten einer Reaktion, die auf einem Server in GraalVM gerendert werden?

Wie im ersten Teil des Artikels erläutert, können Sie im globalen Kontext dem globalen Objekt, auf das Sie über JavaScript zugreifen möchten, Methoden hinzufügen. Es wurde beschlossen, eine Klasse mit allen Methoden für JavaScript verfügbar zu machen.

public class ServerMethods {
    
    /**
     *     
     */
    public String getText(String pkg, String key) {
    }
    
}


Fügen Sie dann eine Instanz dieser Klasse in den globalen JavaScript-Kontext ein:

//     Java   
js.putMember("serverMethods", serverMethods);


Aus diesem Grund rufen wir aus JavaScript in der Serverimplementierung einfach die Funktion auf:

function getText(pkg: string, key: string): string {
    return global.serverMethods.getText(pkg, key);
}


Tatsächlich ist dies ein Funktionsaufruf in Java, der den angeforderten Text zurückgibt. Direkte synchrone Interaktion und keine HTTP-Aufrufe.

Auf dem Client dauert es leider sehr lange, über HTTP zu gehen und für jeden Aufruf der Texteinfügefunktion in den Komponenten Texte zu empfangen. Sie können alle Texte vorab auf den Client herunterladen, aber die Texte allein wiegen mehrere zehn Megabyte, und es gibt andere Arten von Ressourcen.



Der Benutzer wird es leid sein zu warten, bis alles heruntergeladen ist, bevor er die Anwendung startet. Daher ist diese Methode nicht geeignet.

Ich möchte nur die Texte erhalten, die in einer bestimmten Anwendung benötigt werden. Unsere Texte sind in Pakete unterteilt. Daher können Sie die für die Anwendung erforderlichen Pakete sammeln und zusammen mit dem Bundle herunterladen. Wenn die Anwendung gestartet wird, befinden sich alle Texte bereits im Client-Cache.

Wie finde ich heraus, welche Texte eine Bewerbung benötigt?

Wir haben eine Vereinbarung getroffen, dass Textpakete im Code durch Aufrufen der Funktion l10n () erhalten werden, in die der Paketname NUR in Form eines Zeichenfolgenliteral übertragen wird:

const pkg = l10n('smiles');

<div>
    { pkg.getLMsg('title') }
</div>


Wir haben ein Webpack-Plugin geschrieben, das durch Analyse des AST-Baums des Komponentencodes alle Aufrufe der Funktion l10n () findet und Paketnamen aus den Argumenten sammelt. In ähnlicher Weise sammelt das Plugin Informationen über andere Arten von Ressourcen, die von der Anwendung benötigt werden.

Bei der Ausgabe nach dem Zusammenbau für jede Anwendung erhalten wir eine Konfiguration mit ihren Ressourcen:

{
    "events-calendar": {
       "pkg":  [
           "calendar",
           "dates"
       ],
       "cfg":  [
           "config1",
           "config2"
       ],
       "bundleName":  "events-calendar",
       "js":  "events-calendar.js",
       "css":  "events-calendar.css",
    }
}


Und natürlich dürfen wir die Aktualisierung der Texte nicht vergessen. Da auf dem Server alle Texte immer auf dem neuesten Stand sind und der Client einen separaten Cache-Aktualisierungsmechanismus benötigt, z. B. Watcher oder Push.

Alter Code in neuem


Bei einem reibungslosen Übergang tritt das Problem auf, den alten Code in neuen Komponenten wiederzuverwenden, da es große und komplexe Komponenten gibt (z. B. einen Videoplayer), deren Umschreiben viel Zeit in Anspruch nimmt und die Sie jetzt im neuen Stapel verwenden müssen.



Was sind die Probleme?

  • Die alte Site und die neuen React-Apps haben völlig unterschiedliche Lebenszyklen.
  • Wenn Sie den Code des alten Beispiels in die React-Anwendung einfügen, wird dieser Code nicht gestartet, da React nicht weiß, wie er aktiviert werden soll.
  • Aufgrund unterschiedlicher Lebenszyklen versuchen React und die alte Engine möglicherweise gleichzeitig, den Inhalt des alten Codes zu ändern, was zu unangenehmen Nebenwirkungen führen kann.


Um diese Probleme zu lösen, wurde eine gemeinsame Basisklasse für Komponenten zugewiesen, die alten Code enthalten. Die Klasse ermöglicht es Erben, die Lebenszyklen von React- und Old-Style-Anwendungen zu koordinieren.

export class OldCodeBase<T> extends React.Component<T> {

    ref: React.RefObject<HTMLElement> = React.createRef();

    componentDidMount() {
        //       DOM
        this.props.activate(this.ref.current!); 
    }

    componentWillUnmount() {
        //       DOM
        this.props.deactivate(this.ref.current!); 
    }

    shouldComponentUpdate() {
        // React     , 
        //   React-. 
        //     .
        return false;
    }

    render() {
        return (
            <div ref={this.ref}></div>
        );
    }
}


Mit der Klasse können Sie entweder Codeteile erstellen, die auf die alte Weise funktionieren, oder sie zerstören, während keine gleichzeitige Interaktion mit ihnen stattfindet.

Fügen Sie alten Code auf dem Server ein


In der Praxis besteht ein Bedarf an Wrapper-Komponenten (z. B. Popups), deren Inhalt beliebig sein kann, einschließlich solcher, die mit alten Technologien erstellt wurden. Sie müssen herausfinden, wie Sie Code in solche Komponenten auf dem Server einbetten können.

In einem früheren Artikel haben wir über die Verwendung von Attributen gesprochen, um Parameter an neue Komponenten auf dem Client und Server zu übergeben.

<cool-app users="[1,2,3]" />


Und jetzt wollen wir dort noch ein Stück Markup einfügen, was in der Bedeutung kein Attribut ist. Zu diesem Zweck wurde beschlossen, ein System von Slots zu verwenden.

<cool-app>
    <ui:part id="old-code">
        <div>old component</div>
    </ui:part>
</cool-app>


Wie Sie im obigen Beispiel sehen können, wird im Code der Cool-App-Komponente ein alter Code-Slot beschrieben, der alte Komponenten enthält. Dann wird innerhalb der Reaktionskomponente die Stelle angezeigt, an der Sie den Inhalt dieses Steckplatzes einfügen möchten:

render() {
    return (
        <div>
            <UiPart id="old-code" />
        </div>
    );
}


Die Server-Engine rendert diese Reaktionskomponente und rahmt den Inhalt des Steckplatzes in das <ui-part> -Tag ein, wobei ihr das Attribut data-part-id = "old-code" zugewiesen wird.

<cool-app>
    <div>
        <ui-part data-part-id="old-code">
            old code
        </ui-part>
    </div>
</cool-app>


Wenn das Server-Rendering von JS in GraalVM nicht in das Timeout passte, greifen wir auf das Client-Rendering zurück. Zu diesem Zweck gibt die Engine auf dem Server nur Slots an und rahmt sie in das Vorlagen-Tag ein, damit der Browser nicht mit ihrem Code interagiert.

<cool-app>
    <template>
        <ui-part data-part-id="old-code">
            old code
        </ui-part>
    </template>
</cool-app>


Was passiert auf dem Client? Die Client-Engine scannt einfach den Komponentencode, sammelt die <ui-part> -Tags, empfängt ihren Inhalt als Zeichenfolgen und übergibt sie zusammen mit den übrigen Parametern an die Rendering-Funktion.

var tagName = 'cool-app';
var reactComponent = components[tagName];
reactComponent.render({
       tagName: tagName,
       attrs: attrs,
       parts: parts,
       node: element
});


Der Code der Komponente, die die Steckplätze an der gewünschten Stelle einfügt, lautet wie folgt:

export class UiPart extends OldCodeBase<IProps> {

	render() {
		const id = this.props.id;
		const parts = this.props.parts;

		if (!parts.hasOwnProperty(id)) {
			return null;
		}

		return React.createElement('ui-part', {
			'data-part-id': id,
			ref: this.ref,
			dangerouslySetInnerHTML: { __html: parts[id] }
		});
	}
}


Gleichzeitig wird es von der OldCodeBase-Klasse geerbt, wodurch die Probleme der Interaktion zwischen dem alten und dem neuen Stapel gelöst werden.



Jetzt können Sie ein Popup schreiben und es mit dem neuen Stapel oder einer Anforderung vom Server mit dem alten Ansatz füllen. In diesem Fall funktionieren die Komponenten ordnungsgemäß.

Auf diese Weise können Sie Standortkomponenten schrittweise auf einen neuen Stapel migrieren.
Genau dies war eine der Hauptanforderungen für das neue Frontend.

Zusammenfassung


Alle fragen sich, wie schnell GraalVM funktioniert. Die Entwickler von Odnoklassniki führten verschiedene Tests mit React-Anwendungen durch.

Eine einfache Funktion, die nach dem Aufwärmen einen String zurückgibt, dauert etwa 1 Mikrosekunde.

Komponenten (erneut nach dem Aufwärmen) - je nach Größe zwischen 0,5 und 6 Millisekunden.

GraalVM beschleunigt langsamer als V8. Aber für die Zeit des Aufwärmens wird die Situation dank des Fallbacks für das Client-Rendering geglättet. Da es so viele Benutzer gibt, heizt sich die virtuelle Maschine schnell auf.

Was hast du geschafft?



  • Führen Sie JavaScript auf dem Server in der Java-Welt von Classmates aus.
  • Erstellen Sie isomorphen Code für die Benutzeroberfläche.
  • Verwenden Sie einen modernen Stack, den alle Front-End-Anbieter kennen.
  • Erstellen Sie eine gemeinsame Plattform und einen einzigen Ansatz zum Schreiben der Benutzeroberfläche.
  • Starten Sie einen reibungslosen Übergang, ohne den Vorgang zu verkomplizieren und das Server-Rendering nicht zu verlangsamen.


Wir hoffen, dass die Erfahrungen von Odnoklassniki und Beispiele für Sie nützlich sind und Sie sie für Ihre Arbeit finden.

Source: https://habr.com/ru/post/undefined/


All Articles