
为了能够支持跨平台,Angular 通过抽象层封装了不同平台的差异,统一了 API 接口。如定义了抽象类 Renderer2 、抽象类 RootRenderer 等。此外还定义了以下引用类型:ElementRef、TemplateRef、ViewRef 、ComponentRef 和 ViewContainerRef 等。
在日常工作中,Web 工程师经常需要跟 DOM 打交道。通过 DOM API 我们能够方便地获取指定元素,比如获取谷歌首页中 id 为 q 的输入框:
document.querySelector("#q");查询结果为:
<input id="q" aria-hidden="true" autocomplete="off" name="q" tabindex="-1" type="url" 
   jsaction="mousedown:ntp.fkbxclk" style="opacity: 0;">在页面完成渲染后,我们可以通过 DOM API 获取页面中的任意元素,并进行相关的操作。这在大多数情况下,是没有问题的,但如果我们开发的应用要支持跨平台的话,就不能绑定宿主环境为浏览器。
为了解决上述问题,Angular 引入ElementRef 对象,它是视图中 native 元素的包装器。
// angular-master/packages/core/src/linker/element_ref.ts
export class ElementRef<T = any> {
  public nativeElement: T;
  constructor(nativeElement: T) { this.nativeElement = nativeElement; }
}根据 ElementRef 类的定义,我们知道 Angular 内部把不同平台下视图层中的 native 元素封装在 ElementRef 实例的 nativeElement 属性中。在浏览器环境中,nativeElement 属性指向的就是对应的 DOM 元素。
在应用层直接操作 DOM,就会造成应用层与渲染层之间强耦合,导致我们的应用无法运行在不同环境,如 Web Worker 中,因为在 Web Worker 环境中,是不能操作 DOM。有兴趣的读者,可以阅读 Web Workers 中支持的类和方法 这篇文章。因此引入 ElementRef 类主要目的是为了实现跨平台。
import { Component, ElementRef } from "@angular/core";
@Component({
  selector: "hello-world",
  template: `
        <h3 #name>Semlinker</h3>
    `
})
export class HelloWorldComponent {
  constructor(private elementRef: ElementRef) {
    console.log(this.elementRef);
  }
}以上代码运行后,控制台的输出结果:
ElementRef {nativeElement: hello-world}
nativeElement: hello-worldimport { Component, ElementRef, ViewChild, AfterViewInit } from "@angular/core";
@Component({
  selector: "hello-world",
  template: `
    <h3 #name>Semlinker</h3>
  `
})
export class HelloWorldComponent implements AfterViewInit {
  @ViewChild("name") nameElement: ElementRef;
  ngAfterViewInit(): void {
    console.dir(this.nameElement);
  }
  constructor(private elementRef: ElementRef) {
    console.log(this.elementRef);
  }
}以上代码运行后,控制台的输出结果:
ElementRef {nativeElement: hello-world}
nativeElement: hello-world
ElementRef
nativeElement: h3在介绍 TemplateRef 前,我们先来了解一下 HTML 模板元素 —— <template>。模板元素是一种机制,允许包含加载页面时不渲染,但又可以随后通过 JavaScript 进行实例化的客户端内容。我们可以将模板视作为存储在页面上稍后使用的一小段内容。
在 HTML5 标准引入 template 模板元素之前,我们都是使用 <script> 标签进行客户端模板的定义,具体如下:
<script id="tpl-mock" type="text/template">
   <span>I am span in mock template</span>
</script>对于支持 HTML5 template 模板元素的浏览器,我们可以这样创建客户端模板:
<template id="tpl">
   <span>I am span in template</span>
</template>下面我们来看一下 HTML5 template 模板元素的使用示例:
<!-- Template Container -->
<div class="tpl-container"></div>
<!-- Template -->
<template id="tpl">
    <span>I am span in template</span>
</template>
<!-- Script -->
<script type="text/javascript">
    (function renderTpl() {
        // 判断当前浏览器是否支持template元素
        if ('content' in document.createElement('template')) {
            var tpl = document.querySelector('#tpl');
            var tplContainer = document.querySelector('.tpl-container');
            var tplNode = document.importNode(tpl.content, true);
            tplContainer.appendChild(tplNode); 
        } else {
            throw  new Error("Current browser doesn't support template element");
        }
    })();
