Angular9 瀏覽組件樹

2020-07-03 18:22 更新

應(yīng)用的組件之間經(jīng)常需要共享信息。你通常要用松耦合的技術(shù)來共享信息,比如數(shù)據(jù)綁定和服務(wù)共享。但是有時(shí)候讓一個(gè)組件直接引用另一個(gè)組件還是很有意義的。 例如,你需要通過另一個(gè)組件的直接引用來訪問其屬性或調(diào)用其方法。

在 Angular 中獲取組件引用略微有些棘手。 Angular 組件本身并沒有一棵可以用編程方式檢查或?yàn)g覽的樹。 其父子關(guān)系是通過組件的視圖對(duì)象間接建立的。

每個(gè)組件都有一個(gè)宿主視圖和一些內(nèi)嵌視圖。 組件 A 的內(nèi)嵌視圖可以是組件 B 的宿主視圖,而組件 B 還可以有它自己的內(nèi)嵌視圖。 這意味著每個(gè)組件都有一棵以該組件的宿主視圖為根節(jié)點(diǎn)的視圖樹。

有一些用于在視圖樹中向下導(dǎo)航的 API。 請(qǐng)到 API 參考手冊(cè)中查看 Query、QueryList、ViewChildren 和 ContentChildren。

不存在用于獲取父引用的公共 API。 不過,由于每個(gè)組件的實(shí)例都會(huì)添加到注入器的容器中,因此你可以通過 Angular 的依賴注入來訪問父組件。

本節(jié)描述的就是關(guān)于這種做法的一些技巧。

查找已知類型的父組件

你可以使用標(biāo)準(zhǔn)的類注入形式來獲取類型已知的父組件。

在下面的例子中,父組件 AlexComponent 具有一些子組件,包括 CathyComponent

Path:"parent-finder.component.ts (AlexComponent v.1)" 。

@Component({
  selector: 'alex',
  template: `
    <div class="a">
      <h3>{{name}}</h3>
      <cathy></cathy>
      <craig></craig>
      <carol></carol>
    </div>`,
})
export class AlexComponent extends Base
{
  name = 'Alex';
}

在把 AlexComponent 注入到 CathyComponent 的構(gòu)造函數(shù)中之后,Cathy 可以報(bào)告她是否能訪問 Alex

Path:"parent-finder.component.ts (CathyComponent)" 。

@Component({
  selector: 'cathy',
  template: `
  <div class="c">
    <h3>Cathy</h3>
    {{alex ? 'Found' : 'Did not find'}} Alex via the component class.<br>
  </div>`
})
export class CathyComponent {
  constructor( @Optional() public alex?: AlexComponent ) { }
}

注意,雖然為了安全起見我們用了 @Optional 限定符,但是范例中仍然會(huì)確認(rèn) alex 參數(shù)是否有值。

不能根據(jù)父組件的基類訪問父組件

如果你不知道具體的父組件類怎么辦?

可復(fù)用組件可能是多個(gè)組件的子組件。想象一個(gè)用于渲染相關(guān)金融工具的突發(fā)新聞的組件。 出于商業(yè)原因,當(dāng)市場(chǎng)上的數(shù)據(jù)流發(fā)生變化時(shí),這些新組件會(huì)頻繁調(diào)用其父組件。

該應(yīng)用可能定義了十幾個(gè)金融工具組件。理想情況下,它們?nèi)紝?shí)現(xiàn)了同一個(gè)基類,你的 NewsComponent 也能理解其 API。

如果能查找實(shí)現(xiàn)了某個(gè)接口的組件當(dāng)然更好。 但那是不可能的。因?yàn)?TypeScript 接口在轉(zhuǎn)譯后的 JavaScript 中不存在,而 JavaScript 不支持接口。 因此,找無可找。

這個(gè)設(shè)計(jì)并不怎么好。 該例子是為了驗(yàn)證組件是否能通過其父組件的基類來注入父組件。

這個(gè)例子中的 CraigComponent 體現(xiàn)了此問題。往回看,你可以看到 Alex 組件擴(kuò)展(繼承)了基類 Base。

Path:"parent-finder.component.ts (Alex class signature)" 。

export class AlexComponent extends Base

CraigComponent 試圖把 Base 注入到它的構(gòu)造函數(shù)參數(shù) alex 中,并匯報(bào)這次注入是否成功了。

Path:"parent-finder.component.ts (CraigComponent)" 。

@Component({
  selector: 'craig',
  template: `
  <div class="c">
    <h3>Craig</h3>
    {{alex ? 'Found' : 'Did not find'}} Alex via the base class.
  </div>`
})
export class CraigComponent {
  constructor( @Optional() public alex?: Base ) { }
}

不幸的是,這不行! 范例確認(rèn)了 alex 參數(shù)為空。 因此,你不能通過父組件的基類注入它。

根據(jù)父組件的類接口查找它

你可以通過父組件的類接口來查找它。

該父組件必須合作,以類接口令牌為名,為自己定義一個(gè)別名提供者。

回憶一下,Angular 總是會(huì)把組件實(shí)例添加到它自己的注入器中,因此以前你才能把 Alex 注入到 Cathy 中。

編寫一個(gè) 別名提供者(一個(gè) provide 對(duì)象字面量,其中有一個(gè) useExisting 定義),創(chuàng)造了另一種方式來注入同一個(gè)組件實(shí)例,并把那個(gè)提供者添加到 AlexComponent @Component() 元數(shù)據(jù)的 providers 數(shù)組中。

Path:"parent-finder.component.ts (AlexComponent providers)" 。

providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],

