Angulareact

Ich habe ein Problem. Die Anwendung ist in Angular und die Komponentenbibliothek in React geschrieben. Klonen Sie eine Bibliothek zu teuer. Daher müssen Sie React-Komponenten in einer Angular-Anwendung mit minimalen Kosten verwenden. Wir finden heraus, wie es geht.


Haftungsausschluss


Ich bin überhaupt kein Spezialist bei Angular. Ich habe die erste Version im Jahr 2017 ausprobiert und mich dann ein wenig mit AngularDart befasst. Jetzt bin ich auf Anwendungsunterstützung für eine moderne Version des Frameworks gestoßen. Wenn es Ihnen so vorkommt, als wären die Entscheidungen seltsam oder "aus einer anderen Welt", dann scheint es Ihnen nicht so.


Die im Artikel vorgestellte Lösung wurde noch nicht in realen Projekten getestet und ist nur ein Konzept. Verwenden Sie es auf eigenes Risiko.


Das Problem


Ich unterstütze und entwickle jetzt eine ziemlich große Anwendung auf Angular 8. Außerdem gibt es einige kleine Anwendungen auf React und plant, ein Dutzend weitere (auch auf React) zu erstellen. Alle Anwendungen werden für die internen Anforderungen des Unternehmens verwendet und sollten den gleichen Stil haben. Die einzig logische Möglichkeit besteht darin, eine Komponentenbibliothek zu erstellen, auf deren Grundlage Sie schnell jede Anwendung erstellen können.


In der aktuellen Situation können Sie in React jedoch nicht einfach eine Bibliothek aufnehmen und schreiben. Sie können es nicht in der größten Anwendung verwenden - es ist in Angular geschrieben. Ich bin nicht der erste, der auf dieses Problem stößt. Das erste, was bei Google für "Reagieren in Winkel" zu finden ist, ist das Microsoft-Repository . Leider hat diese Lösung überhaupt keine Dokumentation. In der Readme-Datei des Projekts wird außerdem klargestellt, dass das Paket intern von Microsoft verwendet wird, das Team keine offensichtlichen Pläne für seine Entwicklung hat und die Verwendung nur auf eigenes Risiko erfolgt. Ich bin nicht bereit, ein so zweideutiges Paket in die Produktion zu ziehen, also habe ich beschlossen, mein Fahrrad zu schreiben.


Bild


Offizielle Seite @ Angular-React / Core


Lösung


React ist eine Bibliothek zur Lösung eines bestimmten Problems - der Verwaltung des DOM-Baums eines Dokuments. Wir können jedes React-Element in einen beliebigen DOM-Knoten einfügen.


const Hello = () => <p>Hello, React!</p>; render(<Hello />, document.getElementById('#hello')); 

Darüber hinaus gibt es keine Einschränkungen für wiederholte Aufrufe der render . Das heißt, es ist möglich, jede Komponente separat im DOM zu rendern. Und genau das wird dazu beitragen, den ersten Schritt in der Integration zu tun.


Vorbereitung


Zunächst ist es wichtig zu verstehen, dass React standardmäßig keine besonderen Bedingungen während der Montage erfordert. Wenn Sie JSX nicht verwenden, sich aber darauf beschränken, createElement , müssen Sie keine Schritte unternehmen. Alles funktioniert createElement .


Aber natürlich sind wir es gewohnt, JSX zu verwenden und wollen es nicht verlieren. Glücklicherweise verwendet Angular standardmäßig TypeScript, das JSX in Funktionsaufrufe umwandeln kann. Sie müssen nur das Compiler-Flag --jsx=react hinzufügen oder in tsconfig.json im Abschnitt compilerOptions die Zeile "jsx": "react" hinzufügen.


Integration anzeigen


Um zu beginnen, müssen wir sicherstellen, dass die React-Komponenten in der Angular-Anwendung angezeigt werden. Das heißt, damit die resultierenden DOM-Elemente aus der Bibliotheksarbeit die richtigen Stellen im Elementbaum einnehmen.


Jedes Mal, wenn Sie die React-Komponente verwenden, ist es zu schwierig, über das korrekte Aufrufen der render nachzudenken. Außerdem müssen wir in Zukunft Komponenten auf Datenebene und Ereignishandler integrieren. In diesem Fall ist es sinnvoll, eine Winkelkomponente zu erstellen, die die gesamte Logik zum Erstellen und Steuern des React-Elements enthält.


 // Hello.tsx export const Hello = () => <p>Hello, React!</p>; // hello.component.ts import { createElement } from 'react'; import { render } from 'react-dom'; import { Hello } from './Hello'; @Component({ selector: 'app-hello', template: `<div #react></div>`, }) export class HelloComponent implements OnInit { @ViewChild('react', { read: ElementRef, static: true }) ref: ElementRef; ngOnInit() { this.renderReactElement(); } private renderReactElement() { const props = {}; const reactElement = createElement(Hello, props); render(reactElement, this.ref.nativeElement); } } 