</script>针对以上的应用场景,Angular 为我们开发者提供了 <ng-template> 元素,在 Angular 内部它主要应用在结构指令中,比如 *ngIf、*ngFor 等。
接下来我们先来介绍 TemplateRef,它表示可用于实例化内嵌视图的内嵌模板。
TemplateRef_
class TemplateRef_ extends TemplateRef<any> implements TemplateData {
  _projectedViews !: ViewData[];
  constructor(private _parentView: ViewData, private _def: NodeDef) { super(); }
  createEmbeddedView(context: any): EmbeddedViewRef<any> {
    return new ViewRef_(Services.createEmbeddedView(
        this._parentView, this._def, this._def.element !.template !, context));
  }
  get elementRef(): ElementRef {
    return new ElementRef(asElementData(this._parentView, 
      this._def.nodeIndex).renderElement);
  }
}TemplateRef
// angular-master/packages/core/src/linker/template_ref.ts
export abstract class TemplateRef<C> {
  abstract get elementRef(): ElementRef;
  abstract createEmbeddedView(context: C): EmbeddedViewRef<C>;
}(备注:抽象类与普通类的区别是抽象类有包含抽象方法,不能直接实例化抽象类,只能实例化该抽象类的子类)
利用 TemplateRef 实例,我们可以灵活地创建内嵌视图。
前面我们已经介绍了如何使用 HTML5 template 模板元素,下面我们来看一下如何使用 <ng-template> 元素。
@Component({
  selector: "hello-world",
  template: `
    <h3>Hello World</h3>
    <ng-template #tpl>
      <span>I am span in template</span>
    </ng-template>
    `
})
export class HelloWorldComponent implements AfterViewInit {
  @ViewChild("tpl")
  tplRef: TemplateRef<HTMLElement>;
  ngAfterViewInit(): void {
    // 模板中的<ng-template>元素会被编译为<!---->元素
    let commentElement = this.tplRef.elementRef.nativeElement;
    // 创建内嵌视图
    let embeddedView = this.tplRef.createEmbeddedView(null);
    // 动态添加子节点
    embeddedView.rootNodes.forEach(node => {
      commentElement.parentNode.insertBefore(node, commentElement.nextSibling);
    });
  }
}以上示例的核心处理流程如下:
虽然我们已经成功的显示出 template 模板元素中的内容,但发现整个流程还是太复杂了,那有没有简单地方式呢 ?是时候请我们 ViewContainerRef 对象出场了。
假设你的任务是添加一个新的段落作为当前元素的兄弟元素:
<p class="one">Element one</p>使用 jQuery 简单实现上述功能:
$('<p>Element two</p>').insertAfter('.one');当你需要添加新的 DOM 元素 (例如,组件、模板),你需要指定元素插入的地方。Angular 没有什么神奇之处,如果你想要插入新的组件或元素,你需要告诉 Angular 在哪里插入新的元素。
ViewContainerRef 就是这样的:
一个视图容器,可以把新组件作为这个元素的兄弟。
ViewContainerRef_
// angular-master/packages/core/src/view/refs.ts
class ViewContainerRef_ implements ViewContainerData {
  _embeddedViews: ViewData[] = [];
  constructor(private _view: ViewData, private _elDef: NodeDef,
    private _data: ElementData) {}
  get element(): ElementRef { return new ElementRef(this._data.renderElement); }
  get injector(): Injector { return new Injector_(this._view, this._elDef); }
  createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, index?: number):
      EmbeddedViewRef<C> {
    const viewRef = templateRef.createEmbeddedView(context || <any>{});
    this.insert(viewRef, index);
    return viewRef;
  }
  // ...
  }
}ViewContainerRef
// angular-master/packages/core/src/linker/view_container_ref.ts
export abstract class ViewContainerRef {
  abstract get injector(): Injector;
  abstract get parentInjector(): Injector;
  abstract createEmbeddedView<C>(templateRef: TemplateRef<C>, 
    context?: C, index?: number): EmbeddedViewRef<C>;
  