Parent 是該提供者的類接口。 forwardRef 用于打破循環(huán)引用,因?yàn)樵谀銊偛胚@個(gè)定義中 AlexComponent 引用了自身。

Alex 的第三個(gè)子組件 Carol,把其父組件注入到了自己的 parent 參數(shù)中 —— 和你以前做過的一樣。

Path:"parent-finder.component.ts (CarolComponent class)" 。

export class CarolComponent {
  name = 'Carol';
  constructor( @Optional() public parent?: Parent ) { }
}

下面是 Alex 及其家人的運(yùn)行效果。

使用 @SkipSelf() 在樹中查找父組件

想象一下組件樹的一個(gè)分支:Alice -> Barry -> Carol。 無論 Alice 還是 Barry 都實(shí)現(xiàn)了類接口 Parent。

Barry 很為難。他需要訪問他的母親 Alice,同時(shí)他自己還是 Carol 的父親。 這意味著他必須同時(shí)注入 Parent 類接口來找到 Alice,同時(shí)還要提供一個(gè) Parent 來滿足 Carol 的要求。

Barry 的代碼如下。

Path:"parent-finder.component.ts (BarryComponent)" 。

const templateB = `
  <div class="b">
    <div>
      <h3>{{name}}</h3>
      <p>My parent is {{parent?.name}}</p>
    </div>
    <carol></carol>
    <chris></chris>
  </div>`;


@Component({
  selector:   'barry',
  template:   templateB,
  providers:  [{ provide: Parent, useExisting: forwardRef(() => BarryComponent) }]
})
export class BarryComponent implements Parent {
  name = 'Barry';
  constructor( @SkipSelf() @Optional() public parent?: Parent ) { }
}

Barryproviders 數(shù)組看起來和 Alex 的一樣。 如果你準(zhǔn)備繼續(xù)像這樣編寫別名提供者,就應(yīng)該創(chuàng)建一個(gè)輔助函數(shù)。

現(xiàn)在,注意看 Barry 的構(gòu)造函數(shù)。

//Barry's constructor


constructor( @SkipSelf() @Optional() public parent?: Parent ) { }

//Carol's constructor


constructor( @Optional() public parent?: Parent ) { }

除增加了 @SkipSelf 裝飾器之外,它和 Carol 的構(gòu)造函數(shù)相同。

使用 @SkipSelf 有兩個(gè)重要原因:

它告訴注入器開始從組件樹中高于自己的位置(也就是父組件)開始搜索 Parent 依賴。

如果你省略了 @SkipSelf 裝飾器,Angular 就會(huì)拋出循環(huán)依賴錯(cuò)誤。

Cannot instantiate cyclic dependency! (BethComponent -> Parent -> BethComponent)

下面是 Alice、Barry 及其家人的運(yùn)行效果。

父類接口

類接口是一個(gè)抽象類,它實(shí)際上用做接口而不是基類。

下面的例子定義了一個(gè)類接口 Parent。

Path:"parent-finder.component.ts (Parent class-interface)" 。

export abstract class Parent { name: string; }

Parent 類接口定義了一個(gè)帶類型的 name 屬性,但沒有實(shí)現(xiàn)它。 這個(gè) name 屬性是父組件中唯一可供子組件調(diào)用的成員。 這樣的窄化接口幫助把子組件從它的父組件中解耦出來。

一個(gè)組件想要作為父組件使用,就應(yīng)該像 AliceComponent 那樣實(shí)現(xiàn)這個(gè)類接口。

Path:"parent-finder.component.ts (AliceComponent class signature)" 。

export class AliceComponent implements Parent

這樣做可以增加代碼的清晰度,但在技術(shù)上并不是必要的。 雖然 AlexComponentBase 類所要求的一樣具有 name 屬性,但它的類簽名中并沒有提及 Parent。

Path:"parent-finder.component.ts (AlexComponent class signature)" 。

export class AlexComponent extends Base

provideParent() 輔助函數(shù)

你很快就會(huì)厭倦為同一個(gè)父組件編寫別名提供者的變體形式,特別是帶有 forwardRef 的那種。

Path:"dependency-injection-in-action/src/app/parent-finder.component.ts" 。

providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],

你可以像把這些邏輯抽取到輔助函數(shù)中,就像這樣。

Path:"dependency-injection-in-action/src/app/parent-finder.component.ts" 。

// Helper method to provide the current component instance in the name of a `parentType`.
export function provideParent
  (component: any) {
    return { provide: Parent, useExisting: forwardRef(() => component) };
  }

現(xiàn)在,你可以為組件添加一個(gè)更簡(jiǎn)單、更有意義的父組件提供者。

Path:"dependency-injection-in-action/src/app/parent-finder.component.ts" 。

providers:  [ provideParent(AliceComponent) ]

你還可以做得更好。當(dāng)前版本的輔助函數(shù)只能為類接口 Parent 定義別名。 應(yīng)用可能具有多種父組件類型,每個(gè)父組件都有自己的類接口令牌。

這是一個(gè)修訂后的版本,它默認(rèn)為 parent,但是也能接受另一個(gè)父類接口作為可選的第二參數(shù)。

Path:"dependency-injection-in-action/src/app/parent-finder.component.ts" 。

// Helper method to provide the current component instance in the name of a `parentType`.
// The `parentType` defaults to `Parent` when omitting the second parameter.
export function provideParent
  (component: any, parentType?: any) {
    return { provide: parentType || Parent, useExisting: forwardRef(() => component) };
  }

下面是針對(duì)不同父組件類型的用法。

Path:"dependency-injection-in-action/src/app/parent-finder.component.ts" 。

providers:  [ provideParent(BethComponent, DifferentParent) ]
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)