Der Code der Winkelkomponente ist äußerst einfach. Es selbst rendert nur einen leeren Container und erhält einen Link dazu. In diesem Fall wird zum Zeitpunkt der Initialisierung das Rendern des React-Elements aufgerufen. Es wird mit der Funktion createElement und an die createElement übergeben, die es in den aus Angular erstellten DOM-Knoten platziert. Sie können eine solche Komponente wie jede andere Angulat-Komponente ohne besondere Bedingungen verwenden.


Eingabeintegration


Wenn Sie Schnittstellenelemente anzeigen, müssen Sie normalerweise Daten an diese übertragen. Alles hier ist auch ziemlich prosaisch - wenn Sie createElement aufrufen createElement Sie alle Daten über die Requisiten an die Komponente übergeben.


 // Hello.tsx export const Hello = ({ name }) => <p>Hello, {name}!</p>; // hello.component.ts import { createElement } from 'react'; import { render } from 'react-dom'; import { Hello } from './Hello'; @Component({ selector: 'app-hello', template: `<div #react></div>`, }) export class HelloComponent implements OnInit { @ViewChild('react', { read: ElementRef, static: true }) ref: ElementRef; @Input() name: string; ngOnInit() { this.renderReactElement(); } private renderReactElement() { const props = { name: this.name, }; const reactElement = createElement(Hello, props); render(reactElement, this.ref.nativeElement); } } 

Jetzt können Sie die Namenszeichenfolge an die Angular-Komponente übergeben. Sie fällt in die React-Komponente und wird gerendert. Wenn sich die Zeile jedoch aus externen Gründen ändert, weiß React nichts davon und wir erhalten eine veraltete Anzeige. Angular verfügt über eine ngOnChanges Lebenszyklusmethode, mit der Sie Änderungen der Komponentenparameter und Reaktionen darauf verfolgen können. Wir implementieren die OnChanges Schnittstelle und fügen eine Methode hinzu:


 // ... ngOnChanges(_: SimpleChanges) { this.renderReactElement(); } // ... 

Es reicht aus, die render mit einem aus neuen Requisiten erstellten Element erneut aufzurufen, und die Bibliothek selbst ermittelt, welche Teile des Baums gerendert werden sollen. Der lokale Status innerhalb der Komponente bleibt ebenfalls erhalten.


Nach diesen Manipulationen kann die Winkelkomponente auf die übliche Weise verwendet werden und Daten an sie übergeben.


 <app-hello name="Angular"></app-hello> <app-hello [name]="name"></app-hello> 

Für eine genauere Arbeit mit der Aktualisierung von Komponenten können Sie sich die Strategie zur Änderungserkennung ansehen. Ich werde dies nicht im Detail betrachten.


Ausgabe-Integration


Ein weiteres Problem bleibt bestehen - die Reaktion der Anwendung auf Ereignisse innerhalb der React-Komponenten. @Output wir @Output Dekorator und @Output den Rückruf über Requisiten an die Komponente weiter.


 // Hello.tsx export const Hello = ({ name, onClick }) => ( <div> <p>Hello, {name}!</p> <button onClick={onClick}>Say "hello"</button> </div> ); // hello.component.ts import { createElement } from 'react'; import { render } from 'react-dom'; import { Hello } from './Hello'; @Component({ selector: 'app-hello', template: `<div #react></div>`, }) export class HelloComponent implements OnInit { @ViewChild('react', { read: ElementRef, static: true }) ref: ElementRef; @Input() name: string; @Output() click = new EventEmitter<string>(); ngOnInit() { this.renderReactElement(); } private renderReactElement() { const props = { name: this.name, onClick: () => this.lick.emit(`Hello, ${this.name}!`), }; const reactElement = createElement(Hello, props); render(reactElement, this.ref.nativeElement); } } 

Fertig. Wenn Sie die Komponente verwenden, können Sie Ereignishandler registrieren und darauf reagieren.


  <app-hello [name]="name" (click)="sayHello($event)"></app-hello> 

Das Ergebnis ist ein voll funktionsfähiger Wrapper für die React-Komponente zur Verwendung in einer Angular-Anwendung. Es kann Daten übertragen und auf darin enthaltene Ereignisse reagieren.


Noch eine Sache ...


Für mich ist die Zwei-Wege- ngModel die bequemste Sache in Angular. Es ist bequem, einfach und erfordert eine sehr kleine Menge Code. In der aktuellen Implementierung ist eine Integration jedoch nicht möglich. Dies kann behoben werden. Um ehrlich zu sein, verstehe ich nicht wirklich, wie dieser Mechanismus aus Sicht des internen Geräts funktioniert. Daher gebe ich zu, dass meine Lösung super-suboptimal ist und ich würde mich freuen, wenn Sie in den Kommentaren eine schönere Möglichkeit zur Unterstützung von ngModel .


