Angular 2 Component Inheritance

Angular 2.3 版本中引入了组件继承的功能,该功能很是强大,可以大大增长咱们组件的可复用性。javascript

Component Inheritance

组件继承涉及如下的内容:html

  • Metadata:如 @Input()@Output()@ContentChild/Children@ViewChild/Children 等。在派生类中定义的元数据将覆盖继承链中的任何先前的元数据,不然将使用基类元数据。java

  • Constructor:若是派生类未声明构造函数,它将使用基类的构造函数。这意味着在基类构造函数注入的全部服务,子组件都能访问到。git

  • Lifecycle hooks:若是基类中包含生命周期钩子,如 ngOnInit、ngOnChanges 等。尽管在派生类没有定义相应的生命周期钩子,基类的生命周期钩子会被自动调用。程序员

须要注意的是,模板是不能被继承的 ,所以共享的 DOM 结构或行为须要单独处理。了解详细信息,请查看 - properly support inheritancegithub

接下来咱们来快速体验的组件继承的功能并验证以上的结论,具体示例以下(本文全部示例基于的 Angular 版本是 - 4.0.1):typescript

exe-base.component.ts编程

import { Component, ElementRef, Input, HostBinding, HostListener, OnInit } from '@angular/core';

@Component({
    selector: 'exe-base',
    // template will not be inherited 
    template: `
    <div>
       exe-base:我是base组件么? - {{isBase}}
    </div>
  `
})
export class BaseComponent implements OnInit {
    @Input() isBase: boolean = true;

    @HostBinding('style.color') color = 'blue'; // will be inherited 

    @HostListener('click', ['$event']) // will be inherited 
    onClick(event: Event) {
        console.log(`I am BaseComponent`);
    }

    constructor(protected eleRef: ElementRef) { }

    ngOnInit() {
        console.dir('BaseComponent:ngOnInit method has been called');
    }
}

exe-inherited.component.ts浏览器

import { Component, HostListener, OnChanges, SimpleChanges } from '@angular/core';
import { BaseComponent } from './exe-base.component';

@Component({
    selector: 'exe-inherited',
    template: `
    <div>
      exe-inherited:我是base组件么? - {{isBase}}
    </div>
  `
})
export class InheritedComponent extends BaseComponent
    implements OnChanges {

    @HostListener('click', ['$event']) // overridden
    onClick(event: Event) {
        console.log(`I am InheritedComponent`);
    }

    ngOnChanges(changes: SimpleChanges) {
        console.dir(this.eleRef); // this.eleRef.nativeElement:exe-inherited
    }
}

app.component.ts网络

import { Component, OnInit } from '@angular/core';
import {ManagerService} from "./manager.service";

@Component({
  selector: 'exe-app',
  template: `
    <exe-base></exe-base>
    <hr/>
    <exe-inherited [isBase]="false"></exe-inherited>
  `
})
export class AppComponent {
  currentPage: number = 1;
  totalPage: number = 5;
}

图片描述

(备注:BaseComponent 中 ngOnInit() 钩子被调用了两次哦)

接下来咱们简要讨论一个可能使人困惑的主题,@Component() 中元数据是否容许继承?答案是否认的,子组件是不能继承父组件装饰器中元数据。限制元数据继承,从根本上说,是有道理的,由于咱们的元数据用是来描述组件类的,不一样的组件咱们是须要不一样的元数据,如 selectortemplate 等。Angular 2 组件继承主要仍是逻辑层的复用,具体能够先阅读完下面实战的部分,再好好体会一下哈。

Component Inheritance In Action

如今咱们先来实现一个简单的分页组件,预期的效果以下:

图片描述

(图片来源 - https://scotch.io/tutorials/c...

具体实现代码以下:

simple-pagination.component.ts

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
    selector: 'simple-pagination',
    template: `
       <button (click)="previousPage()" [disabled]="!hasPrevious()">Previous</button> 
       <button (click)="nextPage()" [disabled]="!hasNext()">Next</button>
       <p>page {{ page }} of {{ pageCount }} </p>
    `
})

export class SimplePaginationComponent {
    @Input() pageCount: number;

    @Input() page: number;

    @Output() pageChanged = new EventEmitter<number>();

    nextPage() {
        this.pageChanged.emit(++this.page);
    }

