Angular2 ElementRef 实现低耦合高内聚 视图应用分离

来源:互联网 发布:nginx 多个二级域名 编辑:程序博客网 时间:2024/06/03 19:39

为什么需要ElementRef

Angular一直在做的一件事情就是降低视图层和应用层之间的耦合,在应用层直接操作DOM,会导致应用层和视图层之间强耦合,导致我们不能将应用运行在不同的环境中。比如令js能够实现多线程的webWorker,在webWorker中,却不能直接操作DOM,angular为我们封装了一个对象,叫做ElementRef,能够获取到视图层中的native对象,比如在浏览器中,native对象就是DOM对象。

以下是Angular中的ElementRef的具体实现:

export class ElementRef {  public nativeElement: any;  constructor(nativeElement: any) { this.nativeElement = nativeElement; }}

如何使用ElementRef

下面来看一个简单的例子,我们声明一个组件,我们要实现的功能是,点击点击按钮,根据输入框的输入值来更改子组件的颜色。
那么整个组件大概会是这个样子。

这里写图片描述

首先我们编写app.ts,也就是图上的parent component。

app.ts

import { Child } from './child.component';import { bootstrap } from '@angular/platform-browser-dynamic';import { Component, ViewChild, ElementRef } from '@angular/core';@Component({    selector:'app',    template:`<div>        <input #colorIndex type='text' />        <button (click)='changeColor(colorIndex.value)'>change color</button>        <child></child>    </div>`,    directives:[Child]})export class App{    private defaultColor:string;    private targetColor:string;    constructor(private elementRef:ElementRef){        //默认颜色初始化        this.defaultColor = 'transparent';    }    //change color    changeColor(value:string):void{        console.log(value);        this.targetColor = this.defaultColor || value;        let Div = this.elementRef.nativeElement.querySelector('child div');        Div.style.backgroundColor = value;    }}bootstrap(App);

下面是子组件的代码,没有任何功能,只是简单的声明了一下样式。

child.component.ts

import { Component } from '@angular/core';@Component({    selector:"child",    template:`<div></div>`,    styles:[`        div{            width:100px;            height:100px;        }    `]})export class Child{    constructor(){}}

这样,我们就实现了上述的功能。
这里写图片描述
这里写图片描述

该在什么时候获取

这里出现了一个问题,关于我们是怎么获取到child的DOM对象的,我们通过angular的依赖注入引入了elementRef,再通过elementRef的nativeElement属性获取到了对应的native元素(在这里指代DOM对象)。这样理论上看上去是没有问题的(实际也没有问题:))

下面我们换个思路,我现在要一进去就给个初始的颜色,比如红色,我们稍微更改一下代码逻辑,把改变颜色的算法放在构造函数里执行

constructor(private elementRef:ElementRef){        //默认颜色初始化        this.defaultColor = 'transparent';        let Div = this.elementRef.nativeElement.querySelector('child div');        Div.style.backgroundColor = 'red';}

我们发现抛出了一系列的异常(angular),下面我们打印一下div,

constructor(private elementRef:ElementRef){        //默认颜色初始化        this.defaultColor = 'transparent';        let Div = this.elementRef.nativeElement.querySelector('child div');        console.log(Div);        // Div.style.backgroundColor = 'red';    }

输出是:

null

这是因为app组件内部的视图子组件还没创建完毕,因此打印出来是null。

大家都知道angular的组件是有个生命周期的:

  1. onChanges
  2. onInit
  3. doCheck
  4. afterContentInit
  5. afterContentChecked
  6. afterViewInit
  7. afterViewChecked
  8. onDestroy

因此我们需要在父组件内部的视图子组件创建完毕后才能获取到它的DOM对象的引用,初始代码我们是把改变颜色的算法放在了点击事件的回调函数里,因为点击事件是一个异步的操作,它会因此进入事件循环,因此,当我们要获取到它的DOM对象引用的时候,组件以及完全创建完毕。

那么现在要实现我们上面的需求,我们要一进去就初始化为红色,我们可以在外面套一层setTimeout,因为setTimeout也是一个异步操作。

setTimeout(() => {            let Div = this.elementRef.nativeElement.querySelector('child div');            console.log(Div);            Div.style.backgroundColor = 'red';        },10);

我们可以看到这是可行的
这里写图片描述

尽管我们已经把延迟时间改的尽量的小(由于浏览器的特性,最小时间有个限度,低于这个限度,就会按最低限度来执行,我这里写的10,但是chrome 的延迟时间最低限度是16,因此,这里实际的延迟时间是16),但是setTimeout可能在这里是一件奢侈的事情,因为使用它的时候,在浏览器内部会创建一颗红黑树用来执行间断时间执行操作,对于这个demo来说,这个开销是非常大的。
因此我们可能会直接基于angular提供的生命周期钩子来执行执行改变颜色的操作。

app.ts

import { Child } from './child.component';import { bootstrap } from '@angular/platform-browser-dynamic';import { Component, ViewChild, ElementRef } from '@angular/core';@Component({    selector:'app',    template:`<div>        <input #colorIndex type='text' />        <button (click)='changeColor(colorIndex.value)'>change color</button>        <child></child>    </div>`,    directives:[Child]})export class App{    private defaultColor:string;    private targetColor:string;    constructor(private elementRef:ElementRef){        //默认颜色初始化        this.defaultColor = 'transparent';    }    ngAfterViewInit(){        let Div = this.elementRef.nativeElement.querySelector('child div');        console.log(Div);        Div.style.backgroundColor = 'red';    }    //change color    changeColor(value:string):void{        console.log(value);        this.targetColor = this.defaultColor || value;        let Div = this.elementRef.nativeElement.querySelector('child div');        Div.style.backgroundColor = value;    }}bootstrap(App);

具体咱们看这一部分

ngAfterViewInit(){        let Div = this.elementRef.nativeElement.querySelector('child div');        console.log(Div);        Div.style.backgroundColor = 'red';    }

这里实现了Angular提供的AfterViewInit接口的抽象方法,但是我们并没有显示的实现AfterViewInit接口,直接实现其方法也可以被ts编译器所匹配,这也是ts区别于其他OOP语言的不同之处。

以上的逻辑是可行的。

但是接下来我们还可以进行进一步的优化,比如我们可以用Angular提供的ViewChild装饰器来获取Child DOM对象的引用。
app.ts

import { Child } from './child.component';import { bootstrap } from '@angular/platform-browser-dynamic';import { Component, ViewChild, ElementRef,AfterViewInit} from '@angular/core';@Component({    selector:'app',    template:`<div>        <input #colorIndex type='text' />        <button (click)='changeColor(colorIndex.value)'>change color</button>        <child></child>    </div>`,    directives:[Child]})export class App{    private defaultColor:string;    private targetColor:string;    constructor(){        //默认颜色初始化        this.defaultColor = 'transparent';    }    @ViewChild(Child)    child:Child;    ngAfterViewInit(){        // let Div = this.child.querySelector('child div');        // console.log(Div);        this.child.nativeElement.style.backgroundColor = 'red';        console.log(this.child.nativeElement);    }    //change color    changeColor(value:string):void{        console.log(value);        this.targetColor = this.defaultColor || value;        this.child.nativeElement.style.backgroundColor = value;    }}bootstrap(App);

我们通过@ViewChild获取到了Child的引用,但是现在Child组件是实际没有nativeElement这个属性的。

console.log(this.child.nativeElement); //undefined

我们需要在Child里注入ElementRef,然后声明一个nativeElement成员变量。
child.component.ts

import { Component, ElementRef } from '@angular/core';@Component({    selector:"child",    template:`<div></div>`,    styles:[`        div{            width:100px;            height:100px;        }    `]})export class Child{    nativeElement:any;    constructor(private elementRef:ElementRef){    }    ngAfterViewInit(){        this.nativeElement = this.elementRef.nativeElement.querySelector('div');    }}

但是我想我们现在对视图层和应用层的分离做的还不够彻底,没事,Angular为我们提供了一个叫做Renderer的对象,它作为视图层和应用层的粘合剂,能够使我们把视图层和应用层做到最大程度的分离。

改动后的代码如下

import { Child } from './child.component';import { bootstrap } from '@angular/platform-browser-dynamic';import { Component, ViewChild, ElementRef,AfterViewInit,Renderer} from '@angular/core';@Component({    selector:'app',    template:`<div>        <input #colorIndex type='text' />        <button (click)='changeColor(colorIndex.value)'>change color</button>        <child></child>    </div>`,    directives:[Child]})export class App{    private defaultColor:string;    private targetColor:string;    constructor(private renderer:Renderer){        //默认颜色初始化        this.defaultColor = 'transparent';    }    @ViewChild(Child)    child:Child;    ngAfterViewInit(){        // let Div = this.child.querySelector('child div');        // console.log(Div);        // this.child.nativeElement.style.backgroundColor = 'red';        this.renderer.setElementStyle(this.child.nativeElement,"backgroundColor",'red');        console.log(this.child.nativeElement);    }    //change color    changeColor(value:string):void{        console.log(value);        this.targetColor = this.defaultColor || value;        this.renderer.setElementStyle(this.child.nativeElement,"backgroundColor",value);    }}bootstrap(App);

通过注入Renderer,我们可以轻松的操作DOM,并且对视图层和应用的分离做到最大限度。

为此Renderer提供了很多实用的方法

/** * @experimental */export declare abstract class Renderer {    abstract selectRootElement(selectorOrNode: string | any, debugInfo?: RenderDebugInfo): any;    abstract createElement(parentElement: any, name: string, debugInfo?: RenderDebugInfo): any;    abstract createViewRoot(hostElement: any): any;    abstract createTemplateAnchor(parentElement: any, debugInfo?: RenderDebugInfo): any;    abstract createText(parentElement: any, value: string, debugInfo?: RenderDebugInfo): any;    abstract projectNodes(parentElement: any, nodes: any[]): void;    abstract attachViewAfter(node: any, viewRootNodes: any[]): void;    abstract detachView(viewRootNodes: any[]): void;    abstract destroyView(hostElement: any, viewAllNodes: any[]): void;    abstract listen(renderElement: any, name: string, callback: Function): Function;    abstract listenGlobal(target: string, name: string, callback: Function): Function;    abstract setElementProperty(renderElement: any, propertyName: string, propertyValue: any): void;    abstract setElementAttribute(renderElement: any, attributeName: string, attributeValue: string): void;    /**     * Used only in debug mode to serialize property changes to dom nodes as attributes.     */    abstract setBindingDebugInfo(renderElement: any, propertyName: string, propertyValue: string): void;    abstract setElementClass(renderElement: any, className: string, isAdd: boolean): any;    abstract setElementStyle(renderElement: any, styleName: string, styleValue: string): any;    abstract invokeElementMethod(renderElement: any, methodName: string, args?: any[]): any;    abstract setText(renderNode: any, text: string): any;    abstract animate(element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], duration: number, delay: number, easing: string): AnimationPlayer;}

(具体请参考Angular源码)

Angular通过ElementRef实现跨平台,用Renderer实现视图应用相分离,想必也是一种很赞的设计模式。

原创粉丝点击