Angular 4 ElementRef

Angular 的口号是 - "一套框架,多种平台。同时适用手机与桌面 (One framework.Mobile & desktop.)",即 Angular 是支持开发跨平台的应用,好比:Web 应用、移动 Web 应用、原生移动应用和原生桌面应用等。node

为了可以支持跨平台,Angular 经过抽象层封装了不一样平台的差别,统一了 API 接口。如定义了抽象类 Renderer 、抽象类 RootRenderer 等。此外还定义了如下引用类型:ElementRef、TemplateRef、ViewRef 、ComponentRef 和 ViewContainerRef 等。下面咱们就来分析一下 ElementRef 类:web

ElementRef的做用

在应用层直接操做 DOM,就会形成应用层与渲染层之间强耦合,致使咱们的应用没法运行在不一样环境,如 web worker 中,由于在 web worker 环境中,是不能直接操做 DOM。有兴趣的读者,能够阅读一下 Web Workers 中支持的类和方法 这篇文章。经过 ElementRef 咱们就能够封装不一样平台下视图层中的 native 元素 (在浏览器环境中,native 元素一般是指 DOM 元素),最后借助于 Angular 提供的强大的依赖注入特性,咱们就能够轻松地访问到 native 元素。segmentfault

ElementRef的定义

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

ElementRef的应用

咱们先来介绍一下总体需求,咱们想在页面成功渲染后,获取页面中的 div 元素,并改变该 div 元素的背景颜色。接下来咱们来一步步,实现这个需求。浏览器

首先咱们要先获取 div 元素,在文中 "ElementRef 的做用" 部分,咱们已经提到能够利用 Angular 提供的强大的依赖注入特性,获取封装后的 native 元素。在浏览器中 native 元素就是 DOM 元素,咱们只要先获取 my-app元素,而后利用 querySelector API 就能获取页面中 div 元素。具体代码以下:app

import { Component, ElementRef } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <div>Hello {{ name }}</div>
  `,
})
export class AppComponent {

  name: string = 'Semlinker';

  constructor(private elementRef: ElementRef) {
    let divEle = this.elementRef.nativeElement.querySelector('div');
    console.dir(divEle);
  }
}

运行上面代码,在控制台中没有出现异常,可是输出的结果倒是 null 。什么状况 ? 没有抛出异常,咱们能够推断 this.elementRef.nativeElement 这个对象是存在,但却找不到它的子元素,那应该是在调用构造函数的时候,my-app 元素下的子元素还未建立。那怎么解决这个问题呢 ?沉思中… ,不是有 setTimeout 么,咱们在稍微改造一下:框架

constructor(private elementRef: ElementRef) {
  setTimeout(() => { // 此处须要使用箭头函数哈,你懂的...
      let divEle = this.elementRef.nativeElement.querySelector('div');
      console.dir(divEle);
   }, 0);
}

更新一下代码,此时控制台成功输出了 div 。为何添加个 setTimeout 就能成功获取到想要的 div 元素呢?此处就不展开了,有兴趣的读者能够参考 - What the heck is the event loop anyway? 这个演讲的示例。函数

问题解决了,但感受不是很优雅 ?有没有更好的方案,答案是确定的。Angular 不是有提供组件生命周期的钩子,咱们能够选择一个合适的时机,而后获取咱们想要的 div 元素。oop

import { Component, ElementRef, AfterViewInit } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <div>Hello {{ name }}</div>
  `,
})
export class AppComponent {

  name: string = 'Semlinker';

  // 在构造函数中 this.elementRef = elementRef 是可选的,编译时会自动赋值
  // function AppComponent(elementRef) { this.elementRef = elementRef; }
  constructor(private elementRef: ElementRef) { } 

  ngAfterViewInit() { // 模板中的元素已建立完成
    console.dir(this.elementRef.nativeElement.querySelector('div'));
    // let greetDiv: HTMLElement = this.elementRef.nativeElement.querySelector('div'); 
    // greetDiv.style.backgroundColor = 'red';
  }
}

