Angular 2.3 版本中引入了組件繼承的功能,該功能很是強大,可以大大增長咱們組件的可複用性。javascript
組件繼承涉及如下的內容:html
Metadata:如 @Input()
、@Output()
、@ContentChild/Children
、@ViewChild/Children
等。在派生類中定義的元數據將覆蓋繼承鏈中的任何先前的元數據,不然將使用基類元數據。java
Constructor:若是派生類未聲明構造函數,它將使用基類的構造函數。這意味着在基類構造函數注入的全部服務,子組件都能訪問到。git
Lifecycle hooks:若是基類中包含生命週期鉤子,如 ngOnInit、ngOnChanges 等。儘管在派生類沒有定義相應的生命週期鉤子,基類的生命週期鉤子會被自動調用。程序員
須要注意的是,模板是不能被繼承的 ,所以共享的 DOM 結構或行爲須要單獨處理。瞭解詳細信息,請查看 - properly support inheritance。github
接下來咱們來快速體驗的組件繼承的功能並驗證以上的結論,具體示例以下(本文全部示例基於的 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()
中元數據是否容許繼承?答案是否認的,子組件是不能繼承父組件裝飾器中元數據。限制元數據繼承,從根本上說,是有道理的,由於咱們的元數據用是來描述組件類的,不一樣的組件咱們是須要不一樣的元數據,如 selector
、template
等。Angular 2 組件繼承主要仍是邏輯層的複用,具體能夠先閱讀完下面實戰的部分,再好好體會一下哈。
如今咱們先來實現一個簡單的分頁組件,預期的效果以下:
(圖片來源 - 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 }
傳統的 JavaScript 程序使用函數和基於原型的繼承來建立可重用的組件,但對於熟悉使用面向對象方式的程序員來說就有些棘手,由於他們用的是基於類的繼承而且對象是由類構建出來的。 從 ECMAScript 2015,也就是ECMAScript 6 開始,JavaScript 程序員將可以使用基於類的面向對象的方式。 使用 TypeScript,咱們容許開發者如今就使用這些特性,而且編譯後的 JavaScript 能夠在全部主流瀏覽器和平臺上運行,而不須要等到下個JavaScript 版本。
雖然 JavaScript 中有類的概念,可是可能大多數 JavaScript 程序員並非很是熟悉類,這裏對類相關的概念作一個簡單的介紹。
類 (Class):一種面向對象計算機編程語言的構造,是建立對象的藍圖,描述了所建立的對象共同的屬性和方法。
對象 (Object):類的實例,經過 new
建立
面向對象 (OOP) 的三大特性:封裝、繼承、多態
封裝 (Encapsulation):將對數據的操做細節隱藏起來,只暴露對外的接口。外界調用端不須要知道細節,就能經過對外提供的接口來訪問該對象,同時也保證了外界沒法任意更改對象內部的數據
繼承 (Inheritance):子類繼承父類,子類除了擁有父類的全部特性外,還能夠擴展自有的功能特性
多態 (Polymorphism):由繼承而產生了相關的不一樣的類,對同一個方法能夠有不一樣的響應。好比 Cat
和 Dog
都繼承自 Animal
,可是分別實現了本身的 eat()
方法。此時針對某一個實例,咱們無需瞭解它是 Cat
仍是 Dog
,就能夠直接調用 eat()
方法,程序會自動判斷出來應該如何執行 eat()
存取器(getter & setter):用於屬性的讀取和賦值
修飾符(Modifiers):修飾符是一些關鍵字,用於限定成員或類型的性質。好比 public
表示公有屬性或方法
抽象類(Abstract Class):抽象類是供其餘類繼承的基類,抽象類不容許被實例化。抽象類中的抽象方法必須在子類中被實現
接口(Interfaces):不一樣類之間公有的屬性或方法,能夠抽象成一個接口。接口能夠被類實現(implements)。一個類只能繼承自另外一個類,可是能夠實現多個接口。
class Greeter { private greeting: string; // 定義私有屬性,訪問修飾符:public、protected、private constructor(message: string) { // 構造函數,通常執行用於進行數據初始化操做 this.greeting = message; } greet() { // 定義方法 return "Hello, " + this.greeting; } } let greeter = new Greeter("world");
繼承(英語:inheritance)是面向對象軟件技術當中的一個概念。若是一個類別A 「繼承自」 另外一個類別B,就把這個A稱爲 「B的子類別」,而把B稱爲「A的父類別」也能夠稱 「B是A的超類」。繼承可使得子類別具備父類別的各類屬性和方法,而不須要再次編寫相同的代碼。在令子類別繼承父類別的同時,能夠從新定義某些屬性,並重寫某些方法,即覆蓋父類別的原有屬性和方法,使其得到與父類別不一樣的功能。另外,爲子類別追加新的屬性和方法也是常見的作法。 通常靜態的面向對象編程語言,繼承屬於靜態的,意即在子類別的行爲在編譯期就已經決定,沒法在執行期擴充。 —— 維基百科
繼承 (Inheritance) 是一種聯結類與類的層次模型。指的是一個類 (稱爲子類、子接口) 繼承另外的一個類 (稱爲父類、父接口) 的功能,並能夠增長它本身的新功能的能力,繼承是類與類或者接口與接口之間最多見的關係;繼承是一種 is-a 關係。
(圖片來源網絡)
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();