为了能够支持跨平台,Angular 通过抽象层封装了不同平台的差异,统一了 API 接口。如定义了抽象类 Renderer2 、抽象类 RootRenderer 等。此外还定义了以下引用类型:ElementRef、TemplateRef、ViewRef 、ComponentRef 和 ViewContainerRef 等。
ElementRef
在日常工作中,Web 工程师经常需要跟 DOM 打交道。通过 DOM API 我们能够方便地获取指定元素,比如获取谷歌首页中 id 为 q 的输入框:
代码语言:javascript复制document.querySelector("#q");
查询结果为:
代码语言:javascript复制<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 元素的包装器。
定义
代码语言:javascript复制// 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 类主要目的是为了实现跨平台。
示例
- 利用依赖注入获取宿主 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);
}
}
以上代码运行后,控制台的输出结果:
代码语言:javascript复制ElementRef {nativeElement: hello-world}
nativeElement: hello-world
- 利用 ViewChild 装饰器获取匹配的 ElementRef 实例
import { 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);
}
}
以上代码运行后,控制台的输出结果:
代码语言:javascript复制ElementRef {nativeElement: hello-world}
nativeElement: hello-world
ElementRef
nativeElement: h3
TemplateRef
在介绍 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 模板元素的浏览器,我们可以这样创建客户端模板:
代码语言:javascript复制<template id="tpl">
<span>I am span in template</span>
</template>
下面我们来看一下 HTML5 template 模板元素的使用示例:
代码语言:javascript复制<!-- 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_
代码语言:javascript复制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
代码语言:javascript复制// 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);
});
}
}
以上示例的核心处理流程如下:
- 创建内嵌视图(embedded view)
- 遍历内嵌视图中的 rootNodes,动态的插入 node
虽然我们已经成功的显示出 template 模板元素中的内容,但发现整个流程还是太复杂了,那有没有简单地方式呢 ?是时候请我们 ViewContainerRef 对象出场了。
ViewContainerRef
假设你的任务是添加一个新的段落作为当前元素的兄弟元素:
代码语言:javascript复制<p class="one">Element one</p>
使用 jQuery
简单实现上述功能:
$('<p>Element two</p>').insertAfter('.one');
当你需要添加新的 DOM 元素 (例如,组件、模板),你需要指定元素插入的地方。Angular 没有什么神奇之处,如果你想要插入新的组件或元素,你需要告诉 Angular 在哪里插入新的元素。
ViewContainerRef 就是这样的:
一个视图容器,可以把新组件作为这个元素的兄弟。
定义
ViewContainerRef_
代码语言:javascript复制// 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
代码语言:javascript复制// 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 组件:
代码语言:javascript复制@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
ViewRef 是一种抽象类型,用于表示 Angular 视图。在 Angular 中,视图是构建应用程序 UI 界面基础构建块。
在 Angular 中支持两种类型视图:
- Embedded Views - Template 模板元素
- Host Views - Component 组件
创建 Embedded View
代码语言:javascript复制ngAfterViewInit() {
let view = this.tpl.createEmbeddedView(null);
}
创建 Host View
代码语言:javascript复制constructor(
private injector: Injector,
private r: ComponentFactoryResolver
) {
let factory = this.r.resolveComponentFactory(HelloWorldComponent);
let componentRef = factory.create(injector);
let view = componentRef.hostView;
}
ng-container
作为 Angular 的初学者,可能会在某个标签上同时使用 *ngIf
或 *ngFor
指令,比如:
<div class="lesson" *ngIf="lessons" *ngFor="let lesson of lessons">
<div class="lesson-detail">
{{lesson | json}}
</div>
</div>
当以上代码运行后,你将会看到以下报错信息:
代码语言:javascript复制Uncaught Error: Template parse errors:
Can't have multiple template bindings on one element. Use only one attribute
named 'template' or prefixed with *
这意味着不可能将两个结构指令应用于同一个元素。为了实现这个需求,我们必须做类似的事情:
代码语言:javascript复制<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>
元素。
示例
代码语言:javascript复制<ng-container *ngIf="lessons">
<div class="lesson" *ngFor="let lesson of lessons">
<div class="lesson-detail">
{{lesson | json}}
</div>
</div>
</ng-container>
ngTemplateOutlet
ngTemplateOutlet 指令用于标识指定的 DOM 元素作为视图容器,然后自动地插入设定的内嵌视图,而不用像 ViewContainerRef 章节中示例那样,需要手动创建内嵌视图。
下面我们来使用 ngTemplateOutlet 指令,改写 ViewContainerRef 章节中示例:
代码语言:javascript复制@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 指令的定义:
代码语言:javascript复制@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 顾名思义是用于设置指令的上下文。
代码语言:javascript复制@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"
};
}
ngComponentOutlet
有些场景下,我们希望根据条件动态的创建组件。动态创建组件的流程如下:
- 获取装载动态组件的容器
- 在组件类的构造函数中,注入
ComponentFactoryResolver
对象 - 调用
ComponentFactoryResolver
对象的resolveComponentFactory()
方法创建ComponentFactory
对象 - 调用组件容器对象的
createComponent()
方法创建组件并自动添加动态组件到组件容器中 - 基于返回的
ComponentRef
组件实例,配置组件相关属性 (可选) - 在模块 Metadata 对象的 entryComponents 属性中添加动态组件
- declarations - 用于指定属于该模块的指令和管道列表。
- entryComponents - 用于指定在模块定义时,需要编译的组件列表。对于列表中声明的每个组件,Angular 将会创建对应的一个 ComponentFactory 对象,并将其存储在 ComponentFactoryResolver 对象中。
@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 指令。
代码语言:javascript复制@Component({
selector: "app-root",
template: `
<div>
<div *ngComponentOutlet="authFormComponent"></div>
</div>
`
})
export class AppComponent {
authFormComponent = AuthFormComponent
}
ngComponentOutlet 指令除了支持 ngComponentOutlet 输入属性之外,它还含有另外 3 个输入属性:
代码语言:javascript复制// 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 对象,进行相关的操作。