角效应

我有问题 该应用程序是用Angular编写的,而组件库是用React编写的。 复制库太昂贵。 因此,您需要以最小的成本在Angular应用程序中使用React组件。 我们找出方法。


免责声明


我根本不是Angular的专家。 我在2017年尝试了第一个版本,然后稍微看了看AngularDart,现在遇到了现代版本框架的应用程序支持。 如果您觉得这些决定很奇怪,或者是“来自另一个世界”,那么在您看来,这似乎并没有。


本文提供的解决方案尚未在实际项目中进行测试,仅是一个概念。 使用它需要您自担风险。


问题


我现在在Angular 8上支持并开发了一个相当大的应用程序。此外,React上有几个小型应用程序,并计划构建更多(也基于React)的应用程序。 所有应用程序均用于公司的内部需求,并且应采用相同的样式。 唯一的逻辑方法是创建一个组件库,在此基础上您可以快速构建任何应用程序。


但是在当前情况下,您不能仅仅在React上使用并编写一个库。 您不能在最大的应用程序中使用它-它是用Angular编写的。 我不是第一个遇到这个问题的人。 微软在Google上首先要进行“角度反应”。 不幸的是,该解决方案根本没有文档。 另外,在项目的自述文件中,可以清楚地知道该软件包是由Microsoft内部使用的,团队没有明显的开发计划,使用该风险由您自己承担。 我还没有准备好将这样一个模棱两可的包拖入生产环境,所以我决定写我的自行车。


图片


官方网站@角反应/核心


解决方案


React是一个旨在解决一个特定问题的库-管理文档的DOM树。 我们可以将任何React元素放在任意DOM节点中。


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

此外,对重复调用render函数没有任何限制。 也就是说,可以在DOM中分别渲染每个组件。 而这正是有助于迈出集成第一步的原因。


准备工作


首先,重要的是要了解,默认情况下,React在组装过程中不需要任何特殊条件。 如果您不使用JSX,而只限于调用createElement ,那么您将不必采取任何步骤,一切都将立即可用。


但是,当然,我们习惯于使用JSX,并且不想丢失它。 幸运的是,Angular默认使用TypeScript,它可以将JSX转换为函数调用。 您只需要添加编译器标志--jsx=react或在tsconfig.json部分的tsconfig.json中添加行"jsx": "react"


显示整合


首先,我们需要确保React组件显示在Angular应用程序中。 也就是说,从库工作中得到的DOM元素将在元素树中占据正确的位置。


每次使用React组件时,考虑正确调用render函数都太困难了。 另外,将来我们将需要在数据级别和事件处理程序上集成组件。 在这种情况下,创建一个Angular组件是有意义的,该组件将封装创建和控制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); } } 

Angular组件的代码非常简单。 它本身仅呈现一个空容器并获得指向它的链接。 在这种情况下,在初始化时,将调用React元素的渲染。 它是使用createElement函数创建的,并传递给render函数,该函数将其放置在从Angular创建的DOM节点中。 您可以像其他任何Angulat组件一样使用此类组件,没有特殊条件。


输入整合


通常,在显示界面元素时,您需要向其传输数据。 这里的一切也很平淡-调用createElement您可以将所有数据通过道具传递到组件。


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

现在,您可以将name字符串传递给Angular组件,它将落入React组件并进行渲染。 但是,如果由于某些外部原因导致行更改,React将不知道该行,并且我们将获得过时的显示。 Angular具有ngOnChanges生命周期ngOnChanges ,可让您跟踪组件参数的变化以及对其的反应。 我们实现OnChanges接口并添加一个方法:


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

只需使用从新道具创建的元素再次调用render函数就足够了,并且库本身将确定应渲染树的哪些部分。 组件内部的本地状态也将保留。


完成这些操作后,可以按通常的方式使用Angular组件并将数据传递给它。


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

为了更好地使用更新组件,您可以考虑使用变更检测策略 。 我不会详细考虑这一点。


输出整合


另一个问题仍然存在-应用程序对React组件内部事件的反应。 让我们@Output装饰器,并通过@Output将回调传递给组件。


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

做完了 使用组件时,您可以注册事件处理程序并对其进行响应。


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

结果是在Angular应用程序中使用的React组件的全功能包装器。 它可以传输数据并响应其中的事件。


还有一件事...


对我来说,Angular最方便的事情是双向数据绑定ngModel 。 它方便,简单,只需要很少的代码。 但是在当前的实现中,集成是不可能的。 这可以解决。 老实说,从内部设备的角度来看,我并不真正了解此机制的工作原理。 因此,我承认我的解决方案是超次优的,如果您在注释中写出一种更漂亮的方法来支持ngModel ,我将感到高兴。


首先,您需要实现ControlValueAccessor接口(从@angular/forms包中实现,并将新的提供者添加到组件中。


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

该接口要求实现onBlurwriteValueregisterOnChangeregisterOnTouched 。 所有这些都在文档中进行了详细描述。 我们意识到他们。


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

之后,您需要确保所有这些都传递给React组件。 不幸的是,React无法使用双向数据绑定,因此我们将为其赋予一个值和一个回调以对其进行更改。 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); } // ... 

在React组件中,我们将使用此值和回调。


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

现在,我们确实将React集成到Angular中。 您可以根据需要使用生成的组件。


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

总结


React是一个非常简单的库,很容易与任何东西集成。 使用所示的方法,您不仅可以持续使用Angular应用程序内的任何React组件,还可以逐步迁移整个应用程序。


在本文中,我根本没有涉及样式问题。 如果您使用经典的CSS-in-JS解决方案(样式化组件,情感,JSS),则无需执行任何其他操作。 但是,如果该项目需要更有效的解决方案(astroturf,linaria,CSS模块),则需要进行webpack配置。 专题报道- 在Angular应用程序中自定义Webpack配置


要将应用程序从Angular完全迁移到React,您仍然需要解决在React-components中实现服务的问题。 一种简单的方法是将服务获取到包装器组件中,并将其通过props传递。 困难的方法是编写一个将通过令牌从注入器获取服务的层。 对这个问题的考虑超出了本文的范围。


红利


重要的是要了解,通过这种对85KB Angular的方法,几乎​​添加了40KB的reactreact-dom代码。 这可能会对应用程序的速度产生重大影响。 我建议考虑使用微型Preact,它只有3KB。 它的集成几乎没有什么不同。


  1. tsconfig.json添加了一个新的编译选项- "jsxFactory": "h" ,这表明您需要使用h函数来转换JSX。 现在,在每个带有JSX代码的文件中- import { h } from 'preact'
  2. React.createElement所有调用React.createElementPreact.h替换。
  3. ReactDOM.render所有调用ReactDOM.renderPreact.render替换。

做完了! 阅读有关从React迁移到Preact的说明 。 实际上没有区别。


UPD 19.9.19 16.49


评论中的主题链接-Micro Frontends


UPD 20.9.19 14.30


评论中的另一个主题链接-Micro Frontends

Source: https://habr.com/ru/post/zh-CN468063/


All Articles