背景

Angular 是支持开发跨平台的应用框架,比如:Web 应用、移动 Web 应用、原生移动应用和原生桌面应用等。

为了能够支持跨平台,Angular 通过抽象层封装了不同平台的差异,统一了 API 接口。

例如定义了Renderer 、RootRenderer 等抽象类。

此外还定义了以下引用类型:ElementRef、TemplateRef、ViewRef 、ComponentRef 和 ViewContainerRef 等。

本文我们主要分析一下 ElementRef 类。

定义

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

作用

在应用层直接操作 DOM,就会造成应用层与渲染层之间强耦合,导致我们的应用无法运行在不同环境,如 web worker 中,因为在 web worker 环境中,是不能直接操作 DOM。

通过 ElementRef 我们就可以封装不同平台下视图层中的 native 元素 (在浏览器环境中,native 元素通常是指 DOM 元素),最后借助于 Angular 提供的强大的依赖注入特性,我们就可以轻松地访问到 native 元素。

应用

我们先来介绍一下整体需求,我们想在页面成功渲染后,获取页面中的 div 元素,并改变该 div 元素的背景颜色。

接下来我们来一步步,实现这个需求。

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

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 = 'Hello Angular';

  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 。

问题解决,但不是很优雅 。Angular 有提供组件生命周期的钩子,我们可以在ngAfterViewInit这个钩子函数中获取我们想要的 div 元素。

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 = 'Hello Angular';

  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 元素。

功能已经实现了,但还有优化的空间。 Angular 有内置的属性装饰器,如 @ContentChild、 @ContentChildren、@ViewChild、@ViewChildren 等。

我们可以使用属性装饰器来获取我们想要的div元素:

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 = 'Hello Angular';

  @ViewChild('greet') greetDiv: ElementRef;

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

上面的代码是不是还有进一步的优化空间,我们看到设置 div 元素的背景,我们是默认应用的运行环境在是浏览器中。

前面已经介绍了,我们要尽量减少应用层与渲染层之间强耦合关系,从而让我们应用能够灵活地运行在不同环境。

我们可以使用Angular提供的Renderer2来实现渲染:

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

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

  @ViewChild('greet') greetDiv: ElementRef;

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

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

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;
}