Angulareact

J'ai un problème. L'application est écrite en angulaire et la bibliothèque de composants en React. Clonez une bibliothèque trop chère. Vous devez donc utiliser les composants React dans une application angulaire à un coût minimal. Nous trouvons comment le faire.


Clause de non-responsabilité


Je ne suis pas du tout spécialiste chez Angular. J'ai essayé la première version en 2017, puis j'ai regardé un peu AngularDart, et maintenant je suis tombé sur le support d'application sur une version moderne du framework. S'il vous semble que les décisions sont étranges ou "d'un autre monde", cela ne vous semble pas.


La solution présentée dans l'article n'a pas encore été testée dans des projets réels et n'est qu'un concept. Utilisez-le à vos risques et périls.


Le problème


Je supporte et développe maintenant une application assez grande sur Angular 8. De plus, il y a quelques petites applications sur React et prévoit d'en construire une douzaine d'autres (également sur React). Toutes les applications sont utilisées pour les besoins internes de l'entreprise et doivent être du même style. La seule façon logique est de créer une bibliothèque de composants sur la base de laquelle vous pouvez rapidement créer n'importe quelle application.


Mais dans la situation actuelle, vous ne pouvez pas simplement prendre et écrire une bibliothèque sur React. Vous ne pouvez pas l'utiliser dans la plus grande application - il est écrit en angulaire. Je ne suis pas le premier à rencontrer ce problème. La première chose à être sur Google pour "réagir en angulaire" est le référentiel Microsoft . Malheureusement, cette solution ne contient aucune documentation. De plus, dans le fichier Lisezmoi du projet, il est clairement indiqué que le package est utilisé en interne par Microsoft, l'équipe n'a aucun plan évident pour son développement et ce n'est qu'à vos risques et périls de l'utiliser. Je ne suis pas prêt à faire glisser un paquet aussi ambigu dans la production, j'ai donc décidé d'écrire mon vélo.


image


Site officiel @ angular-react / core


Solution


React est une bibliothèque conçue pour résoudre un problème spécifique - la gestion de l'arborescence DOM d'un document. Nous pouvons placer n'importe quel élément React dans un nœud DOM arbitraire.


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

De plus, il n'y a aucune restriction sur les appels répétés à la fonction de render . Autrement dit, il est possible de rendre chaque composant séparément dans le DOM. Et c'est exactement ce qui aidera à franchir la première étape de l'intégration.


La préparation


Tout d'abord, il est important de comprendre que React par défaut ne nécessite aucune condition particulière lors de l'assemblage. Si vous n'utilisez pas JSX, mais createElement à appeler createElement , vous n'aurez pas à prendre de mesures, tout fonctionnera tout seul.


Mais, bien sûr, nous sommes habitués à utiliser JSX et nous ne voulons pas le perdre. Heureusement, Angular utilise TypeScript par défaut, qui peut transformer JSX en appels de fonction. Il vous suffit d'ajouter l'indicateur du compilateur --jsx=react ou dans tsconfig.json dans la section compilerOptions , ajoutez la ligne "jsx": "react" .


Intégration d'affichage


Pour commencer, nous devons nous assurer que les composants React sont affichés dans l'application Angular. Autrement dit, pour que les éléments DOM résultants du travail de bibliothèque occupent les bons emplacements dans l'arborescence des éléments.


Chaque fois que vous utilisez le composant React, penser à appeler correctement la fonction de render est trop difficile. De plus, à l'avenir, nous devrons intégrer des composants au niveau des données et des gestionnaires d'événements. Dans ce cas, il est logique de créer un composant angulaire qui encapsulera toute la logique de création et de contrôle de l'élément React.


 // 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); } } 

Le code du composant Angular est extrêmement simple. Il ne rend lui-même qu'un conteneur vide et obtient un lien vers celui-ci. Dans ce cas, au moment de l'initialisation, le rendu de l'élément React est appelé. Il est créé à l'aide de la fonction createElement et transmis à la fonction de render , qui le place dans le nœud DOM créé à partir d'Angular. Vous pouvez utiliser un tel composant comme tout autre composant Angulat, sans conditions particulières.


Intégration d'entrée


Habituellement, lors de l'affichage des éléments d'interface, vous devez leur transférer des données. Tout ici est également assez prosaïque - lorsque vous appelez createElement vous pouvez transmettre toutes les données via les accessoires au composant.


 // 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); } } 

Vous pouvez maintenant passer la chaîne de name au composant angulaire, elle tombera dans le composant React et sera rendue. Mais si la ligne change pour des raisons externes, React n'en sera pas informé et nous obtiendrons un affichage obsolète. Angular a une ngOnChanges cycle de vie ngOnChanges qui vous permet de suivre les changements dans les paramètres des composants et leurs réactions. Nous implémentons l'interface OnChanges et ajoutons une méthode:


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

