当前位置: 移动技术网 > IT编程>脚本编程>AngularJs > Angular Renderer (渲染器)的具体使用

Angular Renderer (渲染器)的具体使用

2019年05月28日  | 移动技术网IT编程  | 我要评论
angular 其中的一个设计目标是使浏览器与 dom 独立。dom 是复杂的,因此使组件与它分离,会让我们的应用程序,更容易测试与重构。另外的好处是,由于这种解耦,使得我

angular 其中的一个设计目标是使浏览器与 dom 独立。dom 是复杂的,因此使组件与它分离,会让我们的应用程序,更容易测试与重构。另外的好处是,由于这种解耦,使得我们的应用能够运行在其它平台 (比如:node.js、webworkers、nativescript 等)。

为了能够支持跨平台,angular 通过抽象层封装了不同平台的差异。比如定义了抽象类 renderer、renderer2 、抽象类 rootrenderer 等。此外还定义了以下引用类型:elementref、templateref、viewref 、componentref 和 viewcontainerref 等。

本文的主要内容是分析 angular 中 renderer (渲染器),不过在进行具体分析前,我们先来介绍一下平台的概念。

平台

什么是平台

平台是应用程序运行的环境。它是一组服务,可以用来访问你的应用程序和 angular 框架本身的内置功能。由于angular 主要是一个 ui 框架,平台提供的最重要的功能之一就是页面渲染。

平台和引导应用程序

在我们开始构建一个自定义渲染器之前,我们来看一下如何设置平台,以及引导应用程序。

import {platformbrowserdynamic} from '@angular/platform-browser-dynamic';
import {browsermodule} from '@angular/platform-browser';

@ngmodule({
 imports: [browsermodule],
 bootstrap: [appcmp]
})
class appmodule {}

platformbrowserdynamic().bootstrapmodule(appmodule);

如你所见,引导过程由两部分组成:创建平台和引导模块。在这个例子中,我们导入 browsermodule 模块,它是浏览器平台的一部分。应用中只能有一个激活的平台,但是我们可以利用它来引导多个模块,如下所示:

const platformref: platformref = platformbrowserdynamic();
platformref.bootstrapmodule(appmodule1);
platformref.bootstrapmodule(appmodule2);

由于应用中只能有一个激活的平台,单例的服务必须在该平台中注册。比如,浏览器只有一个地址栏,对应的服务对象就是单例。此外如何让我们自定义的 ui 界面,能够在浏览器中显示出来呢,这就需要使用 angular 为我们提供的渲染器。

渲染器

什么是渲染器

渲染器是 angular 为我们提供的一种内置服务,用于执行 ui 渲染操作。在浏览器中,渲染是将模型映射到视图的过程。模型的值可以是 javascript 中的原始数据类型、对象、数组或其它的数据对象。然而视图可以是页面中的段落、表单、按钮等其他元素,这些页面元素内部使用 dom (document object model) 来表示。

angular renderer

rootrenderer

export abstract class rootrenderer {
 abstract rendercomponent(componenttype: rendercomponenttype): renderer;
}

renderer

/**
 * @deprecated use the `renderer2` instead.
 */
export abstract class renderer {
 abstract createelement(parentelement: any, name: string, 
 debuginfo?: renderdebuginfo): any;
 abstract createtext(parentelement: any, value: string, 
 debuginfo?: renderdebuginfo): any;
 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;
 // ...
}

renderer2

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

需要注意的是在 angular 4.x+ 版本,我们使用 renderer2 替代 renderer。通过观察 renderer 相关的抽象类 (renderer、renderer2),我们发现抽象类中定义了很多抽象方法,用来创建元素、文本、设置属性、添加样式和设置事件监听等。

渲染器如何工作

在实例化一个组件时,angular 会调用 rendercomponent() 方法并将其获取的渲染器与该组件实例相关联。angular 将会在渲染组件时通过渲染器执行对应相关的操作,比如,创建元素、设置属性、添加样式和订阅事件等。

使用 renderer

@component({
 selector: 'exe-cmp',
 template: `
 <h3>exe component</h3>
 `
})
export class execomponent {
 constructor(private renderer: renderer2, elref: elementref) {
 this.renderer.setproperty(elref.nativeelement, 'author', 'semlinker');
 }
}