运行一下上面的代码,咱们看到了意料中的 div 元素。咱们直接选用 ngAfterViewInit 这个钩子,不要问我为何,由于它看得最顺眼咯。不过咱们后面也会有专门的文章,详细分析一下 Angular 组件的生命周期。成功取到 div 元素,就剩下的事情就好办了,直接经过 style 对象设置元素的背景颜色。优化

功能虽然已经实现了,但还有优化的空间么?在 Angular 2 Decorators part - 2 文章中咱们有谈到 Angular 内置的属性装饰器,如 @ContentChild、 @ContentChildren、@ViewChild、@ViewChildren 等。相信读者看完后,已经猜到咱们的优化方案了。具体示例以下:this

import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <div #greet>Hello {{ name }}</div>
  `,
})
export class AppComponent {
  name: string = 'Semlinker';

  @ViewChild('greet')
  greetDiv: ElementRef;

  ngAfterViewInit() {
    this.greetDiv.nativeElement.style.backgroundColor = 'red';
  }
}

是否是感受瞬间高大上了,不过先等等,上面的代码是否是还有进一步的优化空间呢 ?咱们看到设置 div 元素的背景,咱们是默认应用的运行环境在是浏览器中。前面已经介绍了,咱们要尽可能减小应用层与渲染层之间强耦合关系,从而让咱们应用可以灵活地运行在不一样环境。最后咱们来看一下,最终优化后的代码:

import { Component, ElementRef, ViewChild, AfterViewInit, Renderer } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <div #greet>Hello {{ name }}</div>
  `,
})
export class AppComponent {
  name: string = 'Semlinker';

  @ViewChild('greet')
  greetDiv: ElementRef;

  constructor(private elementRef: ElementRef, private renderer: Renderer) { }

  ngAfterViewInit() {
    // this.greetDiv.nativeElement.style.backgroundColor  = 'red';
    this.renderer.setElementStyle(this.greetDiv.nativeElement, 'backgroundColor', 'red');
  }
}

最后咱们经过 renderer 对象提供的 API 优雅地设置了 div 元素的背景颜色。

我有话说

1.Renderer API 还有哪些经常使用的方法 ?

export abstract class Renderer {
  // 建立元素
  abstract createElement(parentElement: any, name: string, 
      debugInfo?: RenderDebugInfo): any;
  
  // 建立文本元素
  abstract createText(parentElement: any, value: string, 
      debugInfo?: RenderDebugInfo): any;
      
  // 设置文本
  abstract setText(renderNode: any, text: string): void;
      
  // 设置元素Property
  abstract setElementProperty(renderElement: any, propertyName: string, 
      propertyValue: any): void;
      
  // 设置元素Attribute
  abstract setElementAttribute(renderElement: any, attributeName: string, 
      attributeValue: string): void;    
      
  // 设置元素的Class
  abstract setElementClass(renderElement: any, className: string,
      isAdd: boolean): void;    
      
  // 设置元素的样式
  abstract setElementStyle(renderElement: any, styleName: string, 
      styleValue: string): void;    
}

须要注意的是在 Angular 4.x+ 版本,咱们使用 Renderer2 替代 Renderer (Angular V2)。

2.Renderer2 API 还有哪些经常使用的方法 ?

export abstract class Renderer2 {
  abstract createElement(name: string, namespace?: string|null): any;
  abstract createComment(value: string): any;
  abstract createText(value: string): any;
  abstract setAttribute(el: any, name: string, value: string,
    namespace?: string|null): void;
  abstract removeAttribute(el: any, name: string, namespace?: string|null): void;
  abstract addClass(el: any, name: string): void;
  abstract removeClass(el: any, name: string): void;
  abstract setStyle(el: any, style: string, value: any, 
    flags?: RendererStyleFlags2): void;
  abstract removeStyle(el: any, style: string, flags?: RendererStyleFlags2): void;
  abstract setProperty(el: any, name: string, value: any): void;
  abstract setValue(node: any, value: string): void;
  abstract listen(
      target: 'window'|'document'|'body'|any, eventName: string,
      callback: (event: any) => boolean | void): () => void;
}

总结

本文主要介绍了 ElementRef 的做用和定义,而后经过一个简单的示例,展现了如何一步步优化已有的功能,但愿对初学者能有所启发。