  abstract createComponent<C>(
      componentFactory: ComponentFactory<C>, index?: number, injector?: Injector,
      projectableNodes?: any[][], ngModule?: NgModuleRef<any>): ComponentRef<C>;
  
  abstract insert(viewRef: ViewRef, index?: number): ViewRef;
  abstract move(viewRef: ViewRef, currentIndex: number): ViewRef;
  // ...
}ViewContainerRef 对象用于表示一个视图容器,可添加一个或多个视图。通过 ViewContainer Ref 实例,我们可以基于 TemplateRef 实例创建内嵌视图,并能指定内嵌视图的插入位置,也可以方便对视图容器中已有的视图进行管理。简而言之,ViewContainerRef 的主要作用是创建和管理内嵌视图或组件视图。
了解完 ViewContainerRef 对象的作用,我们来更新一下之前的 HelloWorldComponent 组件:
@Component({
  selector: "hello-world",
  template: `
    <h3>Hello World</h3>
    <ng-template #tpl>
      <span>I am span in template</span>
    </ng-template>
    `
})
export class HelloWorldComponent implements AfterViewInit {
  @ViewChild("tpl") tplRef: TemplateRef<HTMLElement>;
  @ViewChild("tpl", { read: ViewContainerRef })
  tplVcRef: ViewContainerRef;
  ngAfterViewInit(): void {
    this.tplVcRef.createEmbeddedView(this.tplRef);
  }
}对比一下之前的代码,是不是觉得 ViewContainerRef 如此强大。
ViewRef 是一种抽象类型,用于表示 Angular 视图。在 Angular 中,视图是构建应用程序 UI 界面基础构建块。
在 Angular 中支持两种类型视图:
ngAfterViewInit() {
  let view = this.tpl.createEmbeddedView(null);
}constructor(
  private injector: Injector,
  private r: ComponentFactoryResolver
) {
    let factory = this.r.resolveComponentFactory(HelloWorldComponent);
    let componentRef = factory.create(injector);
    let view = componentRef.hostView;
}作为 Angular 的初学者,可能会在某个标签上同时使用 *ngIf 或 *ngFor 指令,比如:
<div class="lesson" *ngIf="lessons" *ngFor="let lesson of lessons">
    <div class="lesson-detail">
        {{lesson | json}}
    </div>
</div>当以上代码运行后,你将会看到以下报错信息:
Uncaught Error: Template parse errors:
Can't have multiple template bindings on one element. Use only one attribute 
named 'template' or prefixed with *这意味着不可能将两个结构指令应用于同一个元素。为了实现这个需求,我们必须做类似的事情:
<div *ngIf="lessons">
    <div class="lesson" *ngFor="let lesson of lessons">
        <div class="lesson-detail">
            {{lesson | json}}
        </div>
    </div>
</div>在这个例子中,我们将 ngIf 指令移动到外部 div 元素上,但为了满足上述需求,我们必须创建额外的 div 元素。那么有没有办法不用创建一个额外的元素呢?答案是有的,就是使用 <ng-container> 元素。
<ng-container *ngIf="lessons">
    <div class="lesson" *ngFor="let lesson of lessons">
        <div class="lesson-detail">
            {{lesson | json}}
        </div>
    </div>
