如何扩展/继承组件?

我想为一些已经部署在Angular 2中的组件创建扩展,而不需要几乎完全重写它们,因为基本组件可以发生变化,并且希望这些变化也能反映在它的派生组件中。

我创建了这个简单的例子,试图更好地解释我的问题:

使用以下基本组件app/base-panel.component.ts:

import {Component, Input} from 'angular2/core';


@Component({
selector: 'base-panel',
template: '<div class="panel" [style.background-color]="color" (click)="onClick($event)">{{content}}</div>',
styles: [`
.panel{
padding: 50px;
}
`]
})
export class BasePanelComponent {


@Input() content: string;


color: string = "red";


onClick(event){
console.log("Click color: " + this.color);
}
}

你是否想创建另一个衍生组件,只改变,例如,在示例颜色,app/my-panel.component.ts的情况下,基本组件的行为:

import {Component} from 'angular2/core';
import {BasePanelComponent} from './base-panel.component'


@Component({
selector: 'my-panel',
template: '<div class="panel" [style.background-color]="color" (click)="onClick($event)">{{content}}</div>',
styles: [`
.panel{
padding: 50px;
}
`]
})
export class MyPanelComponent extends BasePanelComponent{


constructor() {
super();
this.color = "blue";
}
}

< a href = " http://plnkr.co/edit/bWa1JmH7NaSaJffLsl0x?p=preview" rel="noreferrer">在Plunker中完整的工作示例

注意:显然这个例子很简单,不需要使用继承就可以解决,但它只是为了说明真正的问题。

正如你在派生组件app/my-panel.component.ts的实现中所看到的,大部分实现都是重复的,并且真正继承的单个部分是class BasePanelComponent,但是@Component基本上必须完全重复,而不仅仅是selector: 'my-panel'所更改的部分。

是否有某种方法可以完全继承angular组件,继承标记/注释的class定义,例如@Component?

编辑1 -功能请求

特性请求angular2添加到GitHub上的项目:扩展/继承angar2组件注释#7968

编辑2 -关闭请求

请求被关闭,出于这个原因,表示暂时不知道如何合并将做出的装饰器。让我们别无选择。所以我的观点是引用于本期

205793 次浏览

据我所知,在Angular 2中还没有实现组件继承,我不确定他们是否计划这样做,不过既然Angular 2使用了typescript(如果你决定这么做的话),你可以通过class MyClass extends OtherClass { ... }来使用类继承。对于组件继承,我建议加入Angular 2项目,去https://github.com/angular/angular/issues并提交一个特性请求!

更新

2.3.0-rc.0开始支持组件继承

原始

到目前为止,对我来说最方便的就是保存模板&样式放入单独的*html &*.css文件并通过templateUrlstyleUrls指定这些文件,因此易于重用。

@Component {
selector: 'my-panel',
templateUrl: 'app/components/panel.html',
styleUrls: ['app/components/panel.css']
}
export class MyPanelComponent extends BasePanelComponent

可选择的解决方案:

Thierry Templier的回答是解决这个问题的另一种方法

在与Thierry Templier讨论了一些问题后,我给出了下面的工作示例,它满足了我的期望,可以作为这个问题中提到的继承限制的替代方案:

1 -创建自定义装饰:

export function CustomComponent(annotation: any) {
return function (target: Function) {
var parentTarget = Object.getPrototypeOf(target.prototype).constructor;
var parentAnnotations = Reflect.getMetadata('annotations', parentTarget);


var parentAnnotation = parentAnnotations[0];
Object.keys(parentAnnotation).forEach(key => {
if (isPresent(parentAnnotation[key])) {
// verify is annotation typeof function
if(typeof annotation[key] === 'function'){
annotation[key] = annotation[key].call(this, parentAnnotation[key]);
}else if(
// force override in annotation base
!isPresent(annotation[key])
){
annotation[key] = parentAnnotation[key];
}
}
});


var metadata = new Component(annotation);


Reflect.defineMetadata('annotations', [ metadata ], target);
}
}

2 -使用@Component装饰器的基础组件:

@Component({
// create seletor base for test override property
selector: 'master',
template: `
<div>Test</div>
`
})
export class AbstractComponent {


}

3 -使用@CustomComponent装饰器的子组件

@CustomComponent({
// override property annotation
//selector: 'sub',
selector: (parentSelector) => { return parentSelector + 'sub'}
})
export class SubComponent extends AbstractComponent {
constructor() {
}
}

< a href = " https://plnkr.co/edit/TPps03QCGQCWbX6oVKXp? . p=preview" rel="noreferrer">Plunkr的完整示例

Angular 2 2.3版本刚刚发布,它包含了原生组件继承。看起来你可以继承和重写任何你想要的东西,除了模板和样式。参考:

如果有人正在寻找一个更新的解决方案,费尔南多的答案几乎是完美的。除了ComponentMetadata已弃用。相反,使用Component对我有用。