以上代码中,我们利用构造注入的方式,注入 renderer2 和 elementref 实例。有些读者可能会问,注入的实例对象是怎么生成的。这里我们只是稍微介绍一下相关知识,并不会详细展开。具体代码如下:

tokenkey

// packages/core/src/view/util.ts
const _tokenkeycache = new map<any, string>();
export function tokenkey(token: any): string {
 let key = _tokenkeycache.get(token);
 if (!key) {
 key = stringify(token) + '_' + _tokenkeycache.size;
 _tokenkeycache.set(token, key);
 }
 return key;
}

// packages/core/src/view/provider.ts
const rendererv1tokenkey = tokenkey(rendererv1);
const renderer2tokenkey = tokenkey(renderer2);
const elementreftokenkey = tokenkey(elementref);
const viewcontainerreftokenkey = tokenkey(viewcontainerref);
const templatereftokenkey = tokenkey(templateref);
const changedetectorreftokenkey = tokenkey(changedetectorref);
const injectorreftokenkey = tokenkey(injector);

resolvedep()

export function resolvedep(
 view: viewdata, eldef: nodedef, 
 allowprivateservices: boolean, depdef: depdef,
 notfoundvalue: any = injector.throw_if_not_found): any {
 const tokenkey = depdef.tokenkey;
 // ...
 while (view) {
 if (eldef) {
  switch (tokenkey) {
  case rendererv1tokenkey: { // tokenkey(rendererv1)
   const compview = findcompview(view, eldef, allowprivateservices);
   return createrendererv1(compview);
  }
  case renderer2tokenkey: { // tokenkey(renderer2)
   const compview = findcompview(view, eldef, allowprivateservices);
   return compview.renderer;
  }
  case elementreftokenkey: // tokenkey(elementref)
   return new elementref(aselementdata(view, eldef.index).renderelement);
   // ... 此外还包括:viewcontainerreftokenkey、templatereftokenkey、
  // changedetectorreftokenkey 等
  }
 }
 }
 // ...
}

通过以上代码,我们发现当我们在组件类的构造函数中声明相应的依赖对象时,如 renderer2 和 elementref,angular 内部会调用 resolvedep() 方法,实例化 token 对应依赖对象。

在大多数情况下,我们开发的 angular 应用程序是运行在浏览器平台,接下来我们来了解一下该平台下的默认渲染器 - defaultdomrenderer2。

defaultdomrenderer2

在浏览器平台下,我们可以通过调用 domrendererfactory2 工厂,根据不同的视图封装方案,创建对应渲染器。

domrendererfactory2

// packages/platform-browser/src/dom/dom_renderer.ts
@injectable()
export class domrendererfactory2 implements rendererfactory2 {
 private rendererbycompid = new map<string, renderer2>();
 private defaultrenderer: renderer2;

 constructor(
 private eventmanager: eventmanager, 
 private sharedstyleshost: domsharedstyleshost) {
 // 创建默认的dom渲染器
 this.defaultrenderer = new defaultdomrenderer2(eventmanager);
 };

 createrenderer(element: any, type: renderertype2|null): renderer2 {
 if (!element || !type) {
  return this.defaultrenderer;
 }
 // 根据不同的视图封装方案,创建不同的渲染器
 switch (type.encapsulation) {
  // 无 shadow dom,但是通过 angular 提供的样式包装机制来封装组件,
  // 使得组件的样式不受外部影响,这是 angular 的默认设置。
  case viewencapsulation.emulated: {
  let renderer = this.rendererbycompid.get(type.id);
  if (!renderer) {
   renderer =
    new emulatedencapsulationdomrenderer2(this.eventmanager, 
     this.sharedstyleshost, type);
   this.rendererbycompid.set(type.id, renderer);
  }
  (<emulatedencapsulationdomrenderer2>renderer).applytohost(element);
  return renderer;
  }
  // 使用原生的 shadow dom 特性 
  case viewencapsulation.native:
  return new shadowdomrenderer(this.eventmanager, 
   this.sharedstyleshost, element, type);
  // 无 shadow dom,并且也无样式包装
  default: {
  // ...
  return this.defaultrenderer;
  }
 }
 }
}

上面代码中的 emulatedencapsulationdomrenderer2shadowdomrenderer 类都继承于 defaultdomrenderer2 类,接下来我们再来看一下 defaultdomrenderer2 类的内部实现:

class defaultdomrenderer2 implements renderer2 { 
 constructor(private eventmanager: eventmanager) {}

 // 省略 renderer2 抽象类中定义的其它方法
 createelement(name: string, namespace?: string): any {
 if (namespace) {
  return document.createelementns(namespace_uris[namespace], name);
 }
 return document.createelement(name);
 }

 createcomment(value: string): any { return document.createcomment(value); }

 createtext(value: string): any { return document.createtextnode(value); }

 addclass(el: any, name: string): void { el.classlist.add(name); }

 setstyle(el: any, style: string, value: any, flags: rendererstyleflags2): void {
 if (flags & rendererstyleflags2.dashcase) {
  el.style.setproperty(
   style, value, !!(flags & rendererstyleflags2.important) ? 'important' : '');
 } else {
  el.style[style] = value;
 }
 }

 listen(
 target: 'window'|'document'|'body'|any, 
 event: string, 
 callback: (event: any) => boolean):
  () => void {
 checknosyntheticprop(event, 'listener');
 if (typeof target === 'string') {
  return <() => void>this.eventmanager.addglobaleventlistener(
   target, event, decoratepreventdefault(callback));
 }
 return <() => void>this.eventmanager.addeventlistener(
   target, event, decoratepreventdefault(callback)) as() => void;
 }
}

介绍完 domrendererfactory2defaultdomrenderer2 类,最后我们来看一下 angular 内部如何利用它们。

domrendererfactory2 内部应用

browsermodule

// packages/platform-browser/src/browser.ts
@ngmodule({
 providers: [
 // 配置 domrendererfactory2 和 rendererfactory2 provider
 domrendererfactory2,
 {provide: rendererfactory2, useexisting: domrendererfactory2},
 // ...
 ],
 exports: [commonmodule, applicationmodule]
})
export class browsermodule {
 constructor(@optional() @skipself() parentmodule: browsermodule) {
 // 用于判断应用中是否已经导入browsermodule模块
 if (parentmodule) {
  throw new error(
  `browsermodule has already been loaded. if you need access to common 
  directives such as ngif and ngfor from a lazy loaded module, 
  import commonmodule instead.`);
 }
 }
}

createcomponentview()

// packages/core/src/view/view.ts
export function createcomponentview(
 parentview: viewdata, 
 nodedef: nodedef, 
 viewdef: viewdefinition, 
 hostelement: any): viewdata {
 const renderertype = nodedef.element !.componentrenderertype; // 步骤一
 let comprenderer: renderer2;
 if (!renderertype) { // 步骤二
 comprenderer = parentview.root.renderer;
 } else {
 comprenderer = parentview.root.rendererfactory
  .createrenderer(hostelement, renderertype);
 }
 
 return createview(
 parentview.root, comprenderer, parentview, 
  nodedef.element !.componentprovider, viewdef);
}

步骤一

当 angular 在创建组件视图时,会根据 nodedef.element 对象的 componentrenderertype 属性值,来创建组件的渲染器。接下来我们先来看一下 nodedefelementdefrenderertype2 接口定义:

// packages/core/src/view/types.ts
// 视图中节点的定义
export interface nodedef {
 bindingindex: number;
 bindings: bindingdef[];
 bindingflags: bindingflags;
 outputs: outputdef[];
 element: elementdef|null; // nodedef.element
 provider: providerdef|null;
 // ...
}

// 元素的定义
export interface elementdef {
 name: string|null;
 attrs: [string, string, string][]|null;
 template: viewdefinition|null;
 componentprovider: nodedef|null;
 // 设置组件渲染器的类型
 componentrenderertype: renderertype2|null; // nodedef.element.componentrenderertype
 componentview: viewdefinitionfactory|null;
 handleevent: elementhandleeventfn|null;
 // ...
}

// packages/core/src/render/api.ts
// renderertype2 接口定义
export interface renderertype2 {
 id: string;
 encapsulation: viewencapsulation; // emulated、native、none
 styles: (string|any[])[];
 data: {[kind: string]: any};
}

步骤二

获取 componentrenderertype 的属性值后,如果该值为 null 的话,则直接使用 parentview.root 属性值对应的 renderer 对象。若该值不为空,则调用 parentview.root 对象的 rendererfactory() 方法创建 renderer 对象。