    previousPage() {
        this.pageChanged.emit(--this.page);
    }

    hasPrevious(): boolean {
        return this.page > 1;
    }

    hasNext(): boolean {
        return this.page < this.pageCount;
    }
}

app.component.ts

import { Component, OnInit } from '@angular/core';
import {ManagerService} from "./manager.service";

@Component({
  selector: 'exe-app',
  template: `
   <simple-pagination [page]="currentPage" [pageCount]="totalPage"></simple-pagination>
  `
})
export class AppComponent {
  currentPage: number = 2;
  totalPage: number = 10;
}

假设咱们如今想更换分页组件的风格,以下图所示:

图片描述

(图片来源 - https://scotch.io/tutorials/c...

咱们发现 UI 界面风格已经彻底不同了,但仔细想一下组件分页的控制逻辑仍能够继续使用。Angular 团队也考虑到了这种场景,所以为咱们引入组件继承的特性,这对咱们开发者来讲,能够大大地提升组件的复用性。接下来咱们来一步步实现新的分页组件,首先先更新 UI 界面,具体代码以下:

exe-pagination.component.ts

import { Component } from '@angular/core';
import { SimplePaginationComponent } from './simple-pagination.component';

@Component({
    selector: 'exe-pagination',
    template: `
    <a (click)="previousPage()" [class.disabled]="!hasPrevious()" 
      href="javascript:void(0)">
      ««
    </a> 
    <span>{{ page }} / {{ pageCount }}</span>
    <a (click)="nextPage()" [class.disabled]="!hasNext()"
      href="javascript:void(0)" >
      »»
    </a>
  `
})
export class ExePaginationComponent extends SimplePaginationComponent {
    
}

上面代码中,有几个注意点:

  • 首先咱们先导入已开发完的 SimplePaginationComponent 组件类

  • 而后让咱们新定义的 ExePaginationComponent 类继承于 SimplePaginationComponent 类

  • 接着咱们更新页面的视图模板,把按钮替换为 <<>>

  • 咱们看到更新的视图模板,咱们仍然可使用基类 (SimplePaginationComponent) 中定义的全部输入、输出属性

再继续开发 ExePaginationComponent 组件前,咱们先来更新一下 SimplePaginationComponent 组件:

@Component({
  selector: 'simple-pagination',
  template: `
    <button (click)="previousPage()" [disabled]="!hasPrevious()">{{ previousText }}</button> 
    <button (click)="nextPage()" [disabled]="!hasNext()">{{ nextText }}</button>

    <p>page {{ page }} of {{ pageCount }}</p>
  `
})
export class SimplePaginationComponent {
  ...

  @Input()
  previousText = 'Previous';

  @Input()
  nextText = 'Next';

  ...

}

注意:

  • 当用户没有设置 previousText 输入属性值时,咱们使用的默认值是 'Previous'

  • 当用户没有设置 nextText 输入属性值时,咱们使用的默认值是 'Next'

对于 ExePaginationComponent 组件,咱们也但愿让用户自定义 previousText 和 nextText 的值,但它们对应的默认值是:'<<' 和 '>>',这时咱们能够覆盖 SimplePaginationComponent 组件的输入属性,具体示例以下:

import { Component , Input, Output} from '@angular/core';
import { SimplePaginationComponent } from './simple-pagination.component';

@Component({
    selector: 'exe-pagination',
    template: `
    <a (click)="previousPage()" [class.disabled]="!hasPrevious()" 
      href="javascript:void(0)">
      ««
    </a> 
    <span>{{ page }} / {{ pageCount }}</span>
    <a (click)="nextPage()" [class.disabled]="!hasNext()"
      href="javascript:void(0)" >
      »»
    </a>
  `
})
export class ExePaginationComponent extends SimplePaginationComponent {
    @Input() previousText = '<<'; // override default text
    @Input() nextText = '>>'; // override default text
}

以上代码成功运行后,浏览器的输出结果以下:

图片描述

功能已经实现了,但有时候咱们想在分页中显示一个标题,且支持用户自定义该标题,那就得在现有组件的基础上,再新增一个 title 输入属性,调整后的代码以下:

import { Component , Input, Output} from '@angular/core';
import { SimplePaginationComponent } from './simple-pagination.component';

@Component({
    selector: 'exe-pagination',
    template: `
     <h2>{{ title }}</h2>
    <a (click)="previousPage()" [class.disabled]="!hasPrevious()" 
      href="javascript:void(0)">
      ««
    </a> 
    <span>{{ page }} / {{ pageCount }}</span>
    <a (click)="nextPage()" [class.disabled]="!hasNext()"
      href="javascript:void(0)" >
      »»
    </a>
  `
})
export class ExePaginationComponent extends SimplePaginationComponent {
    @Input() previousText = '<<'; // override default text
    @Input() nextText = '>>'; // override default text
    @Input() title: string; // title input for child component only
}

我有话说

1.面向对象编程中类的概念?

传统的 JavaScript 程序使用函数和基于原型的继承来建立可重用的组件,但对于熟悉使用面向对象方式的程序员来说就有些棘手,由于他们用的是基于类的继承而且对象是由类构建出来的。 从 ECMAScript 2015,也就是ECMAScript 6 开始,JavaScript 程序员将可以使用基于类的面向对象的方式。 使用 TypeScript,咱们容许开发者如今就使用这些特性,而且编译后的 JavaScript 能够在全部主流浏览器和平台上运行,而不须要等到下个JavaScript 版本。

类的概念

虽然 JavaScript 中有类的概念,可是可能大多数 JavaScript 程序员并非很是熟悉类,这里对类相关的概念作一个简单的介绍。

  • 类 (Class):一种面向对象计算机编程语言的构造,是建立对象的蓝图,描述了所建立的对象共同的属性和方法。

  • 对象 (Object):类的实例,经过 new 建立

  • 面向对象 (OOP) 的三大特性:封装、继承、多态

  • 封装 (Encapsulation):将对数据的操做细节隐藏起来,只暴露对外的接口。外界调用端不须要知道细节,就能经过对外提供的接口来访问该对象,同时也保证了外界没法任意更改对象内部的数据

  • 继承 (Inheritance):子类继承父类,子类除了拥有父类的全部特性外,还能够扩展自有的功能特性

  • 多态 (Polymorphism):由继承而产生了相关的不一样的类,对同一个方法能够有不一样的响应。好比 CatDog 都继承自 Animal,可是分别实现了本身的 eat() 方法。此时针对某一个实例,咱们无需了解它是 Cat 仍是 Dog,就能够直接调用 eat() 方法,程序会自动判断出来应该如何执行 eat()

  • 存取器(getter & setter):用于属性的读取和赋值

  • 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。好比 public 表示公有属性或方法

  • 抽象类(Abstract Class):抽象类是供其余类继承的基类,抽象类不容许被实例化。抽象类中的抽象方法必须在子类中被实现

  • 接口(Interfaces):不一样类之间公有的属性或方法,能够抽象成一个接口。接口能够被类实现(implements)。一个类只能继承自另外一个类,可是能够实现多个接口。

TypeScript 类示例

class Greeter {
    private greeting: string; // 定义私有属性,访问修饰符:public、protected、private
    constructor(message: string) { // 构造函数,通常执行用于进行数据初始化操做
        this.greeting = message;
    }
    greet() { // 定义方法
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

2.面向对象编程中继承的概念是什么?

继承(英语:inheritance)是面向对象软件技术当中的一个概念。若是一个类别A “继承自” 另外一个类别B,就把这个A称为 “B的子类别”,而把B称为“A的父类别”也能够称 “B是A的超类”。继承可使得子类别具备父类别的各类属性和方法,而不须要再次编写相同的代码。在令子类别继承父类别的同时,能够从新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其得到与父类别不一样的功能。另外,为子类别追加新的属性和方法也是常见的作法。 通常静态的面向对象编程语言,继承属于静态的,意即在子类别的行为在编译期就已经决定,没法在执行期扩充。 —— 维基百科

继承 (Inheritance) 是一种联结类与类的层次模型。指的是一个类 (称为子类、子接口) 继承另外的一个类 (称为父类、父接口) 的功能,并能够增长它本身的新功能的能力,继承是类与类或者接口与接口之间最多见的关系;继承是一种 is-a 关系。

图片描述

(图片来源网络)

TypeScript 类继承示例

class Animal {
    name:string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

let sam = new Snake("Sammy the Python");
sam.move();

参考资源