</ng-container>ngTemplateOutlet 指令用于标识指定的 DOM 元素作为视图容器,然后自动地插入设定的内嵌视图,而不用像 ViewContainerRef 章节中示例那样,需要手动创建内嵌视图。
下面我们来使用 ngTemplateOutlet 指令,改写 ViewContainerRef 章节中示例:
@Component({
  selector: "hello-world",
  template: `
    <h3>Hello World</h3>
    <ng-container *ngTemplateOutlet="tpl"></ng-container>
    <ng-template #tpl>
      <span>I am span in template</span>
    </ng-template>
    `
})
export class HelloWorldComponent{}可以发现通过 ngTemplateOutlet 指令,大大减轻了我们的工作量,接下来让我们看一下 ngTemplateOutlet 指令的定义:
@Directive({selector: '[ngTemplateOutlet]'})
export class NgTemplateOutlet implements OnChanges {
  // TODO(issue/24571): remove '!'.
  private _viewRef !: EmbeddedViewRef<any>;
  // TODO(issue/24571): remove '!'.
  @Input() public ngTemplateOutletContext !: Object;
  // TODO(issue/24571): remove '!'.
  @Input() public ngTemplateOutlet !: TemplateRef<any>;
  constructor(private _viewContainerRef: ViewContainerRef) {}
  ngOnChanges(changes: SimpleChanges) { }
}我们发现 ngTemplateOutlet 指令除了支持 ngTemplateOutlet 输入属性之外,还支持 ngTemplateOutletContext 输入属性。ngTemplateOutletContext 顾名思义是用于设置指令的上下文。
@Component({
  selector: "hello-world",
  template: `
    <h3>Hello World</h3>
    <ng-container *ngTemplateOutlet="tpl; context: ctx"></ng-container>
    <ng-template #tpl let-name let-location="location">
      <span>I am {{name}} in {{location}}</span>
    </ng-template>
    `
})
export class HelloWorldComponent {
  ctx = {
    $implict: "span",
    location: "template"
  };
}有些场景下,我们希望根据条件动态的创建组件。动态创建组件的流程如下:
ComponentFactoryResolver 对象ComponentFactoryResolver 对象的 resolveComponentFactory() 方法创建 ComponentFactory 对象createComponent() 方法创建组件并自动添加动态组件到组件容器中ComponentRef 组件实例,配置组件相关属性 (可选)@Component({
  selector: "app-root",
  template: `
    <div>
      <div #entry></div>
    </div>
  `
})
export class AppComponent implements AfterContentInit {
  @ViewChild("entry", { read: ViewContainerRef })
  entry: ViewContainerRef;
  constructor(private resolver: ComponentFactoryResolver) {}
  ngAfterContentInit() {
    const authFormFactory = this.resolver.resolveComponentFactory(
      AuthFormComponent
    );
    this.entry.createComponent(authFormFactory);
  }
}通过 ComponentFactoryResolver 对象,我们实现了动态创建组件的功能。但创建的过程还是有点繁琐,为了提高开发者体验和开发效率,Angular 引入了 ngComponentOutlet 指令。 好的,我们马上来体验一下 ngComponentOutlet 指令。
@Component({
  selector: "app-root",
  template: `
    <div>
      <div *ngComponentOutlet="authFormComponent"></div>
    </div>
  `
})
export class AppComponent {
  authFormComponent = AuthFormComponent
}ngComponentOutlet 指令除了支持 ngComponentOutlet 输入属性之外,它还含有另外 3 个输入属性:
// angular-master/packages/common/src/directives/ng_component_outlet.ts
@Directive({selector: '[ngComponentOutlet]'})
export class NgComponentOutlet implements OnChanges, OnDestroy {
  // TODO(issue/24571): remove '!'.
  @Input() ngComponentOutlet !: Type<any>;
  // TODO(issue/24571): remove '!'.
  @Input() ngComponentOutletInjector !: Injector;
  // TODO(issue/24571): remove '!'.
  @Input() ngComponentOutletContent !: any[][];
  // TODO(issue/24571): remove '!'.
  @Input() ngComponentOutletNgModuleFactory !: NgModuleFactory<any>;
  private _componentRef: ComponentRef<any>|null = null;
  private _moduleRef: NgModuleRef<any>|null = null;
  constructor(private _viewContainerRef: ViewContainerRef) {}
  ngOnChanges(changes: SimpleChanges) {}
  ngOnDestroy() {
    if (this._moduleRef) this._moduleRef.destroy();
  }
}本文主要介绍了 Angular 中常见的引用类型,如 ElementRef、TemplateRef、ViewRef 等。实际工作中,还需要利用 ViewChild、ViewChildren、ContentChild 和 ContentChildren 装饰器,或者基于 Angular 依赖注入特性,通过构造注入的方式,获取相关的对象。此外,在获取匹配的元素后,我们往往需要需要对返回的对象进行相应操作。在浏览器环境中,虽然通过 ElementRef 的 nativeElement 属性,我们可以方便地获取对应的 DOM 元素,但我们最好不要利用 DOM API 进行 DOM 操作,最好通过 Angular 提供的 Renderer2 对象,进行相关的操作。