Angular 2.3 版本中引入了组件继承的功能,该功能非常强大,能够大大增加我们组件的可复用性。
Component Inheritance
组件继承涉及以下的内容:
Metadata:如
@Input()
、@Output()
、@ContentChild/Children
、@ViewChild/Children
等。在派生类中定义的元数据将覆盖继承链中的任何先前的元数据,否则将使用基类元数据。Constructor:如果派生类未声明构造函数,它将使用基类的构造函数。这意味着在基类构造函数注入的所有服务,子组件都能访问到。
Lifecycle hooks:如果基类中包含生命周期钩子,如 ngOnInit、ngOnChanges 等。尽管在派生类没有定义相应的生命周期钩子,基类的生命周期钩子会被自动调用。
需要注意的是,模板是不能被继承的 ,因此共享的 DOM 结构或行为需要单独处理。了解详细信息,请查看 - properly support inheritance。
接下来我们来快速体验的组件继承的功能并验证以上的结论,具体示例如下(本文所有示例基于的 Angular 版本是 - 4.0.1):
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,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()
中元数据是否允许继承?答案是否定的,子组件是不能继承父组件装饰器中元数据。限制元数据继承,从根本上说,是有道理的,因为我们的元数据用是来描述组件类的,不同的组件我们是需要不同的元数据,如 selector
、template
等。Angular 2 组件继承主要还是逻辑层的复用,具体可以先阅读完下面实战的部分,再好好体会一下哈。
Component Inheritance In Action
(图片来源 - https://scotch.io/tutorials/c...
具体实现代码如下:
simple-pagination.component.ts
import { Component,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,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'; ... }
注意:
对于 ExePaginationComponent
组件,我们也希望让用户自定义 prevIoUsText 和 nextText 的值,但它们对应的默认值是:'<<' 和 '>>',这时我们可以覆盖 SimplePaginationComponent 组件的输入属性,具体示例如下:
import { Component,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,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 程序员并不是非常熟悉类,这里对类相关的概念做一个简单的介绍。
对象 (Object):类的实例,通过
new
创建面向对象 (OOP) 的三大特性:封装、继承、多态
封装 (Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
多态 (Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如
Cat
和Dog
都继承自Animal
,但是分别实现了自己的eat()
方法。此时针对某一个实例,我们无需了解它是Cat
还是Dog
,就可以直接调用eat()
方法,程序会自动判断出来应该如何执行eat()
存取器(getter & setter):用于属性的读取和赋值
抽象类(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();