每当我阅读中遇到,关于Angular中使用DOM的内容时,总会看到一个或几个这样的类:ElementRef,TemplateRef,ViewContainerRef等等。 不幸的是,虽然其中的一些被Angular文档或相关文章所讲述,但是我还没有找到完整的描述以及这些它们是如何工作的。
如果你来自angular.js世界,那么你知道操纵DOM是相当容易的。Angular注入DOM elementRef到构造函数中,你可以查询组件模板中的任何节点,添加或删除子节点,修改样式等。但是,这种方法有一个主要的缺点 - 它紧紧地绑定到浏览器平台。
新的Angular版本运行在不同的平台上 - 浏览器,移动平台等。 因此,站在平台特定的API和框架接口之间需要抽象层次。Angular中,这些抽象成为以下引用类型的形式:ElementRef,TemplateRef,ViewRef,ComponentRef和ViewContainerRef。 在本文中,我们将详细介绍每种引用类型,并展示如何使用它们来操作DOM。
@ViewChild
在我们探索DOM抽象之前,让我们了解如何在组件/指令类中访问这些抽象。 Angular提供了一种称为DOM查询的机制。 它以@ViewChild和@ViewChildren装饰器的形式出现。 它们的行为相同,只有前者返回一个引用,后者则返回多个引用作为QueryList对象。 在这篇文章的例子中,我将主要使用ViewChild装饰器。
通常,这些装饰器与模板引用变量配对使用。 模板引用变量只是对模板中的DOM元素的命名引用。 您可以将其视为与html元素的id属性类似的东西。 用模板引用标记DOM元素,然后使用ViewChild装饰器在类中查询它。 这里是基本的例子:
@Component({selector: 'sample',template: `<span #tref>I am span</span>`
})
export class SampleComponent implements AfterViewInit {@ViewChild("tref", {read: ElementRef}) tref: ElementRef;ngAfterViewInit(): void {// outputs `I am span`console.log(this.tref.nativeElement.textContent);}
}
ViewChild装饰器的基本语法如下:
@ViewChild([reference from template], {read: [reference type]});
在这个例子中,你可以看到我在html中指定了tref作为模板引用名,并且接收到与这个元素相关的ElementRef。 读取的第二个参数并不总是必需的,因为Angular可以通过DOM元素的类型来推断引用类型。 例如,如果它是一个简单的HTML元素(如span),那么angular将返回ElementRef。 如果它是一个模板元素,它将返回TemplateRef。不过一些引用,如ViewContainerRef不能被推断,并且必须在读参数中特别要求。 其他的,像ViewRef不能从DOM返回,必须手动构造。
ElementRef
这是最基本的抽象。 如果你观察它的类结构,你会发现它只保存了它所关联的本地元素。 对于访问本地DOM元素非常有用,我们可以在这里看到:
// outputs `I am span`
console.log(this.tref.nativeElement.textContent);
不过,Angular团队不鼓励这种用法。 这不仅会带来安全风险,还会在应用程序和渲染层之间造成紧密耦合,这使得在多个平台上运行应用程序变得困难。 我相信这不是对nativeElement的访问,而是打破了抽象,而是像textContent一样使用特定的DOM API。 但是后面你会看到,在Angular中实现的DOM操作心智模型几乎不需要这样一个较低级别的访问。
可以使用ViewChild装饰器为任何DOM元素返回ElementRef。 但是,由于所有组件都驻留在自定义DOM元素中,并且所有指令都应用于DOM元素,因此组件和指令类可以通过DI机制获取与其主机元素关联的ElementRef实例:
@Component({selector: 'sample',...
export class SampleComponent{constructor(private hostElement: ElementRef) {//outputs <sample>...</sample>console.log(this.hostElement.nativeElement.outerHTML);}
因此,虽然组件可以通过DI访问其主机元素,但ViewChild装饰器通常用于在其视图(模板)中获取对DOM元素的引用。 反之亦然,指令没有视图,他们通常直接与他们所附的元素。
模板的概念应该是大多数Web开发人员熟悉的。 这是一组DOM元素,在整个应用程序的视图中被重用。 在HTML5标准引入了模板标签之前,大多数模板都被包含在script标签中。
<script id="tpl" type="text/template"><span>I am span in template</span>
</script>
这种方法当然有许多缺点,如语义和手动创建DOM模型的必要性。 使用模板标签浏览器解析HTML并创建DOM树,但不呈现它。 然后可以通过内容属性访问:
<script>let tpl = document.querySelector('#tpl');let container = document.querySelector('.insert-after-me');insertAfter(container, tpl.content);
</script>
<div class="insert-after-me"></div>
<ng-template id="tpl"><span>I am span in template</span>
</ng-template>
Angular支持这种方法,并实现TemplateRef类来处理模板。 以下是如何使用它:
@Component({selector: 'sample',template: `<ng-template #tpl><span>I am span in template</span></ng-template>`
})
export class SampleComponent implements AfterViewInit {@ViewChild("tpl") tpl: TemplateRef<any>;ngAfterViewInit() {let elementRef = this.tpl.elementRef;// outputs `template bindings={}`console.log(elementRef.nativeElement.textContent);}
}
该框架从DOM中删除模板元素,并在其位置插入注释。 这是呈现时的样子:
<sample><!--template bindings={}-->
</sample>
TemplateRef类本身是一个简单的类。 它的elementRef属性拥有对其宿主元素的引用,并具有一个方法createEmbeddedView。 这个方法非常有用,因为它允许我们创建一个视图并以ViewRef的形式返回一个引用。
ViewRef
这种抽象表示Angular视图。 在Angular世界中,View是应用程序UI的基本构建块。 它是创造和消灭的最小的元素分组。 Angular哲学鼓励开发人员将UI视为Views的组合,而不是将其视为独立的HTML标签。
Angular支持两种类型的视图:
- 嵌入视图链接到模板
- 链接到组件的主机视图
创建嵌入的视图
一个模板只是一个视图的蓝图。 一个视图可以使用前面提到的createEmbeddedView方法从模板实例化,如下所示:
ngAfterViewInit() {let view = this.tpl.createEmbeddedView(null);
}
创建宿主视图
宿主视图是在组件动态实例化时创建的。 可以使用ComponentFactoryResolver动态创建一个组件:
constructor(private injector: Injector,private r: ComponentFactoryResolver) {let factory = this.r.resolveComponentFactory(ColorComponent);let componentRef = factory.create(injector);let view = componentRef.hostView;
}
在Angular中,每个组件都绑定到一个注入器的特定实例,所以我们在创建组件时传递当前的注入器实例。 此外,不要忘记,动态实例化的组件必须添加到模块或主机组件的EntryComponents。
所以,我们已经看到如何创建嵌入和宿主视图。 一旦创建了视图,就可以使用ViewContainer将其插入到DOM中。 下一节将探讨其功能。
ViewContainerRef
表示可以附加一个或多个视图的容器。
首先要提到的是,任何DOM元素都可以用作视图容器。有趣的是,Angular不在元素内插入视图,而是在绑定到ViewContainer的元素之后附加它们。 这与路由器插座如何插入组件类似。
通常,标记应该创建ViewContainer的地方的好候选者是ng-container元素。 它被渲染为一个注释,所以它不会在DOM中引入多余的html元素。 以下是在组件模板的特定位置创建ViewContainer的示例:
@Component({selector: 'sample',template: `<span>I am first span</span><ng-container #vc></ng-container><span>I am last span</span>`
})
export class SampleComponent implements AfterViewInit {@ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;ngAfterViewInit(): void {// outputs `template bindings={}`console.log(this.vc.element.nativeElement.textContent);}
}
就像其他DOM抽象一样,ViewContainer绑定到通过元素属性访问的特定DOM元素。 在这个例子中,ng-container元素被绑定为注释的示例中,输出为template bindings = {}。
Manipulating views
ViewContainer为操作视图提供了一个方便的API:
class ViewContainerRef {...clear() : voidinsert(viewRef: ViewRef, index?: number) : ViewRefget(index: number) : ViewRefindexOf(viewRef: ViewRef) : numberdetach(index?: number) : ViewRefmove(viewRef: ViewRef, currentIndex: number) : ViewRef
}
我们之前已经看到,如何从模板和组件手动创建两种类型的视图。 一旦我们有了一个视图,我们可以使用插入方法将其插入到DOM中。 所以,下面是从模板中创建一个嵌入式视图并将其插入到由ng-container元素标记的特定位置的示例:
@Component({selector: 'sample',template: `<span>I am first span</span><ng-container #vc></ng-container><span>I am last span</span><ng-template #tpl><span>I am span in template</span></ng-template>`
})
export class SampleComponent implements AfterViewInit {@ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;@ViewChild("tpl") tpl: TemplateRef<any>;ngAfterViewInit() {let view = this.tpl.createEmbeddedView(null);this.vc.insert(view);}
}
通过这个实现,生成的html看起来像这样:
<sample><span>I am first span</span><!--template bindings={}--><span>I am span in template</span><span>I am last span</span><!--template bindings={}-->
</sample>
要从DOM中删除视图,我们可以使用detach方法。 所有其他方法都是自解释性的,可用于通过索引获取对视图的引用,将视图移至其他位置或从容器中移除所有视图。
Creating Views
ViewContainer还提供API来自动创建视图:
class ViewContainerRef {element: ElementReflength: numbercreateComponent(componentFactory...): ComponentRef<C>createEmbeddedView(templateRef...): EmbeddedViewRef<C>...
}
这些都是我们上面手动完成的简单包装。 他们从模板或组件创建一个视图,并将其插入到指定位置。
ngTemplateOutlet and ngComponentOutlet
ngTemplateOutlet
这个将一个DOM元素标记为ViewContainer,并在其中插入一个由模板创建的嵌入视图,而不需要在组件类中明确地做到这一点。 这意味着上面我们创建视图并将其插入到#vc DOM元素的示例可以像这样重写:
@Component({selector: 'sample',template: `<span>I am first span</span><ng-container [ngTemplateOutlet]="tpl"></ng-container><span>I am last span</span><ng-template #tpl><span>I am span in template</span></ng-template>`
})
export class SampleComponent {}
正如你所看到的,我们不使用任何视图实例化组件类中的代码。 非常便利。
ngComponentOutlet
该指令类似于ngTemplateOutlet,不同之处在于它创建一个宿主视图(实例化一个组件),而不是嵌入视图。 你可以像这样使用它:
<ng-container *ngComponentOutlet="ColorComponent"></ng-container>
总结
现在,所有这些信息似乎都可以被消化,但实际上这些信息是非常连贯的,并且通过视图来显示操纵DOM的清晰模型。 通过使用ViewChild查询和模板变量引用,您可以获得对Angular DOM抽象的引用。 围绕DOM元素的最简单的包装是ElementRef。 对于具有TemplateRef的模板,您可以创建嵌入式视图。 主机视图可以在使用ComponentFactoryResolver创建的componentRef上访问。 视图可以用ViewContainerRef来操作。 有两个使自动手动过程的指令:ngTemplateOutlet - 用于嵌入视图,ngComponentOutlet用于宿主视图(动态组件)。