通过上面分析,我们发现不管走哪条分支,我们都需要使用 parentview.root 对象,然而该对象是什么特殊对象?我们发现 parentview 的数据类型是 viewdata ,该数据接口定义如下:

// packages/core/src/view/types.ts
export interface viewdata {
 def: viewdefinition;
 root: rootdata;
 renderer: renderer2;
 nodes: {[key: number]: nodedata};
 state: viewstate;
 oldvalues: any[];
 disposables: disposablefn[]|null;
 // ...
}

通过 viewdata 的接口定义,我们终于发现了 parentview.root 的属性类型,即 rootdata

// packages/core/src/view/types.ts
export interface rootdata {
 injector: injector;
 ngmodule: ngmoduleref<any>;
 projectablenodes: any[][];
 selectorornode: any;
 renderer: renderer2;
 rendererfactory: rendererfactory2;
 errorhandler: errorhandler;
 sanitizer: sanitizer;
}

那好,现在问题来了:

  1. 什么时候创建 rootdata 对象?
  2. 怎么创建 rootdata 对象?

什么时候创建 rootdata 对象?

当创建根视图的时候会创建 rootdata,在开发环境会调用 debugcreaterootview() 方法创建 rootview,而在生产环境会调用 createprodrootview() 方法创建 rootview。简单起见,我们只分析 createprodrootview() 方法:

function createprodrootview(
 elinjector: injector, 
 projectablenodes: any[][], 
 rootselectorornode: string | any,
 def: viewdefinition, 
 ngmodule: ngmoduleref<any>, 
 context?: any): viewdata {
 /** rendererfactory2 provider 配置
 * domrendererfactory2,
 * {provide: rendererfactory2, useexisting: domrendererfactory2},
 */
 const rendererfactory: rendererfactory2 = ngmodule.injector.get(rendererfactory2);
  
 return createrootview(
  createrootdata(elinjector, ngmodule, rendererfactory,
  projectablenodes, rootselectorornode),
  def, context);
}

// 创建根视图
export function createrootview(root: rootdata, def: viewdefinition, 
 context?: any): viewdata {
 // 创建viewdata对象
 const view = createview(root, root.renderer, null, null, def);
 initview(view, context, context);
 createviewnodes(view);
 return view;
}

上面代码中,当创建 rootview 的时候,会调用 createrootdata() 方法创建 rootdata 对象。最后一步就是分析 createrootdata() 方法。

怎么创建 rootdata 对象?

通过上面分析,我们知道通过 createrootdata() 方法,来创建 rootdata 对象。createrootdata() 方法具体实现如下:

function createrootdata(
 elinjector: injector, 
 ngmodule: ngmoduleref<any>, 
 rendererfactory: rendererfactory2,
 projectablenodes: any[][], 
 rootselectorornode: any): rootdata {
 const sanitizer = ngmodule.injector.get(sanitizer);
 const errorhandler = ngmodule.injector.get(errorhandler);
 // 创建rootrenderer
 const renderer = rendererfactory.createrenderer(null, null); 
 return {
 ngmodule,
 injector: elinjector,
 projectablenodes,
 selectorornode: rootselectorornode, 
 sanitizer, 
 rendererfactory, 
 renderer,
 errorhandler
 };
}

此时浏览器平台下, renderer 渲染器的相关基础知识已介绍完毕。接下来,我们做一个简单总结:

  1. angular 应用程序启动时会创建 rootview (生产环境下通过调用 createprodrootview() 方法)
  2. 创建 rootview 的过程中,会创建 rootdata 对象,该对象可以通过 viewdata 的 root 属性访问到。基于 rootdata 对象,我们可以通过 renderer 访问到默认的渲染器,即 defaultdomrenderer2 实例,此外也可以通过 rendererfactory 访问到 rendererfactory2 实例。
  3. 在创建组件视图 (viewdata) 时,会根据 componentrenderertype 的属性值,来设置组件关联的 renderer 渲染器。
  4. 当渲染组件视图的时候,angular 会利用该组件关联的 renderer 提供的 api,创建该视图中的节点或执行视图的相关操作,比如创建元素 (createelement)、创建文本 (createtext)、设置样式 (setstyle) 和 设置事件监听 (listen) 等。

后面如果有时间的话,我们会介绍如何自定义渲染器,有兴趣的读者,可以先查阅 "参考资源" 中的链接。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如您对本文有疑问或者有任何想说的,请 点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网