将 Angular 组件从许多 inputs/outputs 重构为单个配置对象
Refactoring Angular components from many inputs/outputs to a single config object
我的组件通常以具有多个 @Input
和 @Output
属性开始。当我添加属性时,切换到单个配置对象作为输入似乎更清晰。
例如,这里有一个具有多个输入和输出的组件:
export class UsingEventEmitter implements OnInit {
@Input() prop1: number;
@Output() prop1Change = new EventEmitter<number>();
@Input() prop2: number;
@Output() prop2Change = new EventEmitter<number>();
ngOnInit() {
// Simulate something that changes prop1
setTimeout(() => this.prop1Change.emit(this.prop1 + 1));
}
}
及其用法:
export class AppComponent {
prop1 = 1;
onProp1Changed = () => {
// prop1 has already been reassigned by using the [(prop1)]='prop1' syntax
}
prop2 = 2;
onProp2Changed = () => {
// prop2 has already been reassigned by using the [(prop2)]='prop2' syntax
}
}
模板:
<using-event-emitter
[(prop1)]='prop1'
(prop1Change)='onProp1Changed()'
[(prop2)]='prop2'
(prop2Change)='onProp2Changed()'>
</using-event-emitter>
随着属性数量的增加,似乎切换到单个配置对象可能会更简洁。例如,这是一个采用单个配置对象的组件:
export class UsingConfig implements OnInit {
@Input() config;
ngOnInit() {
// Simulate something that changes prop1
setTimeout(() => this.config.onProp1Changed(this.config.prop1 + 1));
}
}
及其用法:
export class AppComponent {
config = {
prop1: 1,
onProp1Changed(val: number) {
this.prop1 = val;
},
prop2: 2,
onProp2Changed(val: number) {
this.prop2 = val;
}
};
}
模板:
<using-config [config]='config'></using-config>
现在我可以通过多层嵌套组件传递配置对象引用。使用配置的组件将调用回调,如 config.onProp1Changed(...)
,这会导致配置对象重新分配新值。所以看起来我们仍然有单向数据流。此外,添加和删除属性不需要更改中间层。
将单个配置对象作为组件的输入而不是具有多个输入和输出有什么缺点吗?像这样避免 @Output
和 EventEmitter
会导致以后可能会遇到的任何问题吗?
就个人而言,如果我发现我需要超过 4 个输入+输出,我会再次检查我创建组件的方法,也许它应该不止一个组件,但我做错了什么。
无论如何,即使我需要那么多的输入和输出,我也不会在一个配置中完成,原因如下:
1- 更难知道输入和输出中应该包含什么,如下所示:
(考虑带有 html 输入元素和标签的组件)
想象一下,如果你只有这个组件的 3 个,你应该在 1 或 2 个月后回来从事这个项目,或者其他人会与你合作或使用你的代码!。
真的很难理解你的代码。
2- 表现不佳。 angular 观察单个变量比观察数组或对象便宜得多。除了考虑我在第一个给你的例子,为什么你应该强制跟踪标签,其中可能永远不会随着值而变化,而值总是在变化。
3- 更难跟踪变量和调试。 angular 本身带有难以调试的混乱错误,我为什么要让它更难。一个一个地跟踪和修复任何错误的输入或输出对我来说比在一个配置变量中做一堆数据更容易。
就我个人而言,我更喜欢将我的组件分解成尽可能小的组件并测试每个组件。然后用小的组件制造更大的组件,而不是只有一个大组件。
更新:
我使用这种方法进行一次输入并且没有更改数据(如标签)
@Component({
selector: 'icon-component',
templateUrl: './icon.component.html',
styleUrls: ['./icon.component.scss'],
inputs: ['name', 'color']
});
export class IconComponent implements OnInit {
name: any;
color: any;
ngOnInit() {
}
}
Html:
<icon-component name="fa fa-trash " color="white"></icon-component>
使用此方法 angular 不会跟踪组件内部或外部的任何更改。
但是使用 @input 方法,如果你的变量在父组件中发生变化,你也会在组件内部发生变化。
Are there any downsides to having a single config object as an input to a component, instead of having multiple input and outputs?
是的,当您想要切换到 onpush change detection strategy 时,大型项目通常需要它来缓解过多渲染周期引起的性能问题,angular 将不会检测发生的更改在 你的配置对象中。
Will avoiding @Output and EventEmitter like this cause any issues that might catch up to me later?
是的,如果你开始远离 @Output
并且在你的模板中直接操作配置对象本身,那么你会在你的视图中产生副作用,这将是困难的根源将来发现错误。您的视图永远不应该修改它注入的数据。从这个意义上讲,它应该保持 "pure" 并且仅通过事件(或其他回调)通知控制组件发生了某些事情。
更新: 又看了你post里面的例子,好像不是说要直接对输入模型进行操作但直接通过配置对象传递事件发射器。通过 @input
传递回调(这是你隐式做的)也有 ,例如:
- 你的组件变得更难理解和推理(它的输入和输出是什么?)
- 不能再使用 banana box syntax
我想说 Input
可以使用单个配置对象,但您应该始终坚持使用 Output
。 Input
定义您的组件从外部需要什么,其中一些可能是可选的。但是,Output
完全是组件的业务,应该在其中定义。如果你依赖用户传递这些函数,你要么必须检查 undefined
函数,要么你继续调用函数,就好像它们总是在配置中传递一样,如果有的话,使用你的组件可能很麻烦有太多事件需要定义,即使用户不需要它们。所以,总是在你的组件中定义你的 Output
s 并发出你需要发出的任何东西。如果用户不绑定那些事件的功能,那很好。
此外,我认为为 Input
设置单个 config
并不是最佳做法。它隐藏了真正的输入,用户可能不得不查看你的代码或文档以找出他们应该传递的内容。但是,如果你的 Input
是单独定义的,用户可以使用诸如 Language Service
此外,我认为它也可能破坏更改检测策略。
我们来看下面的例子
@Component({
selector: 'my-comp',
template: `
<div *ngIf="config.a">
{{config.b + config.c}}
</div>
`
})
export class MyComponent {
@Input() config;
}
让我们使用它
@Component({
selector: 'your-comp',
template: `
<my-comp [config]="config"></my-comp>
`
})
export class YourComponent {
config = {
a: 1, b: 2, c: 3
};
}
并且对于单独的输入
@Component({
selector: 'my-comp',
template: `
<div *ngIf="a">
{{b + c}}
</div>
`
})
export class MyComponent {
@Input() a;
@Input() b;
@Input() c;
}
让我们用这个
@Component({
selector: 'your-comp',
template: `
<my-comp
[a]="1"
[b]="2"
[c]="3">
</my-comp>
`
})
export class YourComponent {}
正如我上面所说,您必须查看 YourComponent
的代码以查看您传递的值是什么。此外,您必须在任何地方键入 config
才能使用这些 Input
秒。另一方面,您可以清楚地看到在第二个示例中传递的值更好。如果您使用语言服务
,您甚至可以获得一些智能感知
另一件事是,第二个例子更适合扩展。如果您需要添加更多 Input
,则必须一直编辑 config
,这可能会破坏您的组件。但是,在第二个示例中,添加另一个 Input
很容易,您无需修改工作代码。
最后但并非最不重要的一点是,您无法真正以自己的方式提供双向绑定。您可能知道,如果您在 Input
中调用了 data
并且在 Output
中调用了 dataChange
,您组件的使用者可以使用双向绑定糖语法和简单类型
<your-comp [(data)]="value">
当您使用
发出事件时,这将在父组件上更新 value
this.dataChange.emit(someValue)
希望这能澄清我对单身的看法 Input
编辑
我认为单个 Input
是有效的,其中还定义了一些 function
。如果你正在开发像图表组件这样的东西,通常需要复杂的 options/configs,那么使用单个 Input
实际上会更好。这是因为,该输入设置一次,永远不会改变,最好将图表选项放在一个地方。此外,用户可能会传递一些函数来帮助您绘制图例、工具提示、x 轴标签、y 轴标签等。
对于这种情况,像下面这样的输入会更好
export interface ChartConfig {
width: number;
height: number;
legend: {
position: string,
label: (x, y) => string
};
tooltip: (x, y) => string;
}
...
@Input() config: ChartConfig;
如果你想将输入参数打包成一个对象,我建议这样做:
export class UsingConfig implements OnInit {
@Input() config: any;
@Output() configChange = new EventEmitter<any>();
ngOnInit() {
// Simulate something that changes prop1
setTimeout(() =>
this.configChange.emit({
...this.config,
prop1: this.config.prop1 + 1
});
);
}
}
- 您在更改 属性 时正在创建新的配置对象。
- 您正在使用输出事件来发出更改后的配置对象。
这两点确保 ChangeDetection 将正常工作(假设您使用更高效的 OnPush 策略)。此外,在调试时更容易遵循逻辑。
编辑:
这是父组件中的明显部分。
模板:
<using-config [config]="config" (configChange)="onConfigChange($event)"></using-config>
代码:
export class AppComponent {
config = {prop1: 1};
onConfigChange(newConfig: any){
// if for some reason you need to handle specific changes
// you could check for those here, e.g.:
// if (this.config.prop1 !== newConfig.prop1){...
this.config = newConfig;
}
}
Input
除了其明显的功能之外,还有一点是让您的组件声明式且易于理解。
将所有配置放在一个巨大的对象中,它肯定会增长(相信我)是一个坏主意,出于上述所有原因以及测试。
使用简单的 input
属性 测试组件的行为比提供一个巨大的令人困惑的对象要容易得多。
你正在倒退并像 jQuery 插件过去的工作方式那样思考,你会调用一个名为 init
的函数,然后你提供一大堆您甚至不记得是否应该提供的配置,然后您将这个未知且不断增长的对象复制粘贴到您的组件中,它们可能甚至不需要它们
使用简单的 Input
s 创建默认值非常简单明了,而创建默认值的对象会变得有点混乱。
如果你有太多类似的Input
,Output
,你可以考虑以下:
1- 您可以创建一个 Base
class 并放置所有相似的 Input/Output
,然后从中扩展所有组件。
export class Base{
@Input() prop1: number;
@Output() prop1Change = new EventEmitter<number>();
@Input() prop2: number;
@Output() prop2Change = new EventEmitter<number>();
}
@Component({})
export class MyComponent extends from Base{
constructor(){super()}
}
2- 如果您不喜欢这样,您可以使用合成并创建一个可重复使用的 mixin
并像那样应用所有 Input/Output
。
下面是一个可以用来应用mixins的函数示例,NOTE不一定是你想要的,你需要根据自己的需要进行调整。
export function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}
然后创建你的混入:
export class MyMixin{
@Input() prop1: number;
@Output() prop1Change = new EventEmitter<number>();
@Input() prop2: number;
@Output() prop2Change = new EventEmitter<number>();
}
applyMixins(MyComponent, [MyMixin]);
3- 您可以为输入设置默认属性,以便仅在需要时覆盖它们:
export class MyComponent{
@Input() prop1: number = 10; // default
}
我的组件通常以具有多个 @Input
和 @Output
属性开始。当我添加属性时,切换到单个配置对象作为输入似乎更清晰。
例如,这里有一个具有多个输入和输出的组件:
export class UsingEventEmitter implements OnInit {
@Input() prop1: number;
@Output() prop1Change = new EventEmitter<number>();
@Input() prop2: number;
@Output() prop2Change = new EventEmitter<number>();
ngOnInit() {
// Simulate something that changes prop1
setTimeout(() => this.prop1Change.emit(this.prop1 + 1));
}
}
及其用法:
export class AppComponent {
prop1 = 1;
onProp1Changed = () => {
// prop1 has already been reassigned by using the [(prop1)]='prop1' syntax
}
prop2 = 2;
onProp2Changed = () => {
// prop2 has already been reassigned by using the [(prop2)]='prop2' syntax
}
}
模板:
<using-event-emitter
[(prop1)]='prop1'
(prop1Change)='onProp1Changed()'
[(prop2)]='prop2'
(prop2Change)='onProp2Changed()'>
</using-event-emitter>
随着属性数量的增加,似乎切换到单个配置对象可能会更简洁。例如,这是一个采用单个配置对象的组件:
export class UsingConfig implements OnInit {
@Input() config;
ngOnInit() {
// Simulate something that changes prop1
setTimeout(() => this.config.onProp1Changed(this.config.prop1 + 1));
}
}
及其用法:
export class AppComponent {
config = {
prop1: 1,
onProp1Changed(val: number) {
this.prop1 = val;
},
prop2: 2,
onProp2Changed(val: number) {
this.prop2 = val;
}
};
}
模板:
<using-config [config]='config'></using-config>
现在我可以通过多层嵌套组件传递配置对象引用。使用配置的组件将调用回调,如 config.onProp1Changed(...)
,这会导致配置对象重新分配新值。所以看起来我们仍然有单向数据流。此外,添加和删除属性不需要更改中间层。
将单个配置对象作为组件的输入而不是具有多个输入和输出有什么缺点吗?像这样避免 @Output
和 EventEmitter
会导致以后可能会遇到的任何问题吗?
就个人而言,如果我发现我需要超过 4 个输入+输出,我会再次检查我创建组件的方法,也许它应该不止一个组件,但我做错了什么。 无论如何,即使我需要那么多的输入和输出,我也不会在一个配置中完成,原因如下:
1- 更难知道输入和输出中应该包含什么,如下所示: (考虑带有 html 输入元素和标签的组件)
想象一下,如果你只有这个组件的 3 个,你应该在 1 或 2 个月后回来从事这个项目,或者其他人会与你合作或使用你的代码!。 真的很难理解你的代码。
2- 表现不佳。 angular 观察单个变量比观察数组或对象便宜得多。除了考虑我在第一个给你的例子,为什么你应该强制跟踪标签,其中可能永远不会随着值而变化,而值总是在变化。
3- 更难跟踪变量和调试。 angular 本身带有难以调试的混乱错误,我为什么要让它更难。一个一个地跟踪和修复任何错误的输入或输出对我来说比在一个配置变量中做一堆数据更容易。
就我个人而言,我更喜欢将我的组件分解成尽可能小的组件并测试每个组件。然后用小的组件制造更大的组件,而不是只有一个大组件。
更新: 我使用这种方法进行一次输入并且没有更改数据(如标签)
@Component({
selector: 'icon-component',
templateUrl: './icon.component.html',
styleUrls: ['./icon.component.scss'],
inputs: ['name', 'color']
});
export class IconComponent implements OnInit {
name: any;
color: any;
ngOnInit() {
}
}
Html:
<icon-component name="fa fa-trash " color="white"></icon-component>
使用此方法 angular 不会跟踪组件内部或外部的任何更改。 但是使用 @input 方法,如果你的变量在父组件中发生变化,你也会在组件内部发生变化。
Are there any downsides to having a single config object as an input to a component, instead of having multiple input and outputs?
是的,当您想要切换到 onpush change detection strategy 时,大型项目通常需要它来缓解过多渲染周期引起的性能问题,angular 将不会检测发生的更改在 你的配置对象中。
Will avoiding @Output and EventEmitter like this cause any issues that might catch up to me later?
是的,如果你开始远离 @Output
并且在你的模板中直接操作配置对象本身,那么你会在你的视图中产生副作用,这将是困难的根源将来发现错误。您的视图永远不应该修改它注入的数据。从这个意义上讲,它应该保持 "pure" 并且仅通过事件(或其他回调)通知控制组件发生了某些事情。
更新: 又看了你post里面的例子,好像不是说要直接对输入模型进行操作但直接通过配置对象传递事件发射器。通过 @input
传递回调(这是你隐式做的)也有
- 你的组件变得更难理解和推理(它的输入和输出是什么?)
- 不能再使用 banana box syntax
我想说 Input
可以使用单个配置对象,但您应该始终坚持使用 Output
。 Input
定义您的组件从外部需要什么,其中一些可能是可选的。但是,Output
完全是组件的业务,应该在其中定义。如果你依赖用户传递这些函数,你要么必须检查 undefined
函数,要么你继续调用函数,就好像它们总是在配置中传递一样,如果有的话,使用你的组件可能很麻烦有太多事件需要定义,即使用户不需要它们。所以,总是在你的组件中定义你的 Output
s 并发出你需要发出的任何东西。如果用户不绑定那些事件的功能,那很好。
此外,我认为为 Input
设置单个 config
并不是最佳做法。它隐藏了真正的输入,用户可能不得不查看你的代码或文档以找出他们应该传递的内容。但是,如果你的 Input
是单独定义的,用户可以使用诸如 Language Service
此外,我认为它也可能破坏更改检测策略。
我们来看下面的例子
@Component({
selector: 'my-comp',
template: `
<div *ngIf="config.a">
{{config.b + config.c}}
</div>
`
})
export class MyComponent {
@Input() config;
}
让我们使用它
@Component({
selector: 'your-comp',
template: `
<my-comp [config]="config"></my-comp>
`
})
export class YourComponent {
config = {
a: 1, b: 2, c: 3
};
}
并且对于单独的输入
@Component({
selector: 'my-comp',
template: `
<div *ngIf="a">
{{b + c}}
</div>
`
})
export class MyComponent {
@Input() a;
@Input() b;
@Input() c;
}
让我们用这个
@Component({
selector: 'your-comp',
template: `
<my-comp
[a]="1"
[b]="2"
[c]="3">
</my-comp>
`
})
export class YourComponent {}
正如我上面所说,您必须查看 YourComponent
的代码以查看您传递的值是什么。此外,您必须在任何地方键入 config
才能使用这些 Input
秒。另一方面,您可以清楚地看到在第二个示例中传递的值更好。如果您使用语言服务
另一件事是,第二个例子更适合扩展。如果您需要添加更多 Input
,则必须一直编辑 config
,这可能会破坏您的组件。但是,在第二个示例中,添加另一个 Input
很容易,您无需修改工作代码。
最后但并非最不重要的一点是,您无法真正以自己的方式提供双向绑定。您可能知道,如果您在 Input
中调用了 data
并且在 Output
中调用了 dataChange
,您组件的使用者可以使用双向绑定糖语法和简单类型
<your-comp [(data)]="value">
当您使用
发出事件时,这将在父组件上更新value
this.dataChange.emit(someValue)
希望这能澄清我对单身的看法 Input
编辑
我认为单个 Input
是有效的,其中还定义了一些 function
。如果你正在开发像图表组件这样的东西,通常需要复杂的 options/configs,那么使用单个 Input
实际上会更好。这是因为,该输入设置一次,永远不会改变,最好将图表选项放在一个地方。此外,用户可能会传递一些函数来帮助您绘制图例、工具提示、x 轴标签、y 轴标签等。
对于这种情况,像下面这样的输入会更好
export interface ChartConfig {
width: number;
height: number;
legend: {
position: string,
label: (x, y) => string
};
tooltip: (x, y) => string;
}
...
@Input() config: ChartConfig;
如果你想将输入参数打包成一个对象,我建议这样做:
export class UsingConfig implements OnInit {
@Input() config: any;
@Output() configChange = new EventEmitter<any>();
ngOnInit() {
// Simulate something that changes prop1
setTimeout(() =>
this.configChange.emit({
...this.config,
prop1: this.config.prop1 + 1
});
);
}
}
- 您在更改 属性 时正在创建新的配置对象。
- 您正在使用输出事件来发出更改后的配置对象。
这两点确保 ChangeDetection 将正常工作(假设您使用更高效的 OnPush 策略)。此外,在调试时更容易遵循逻辑。
编辑: 这是父组件中的明显部分。
模板:
<using-config [config]="config" (configChange)="onConfigChange($event)"></using-config>
代码:
export class AppComponent {
config = {prop1: 1};
onConfigChange(newConfig: any){
// if for some reason you need to handle specific changes
// you could check for those here, e.g.:
// if (this.config.prop1 !== newConfig.prop1){...
this.config = newConfig;
}
}
Input
除了其明显的功能之外,还有一点是让您的组件声明式且易于理解。将所有配置放在一个巨大的对象中,它肯定会增长(相信我)是一个坏主意,出于上述所有原因以及测试。
使用简单的
input
属性 测试组件的行为比提供一个巨大的令人困惑的对象要容易得多。你正在倒退并像 jQuery 插件过去的工作方式那样思考,你会调用一个名为
init
的函数,然后你提供一大堆您甚至不记得是否应该提供的配置,然后您将这个未知且不断增长的对象复制粘贴到您的组件中,它们可能甚至不需要它们使用简单的
Input
s 创建默认值非常简单明了,而创建默认值的对象会变得有点混乱。
如果你有太多类似的Input
,Output
,你可以考虑以下:
1- 您可以创建一个 Base
class 并放置所有相似的 Input/Output
,然后从中扩展所有组件。
export class Base{
@Input() prop1: number;
@Output() prop1Change = new EventEmitter<number>();
@Input() prop2: number;
@Output() prop2Change = new EventEmitter<number>();
}
@Component({})
export class MyComponent extends from Base{
constructor(){super()}
}
2- 如果您不喜欢这样,您可以使用合成并创建一个可重复使用的 mixin
并像那样应用所有 Input/Output
。
下面是一个可以用来应用mixins的函数示例,NOTE不一定是你想要的,你需要根据自己的需要进行调整。
export function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}
然后创建你的混入:
export class MyMixin{
@Input() prop1: number;
@Output() prop1Change = new EventEmitter<number>();
@Input() prop2: number;
@Output() prop2Change = new EventEmitter<number>();
}
applyMixins(MyComponent, [MyMixin]);
3- 您可以为输入设置默认属性,以便仅在需要时覆盖它们:
export class MyComponent{
@Input() prop1: number = 10; // default
}