Il suffit d'appeler à nouveau la fonction de render avec un élément créé à partir de nouveaux accessoires, et la bibliothèque elle-même déterminera quelles parties de l'arborescence doivent être rendues. L'état local à l'intérieur du composant sera également conservé.


Après ces manipulations, le composant angulaire peut être utilisé de la manière habituelle et lui transmettre des données.


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

Pour un travail plus fin avec la mise à jour des composants, vous pouvez regarder vers la stratégie de détection des changements . Je ne l'examinerai pas en détail.


Intégration de sortie


Un autre problème demeure - la réaction de l'application aux événements à l'intérieur des composants React. Utilisons @Output décorateur @Output et passons le rappel au composant via des accessoires.


 // 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); } } 

C'est fait. Lorsque vous utilisez le composant, vous pouvez enregistrer des gestionnaires d'événements et y répondre.


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

Le résultat est un wrapper entièrement fonctionnel pour le composant React à utiliser dans une application angulaire. Il peut transmettre des données et répondre aux événements qu'il contient.


Encore une chose ...


Pour moi, la chose la plus pratique dans Angular est la liaison de données bidirectionnelle, ngModel . Il est pratique, simple, nécessite une très petite quantité de code. Mais dans l'implémentation actuelle, l'intégration n'est pas possible. Cela peut être corrigé. Pour être honnête, je ne comprends pas vraiment comment ce mécanisme fonctionne du point de vue de l'appareil interne. Par conséquent, j'avoue que ma solution est super-sous-optimale et je serai heureux si vous écrivez dans les commentaires une manière plus belle de prendre en charge ngModel .


Tout d'abord, vous devez implémenter l'interface ControlValueAccessor (à partir du package @angular/forms et ajouter un nouveau fournisseur au composant.


 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 { // ... } 

Cette interface nécessite l'implémentation des méthodes onBlur , writeValue , registerOnChange , registerOnTouched . Tous sont bien décrits dans la documentation. Nous les réalisons.


 //    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(); } } 

Après cela, vous devez vous assurer que tout cela est transmis au composant React. Malheureusement, React n'est pas en mesure de fonctionner avec la liaison de données bidirectionnelle, nous lui donnerons donc une valeur et un rappel pour le modifier. renderReactElement méthode renderReactElement .


 // ... 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); } // ... 

Et dans le composant React, nous utiliserons cette valeur et le rappel.


 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> ); 

Maintenant, nous avons vraiment intégré React dans Angular. Vous pouvez utiliser le composant résultant comme vous le souhaitez.


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

Résumé


React est une bibliothèque très simple qui est facile à intégrer avec n'importe quoi. En utilisant l'approche illustrée, vous pouvez non seulement utiliser de façon continue des composants React dans des applications angulaires, mais également migrer progressivement l'ensemble de l'application.


Dans cet article, je n'ai pas du tout abordé les problèmes de style. Si vous utilisez des solutions CSS-in-JS classiques (composants de style, émotion, JSS), vous n'avez aucune action supplémentaire à effectuer. Mais si le projet nécessite des solutions plus productives (astroturf, linaria, modules CSS), vous devrez travailler sur la configuration du webpack. Article de fond - Personnalisez la configuration de Webpack dans votre application angulaire .


Pour migrer complètement l'application d'Angular vers React, vous devez toujours résoudre le problème d'implémentation des services dans les composants React. Un moyen simple consiste à obtenir le service dans un composant wrapper et à le passer par des accessoires. La difficulté est d'écrire une couche qui obtiendra les services de l'injecteur par jeton. Examen de cette question au-delà de la portée de l'article.


Bonus


Il est important de comprendre qu'avec cette approche de 85 Ko d'Angular, près de 40 Ko de code React et React react-dom sont react . Cela peut avoir un impact significatif sur la vitesse de l'application. Je recommande d'envisager d'utiliser le Preact miniature, qui ne pèse que 3 Ko. Son intégration n'est presque pas différente.


  1. Dans tsconfig.json ajoutons une nouvelle option de compilation - "jsxFactory": "h" , cela indiquera que vous devez utiliser la fonction h pour convertir JSX. Maintenant, dans chaque fichier avec le code JSX - import { h } from 'preact' .
  2. Tous les appels à React.createElement remplacés par Preact.h .
  3. Tous les appels à ReactDOM.render remplacés par Preact.render .

C'est fait! Lisez les instructions de migration de React vers Preact . Il n'y a pratiquement aucune différence.


UPD 19.9.19 16.49


Lien thématique à partir des commentaires - Micro Frontends


UPD 20.9.19 14.30


Un autre lien thématique des commentaires - Micro Frontends

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


All Articles