完整的自定义装饰器CustomDecorator.ts文件如下所示:

import 'zone.js';
import 'reflect-metadata';
import { Component } from '@angular/core';
import { isPresent } from "@angular/platform-browser/src/facade/lang";


export function CustomComponent(annotation: any) {
return function (target: Function) {
var parentTarget = Object.getPrototypeOf(target.prototype).constructor;
var parentAnnotations = Reflect.getMetadata('annotations', parentTarget);


var parentAnnotation = parentAnnotations[0];
Object.keys(parentAnnotation).forEach(key => {
if (isPresent(parentAnnotation[key])) {
// verify is annotation typeof function
if(typeof annotation[key] === 'function'){
annotation[key] = annotation[key].call(this, parentAnnotation[key]);
}else if(
// force override in annotation base
!isPresent(annotation[key])
){
annotation[key] = parentAnnotation[key];
}
}
});


var metadata = new Component(annotation);


Reflect.defineMetadata('annotations', [ metadata ], target);
}
}

然后将它导入到你的新组件sub-component.component.ts文件中,并像这样使用@CustomComponent而不是@Component:

import { CustomComponent } from './CustomDecorator';
import { AbstractComponent } from 'path/to/file';


...


@CustomComponent({
selector: 'subcomponent'
})
export class SubComponent extends AbstractComponent {


constructor() {
super();
}


// Add new logic here!
}

既然打字稿2.2支持通过类表达式混合,我们就有了更好的方式在组件上表达mixin。请注意,从angular 2.3 (讨论)开始,你也可以使用组件继承或自定义装饰器,就像在这里的其他回答中讨论的那样。然而,我认为mixin有一些属性,使它们更适合跨组件重用行为:

  • Mixins的组合更加灵活,也就是说,你可以在现有组件上混合和匹配Mixins,或者组合Mixins来形成新的组件
  • Mixin组合仍然很容易理解,这要归功于它对类继承层次结构的明显线性化
  • 你可以更容易地避免装饰器和注释的问题,这些问题会困扰组件继承(讨论)

我强烈建议你阅读上面的TypeScript 2.2公告,以了解Mixins是如何工作的。angular GitHub问题中的链接讨论提供了更多细节。

你需要这些类型:

export type Constructor<T> = new (...args: any[]) => T;


export class MixinRoot {
}

然后你可以声明一个类似Destroyable Mixin的Mixin,它帮助组件跟踪需要在ngOnDestroy中释放的订阅:

export function Destroyable<T extends Constructor<{}>>(Base: T) {
return class Mixin extends Base implements OnDestroy {
private readonly subscriptions: Subscription[] = [];


protected registerSubscription(sub: Subscription) {
this.subscriptions.push(sub);
}


public ngOnDestroy() {
this.subscriptions.forEach(x => x.unsubscribe());
this.subscriptions.length = 0; // release memory
}
};
}

要将Destroyable混合到Component中,你可以像这样声明你的组件:

export class DashboardComponent extends Destroyable(MixinRoot)
implements OnInit, OnDestroy { ... }

注意,只有当你想extend一个Mixin组合时,MixinRoot才有必要。你可以很容易地扩展多个mixin,例如A extends B(C(D))。这是我上面谈到的mixin的明显线性化,例如,你有效地组合了一个inheritnace层次A -> B -> C -> D

在其他情况下,例如当你想在一个现有的类上组合Mixin时,你可以像这样应用Mixin:

const MyClassWithMixin = MyMixin(MyClass);

然而,我发现第一种方法最适合ComponentsDirectives,因为这些也需要用@Component@Directive来装饰。

只需使用继承,在子类中扩展父类,并声明带有父类参数的构造函数,该参数使用在super()中。

  1. 父类:
@Component({
selector: 'teams-players-box',
templateUrl: '/maxweb/app/app/teams-players-box.component.html'
})
export class TeamsPlayersBoxComponent {
public _userProfile: UserProfile;
public _user_img: any;
public _box_class: string = "about-team teams-blockbox";
public fullname: string;
public _index: any;
public _isView: string;
indexnumber: number;


constructor(
public _userProfilesSvc: UserProfiles,
public _router: Router,
){}
  1. 子类:
@Component({
selector: '[teams-players-eligibility]',
templateUrl: '/maxweb/app/app/teams-players-eligibility.component.html'
})
export class TeamsPlayersEligibilityComponent extends TeamsPlayersBoxComponent {
constructor (public _userProfilesSvc: UserProfiles,
public _router: Router) {
super(_userProfilesSvc,_router);
}
}

如果你通读CDK库和材质库,你会发现它们都在使用继承,但对组件本身使用的不多,在我看来,内容投影才是王道。请看这个链接https://blog.angular-university.io/angular-ng-content/,上面写着“这个设计的关键问题”;

我知道这不能回答你的问题,但是我真的认为应该避免继承/扩展组件。我的理由是:

如果由两个或多个组件扩展的抽象类包含共享逻辑: 使用一个服务,甚至创建一个新的typescript类,可以在两个组件之间共享 如果抽象类…包含共享变量或onClicketc函数, 然后,两个扩展组件视图的html之间将出现重复。这是不好的做法。共享的html需要被分解成组件。这些组件(部件)可以在两个组件之间共享。

我是否遗漏了为组件创建抽象类的其他原因?

我最近看到的一个例子是组件扩展AutoUnsubscribe:

import { Subscription } from 'rxjs';
import { OnDestroy } from '@angular/core';
export abstract class AutoUnsubscribeComponent implements OnDestroy {
protected infiniteSubscriptions: Array<Subscription>;


constructor() {
this.infiniteSubscriptions = [];
}


ngOnDestroy() {
this.infiniteSubscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
}
}

这是因为在一个大型代码库中,infiniteSubscriptions.push()只使用了10次。同时进口&扩展AutoUnsubscribe实际上比在组件本身的ngOnDestroy()方法中添加mySubscription.unsubscribe()需要更多的代码,后者无论如何都需要额外的逻辑。

组件可以像typescript类继承一样扩展,只是您必须用一个新名称覆盖选择器。所有来自父组件的Input()和Output()属性正常工作

更新

@Component是一个装饰器,

装饰符是在类声明期间应用的,而不是在对象上。

基本上,装饰器向类对象添加一些不能通过继承访问的元数据。

如果你想实现装饰器继承,我建议写一个自定义装饰器。类似下面的例子。

export function CustomComponent(annotation: any) {
return function (target: Function) {
var parentTarget = Object.getPrototypeOf(target.prototype).constructor;


var parentAnnotations = Reflect.getMetadata('annotations', parentTarget);
var parentParamTypes = Reflect.getMetadata('design:paramtypes', parentTarget);
var parentPropMetadata = Reflect.getMetadata('propMetadata', parentTarget);
var parentParameters = Reflect.getMetadata('parameters', parentTarget);


var parentAnnotation = parentAnnotations[0];


Object.keys(parentAnnotation).forEach(key => {
if (isPresent(parentAnnotation[key])) {
if (!isPresent(annotation[key])) {
annotation[key] = parentAnnotation[key];
}
}
});
// Same for the other metadata
var metadata = new ComponentMetadata(annotation);


Reflect.defineMetadata('annotations', [ metadata ], target);
};
};
< p >参考: https://medium.com/@ttemplier/angular2-decorators-and-class-inheritance-905921dbd1b7 < / p >

让我们来了解一些关键的限制&Angular组件继承系统的特性。

组件只继承类逻辑:

  • @Component装饰器中的所有元数据都不会被继承。
  • 组件@Input属性和@Output属性被继承。
  • 组件生命周期不继承。

记住这些特性是非常重要的,所以让我们单独研究每一个特性。

组件只继承类逻辑

当你继承一个组件时,里面的所有逻辑都是平等继承的。值得注意的是,只有public成员被继承,而private成员只能在实现它们的类中访问。

@Component装饰器中的所有元数据都不会被继承

没有元数据被继承的事实乍一看似乎是违反直觉的,但如果你仔细想想,它实际上是完全有道理的。如果你从一个组件继承,比如(componentA),你不会想要从componentA继承的选择器替换被继承的类ComponentB的选择器。对于template/templateUrl以及style/styleUrls也是如此。

组件@Input和@Output属性被继承

这是我非常喜欢Angular中组件继承的另一个特性。简单地说,只要有自定义的@Input和@Output属性,这些属性就会被继承。

组件生命周期不继承

这一部分不是很明显,特别是对那些没有广泛使用过面向对象原则的人来说。例如,假设你有ComponentA,它实现了Angular的许多生命周期钩子中的一个,比如OnInit。如果你创建了ComponentB并继承了ComponentA,那么ComponentA的OnInit生命周期将不会触发,直到你显式地调用它,即使你确实为ComponentB拥有这个OnInit生命周期。

调用超级/基本组件方法

为了从ComponentA中获得ngOnInit()方法,我们需要使用super关键字,然后调用我们需要的方法,在本例中是ngOnInit。super关键字指的是要继承的组件的实例,在本例中是ComponentA。

你可以继承@Input, @Output, @ViewChild等等。看这个例子:

@Component({
template: ''
})
export class BaseComponent {
@Input() someInput: any = 'something';


@Output() someOutput: EventEmitter<void> = new EventEmitter<void>();


}


@Component({
selector: 'app-derived',
template: '<div (click)="someOutput.emit()">\{\{someInput}}</div>',
providers: [
{ provide: BaseComponent, useExisting: DerivedComponent }
]
})
export class DerivedComponent {


}