Zunächst müssen Sie die ControlValueAccessor Schnittstelle implementieren (aus dem Paket @angular/forms und der Komponente einen neuen Anbieter hinzufügen.


 import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; const REACT_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => HelloComponent), multi: true }; @Component({ selector: 'app-hello', template: `<div #react></div>`, providers: [PREACT_VALUE_ACCESSOR], }) export class PreactComponent implements OnInit, OnChanges, ControlValueAccessor { // ... } 

Diese Schnittstelle erfordert die Implementierung der Methoden onBlur , writeValue , registerOnChange , registerOnTouched . Alle von ihnen sind in der Dokumentation gut beschrieben. Wir erkennen sie.


 //    const noop = () => {}; @Component({ selector: 'app-hello', template: `<div #react></div>`, providers: [PREACT_VALUE_ACCESSOR], }) export class PreactComponent implements OnInit, OnChanges, ControlValueAccessor { // ... private innerValue: string; //      ngModel private onTouchedCallback: Callback = noop; private onChangeCallback: Callback = noop; //       //       get value(): string { return this.innerValue; } set value(v: string) { if (v !== this.innerValue) { this.innerValue = v; //    ,   this.onChangeCallback(v); } } writeValue(value: string) { if (value !== this.innerValue) { this.innerValue = value; //        this.renderReactElement(); } } //   registerOnChange(fn: Callback) { this.onChangeCallback = fn; } registerOnTouched(fn: Callback) { this.onTouchedCallback = fn; } //    onBlur() { this.onTouchedCallback(); } } 

Danach müssen Sie sicherstellen, dass all dies an die React-Komponente übergeben wird. Leider kann React nicht mit der bidirektionalen Datenbindung arbeiten, daher geben wir ihm einen Wert und einen Rückruf, um ihn zu ändern. renderReactElement Methode.


 // ... private renderReactElement() { const props = { name: this.name, onClick: () => this.lick.emit(`Hello, ${this.name}!`), model: { value: this.value, onChange: v => { this.value = v; } }, }; const reactElement = createElement(Hello, props); render(reactElement, this.ref.nativeElement); } // ... 

In der React-Komponente verwenden wir diesen Wert und den Rückruf.


 export const Hello = ({ name, onClick, model }) => ( <div> <p>Hello, {name}!</p> <button onClick={onClick}>Say "hello"</button> <input value={model.value} onChange={e => model.onChange(e.target.value)} /> </div> ); 

Jetzt haben wir React wirklich in Angular integriert. Sie können die resultierende Komponente beliebig verwenden.


 <app-hello [name]="name" (click)="sayHello($event)" [(ngModel)]="name" ></app-hello> 

Zusammenfassung


React ist eine sehr einfache Bibliothek, die sich leicht in alles integrieren lässt. Mit dem gezeigten Ansatz können Sie nicht nur alle React-Komponenten in Angular-Anwendungen fortlaufend verwenden, sondern auch die gesamte Anwendung schrittweise migrieren.


In diesem Artikel habe ich überhaupt nicht auf Stylingprobleme eingegangen. Wenn Sie klassische CSS-in-JS-Lösungen (Styled-Components, Emotion, JSS) verwenden, müssen Sie keine zusätzlichen Maßnahmen ergreifen. Wenn das Projekt jedoch produktivere Lösungen erfordert (Kunstrasen, Linaria, CSS-Module), müssen Sie an der Webpack-Konfiguration arbeiten. Feature Story - Passen Sie die Webpack-Konfiguration in Ihrer Angular-Anwendung an .


Um die Anwendung vollständig von Angular nach React zu migrieren, müssen Sie noch das Problem der Implementierung von Diensten in React-Komponenten lösen. Eine einfache Möglichkeit besteht darin, den Service in eine Wrapper-Komponente zu integrieren und durch Requisiten weiterzuleiten. Der schwierige Weg besteht darin, eine Schicht zu schreiben, die per Token Dienste vom Injektor erhält. Berücksichtigung dieses Themas über den Rahmen des Artikels hinaus.


Bonus


Es ist wichtig zu verstehen, dass mit diesem Ansatz für 85 KB Angular fast 40 KB React- und React react-dom Code react . Dies kann erhebliche Auswirkungen auf die Geschwindigkeit der Anwendung haben. Ich empfehle die Verwendung des Miniatur-Preact, der nur 3 KB wiegt. Die Integration ist fast nicht anders.


  1. In tsconfig.json fügen tsconfig.json eine neue Kompilierungsoption hinzu - "jsxFactory": "h" zeigt an, dass Sie die h Funktion zum Konvertieren von JSX verwenden müssen. import { h } from 'preact' nun in jeder Datei mit JSX-Code import { h } from 'preact' .
  2. Alle Aufrufe von React.createElement durch Preact.h .
  3. Alle Aufrufe von ReactDOM.render durch Preact.render .

Fertig! Lesen Sie die Anweisungen für die Migration von React zu Preact . Es gibt praktisch keine Unterschiede.


UPD 19.9.19 16.49


Thematischer Link aus Kommentaren - Micro Frontends


UPD 20.9.19 14.30


Ein weiterer thematischer Link aus den Kommentaren - Micro Frontends

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


All Articles