Angular Renderer使用案例分享

这次给大家带来Angular Renderer使用案例分享,Angular Renderer使用的注意事项有哪些,下面就是实战案例,一起来看一下。

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 将会在渲染组件时通过渲染器执行对应相关的操作,比如,创建元素、设置属性、添加样式和订阅事件等。

Angular Renderer使用案例分享

使用 Renderer

@Component({ selector: 'exe-cmp', template: ` 

Exe Component

`})export class ExeComponent { constructor(private renderer: Renderer2, elRef: ElementRef) { this.renderer.setProperty(elRef.nativeElement, 'author', 'semlinker'); }}

登录后复制

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

TokenKey

// packages/core/src/view/util.tsconst _tokenKeyCache = new Map();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.tsconst 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(); 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);  }  (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;  } } }}

登录后复制

上面代码中的 EmulatedEncapsulationDomRenderer2 和 ShadowDomRenderer 类都继承于 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; }}

登录后复制

介绍完 DomRendererFactory2 和 DefaultDomRenderer2 类,最后我们来看一下 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.tsexport 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 属性值,来创建组件的渲染器。接下来我们先来看一下 NodeDef 、 ElementDef 和 RendererType2 接口定义:

// 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.tsexport 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.tsexport interface RootData { injector: Injector; ngModule: NgModuleRef; projectableNodes: any[][]; selectorOrNode: any; renderer: Renderer2; rendererFactory: RendererFactory2; errorHandler: ErrorHandler; sanitizer: Sanitizer;}

登录后复制

那好,现在问题来了:

什么时候创建 RootData 对象?

怎么创建 RootData 对象?

什么时候创建 RootData 对象?

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

function createProdRootView( elInjector: Injector,  projectableNodes: any[][],  rootSelectorOrNode: string | any, def: ViewDefinition,  ngModule: NgModuleRef,  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,  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 渲染器的相关基础知识已介绍完毕。接下来,我们做一个简单总结:

Angular 应用程序启动时会创建 RootView (生产环境下通过调用 createProdRootView() 方法)

创建 RootView 的过程中,会创建 RootData 对象,该对象可以通过 ViewData 的 root 属性访问到。基于 RootData 对象,我们可以通过 renderer 访问到默认的渲染器,即 DefaultDomRenderer2 实例,此外也可以通过 rendererFactory 访问到 RendererFactory2 实例。

在创建组件视图 (ViewData) 时,会根据 componentRendererType 的属性值,来设置组件关联的 renderer 渲染器。

当渲染组件视图的时候,Angular 会利用该组件关联的 renderer 提供的 API,创建该视图中的节点或执行视图的相关操作,比如创建元素 (createElement)、创建文本 (createText)、设置样式 (setStyle) 和 设置事件监听 (listen) 等。

相信看了本文案例你已经掌握了方法,更多精彩请关注【创想鸟】其它相关文章!

推荐阅读:

使用JS判断字符串中包含内容方法总结

JS+HTML5实绑定鼠标事件的粒子动画

以上就是Angular Renderer使用案例分享的详细内容,更多请关注【创想鸟】其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至253000106@qq.com举报,一经查实,本站将立刻删除。

发布者:PHP中文网,转转请注明出处:https://www.chuangxiangniao.com/p/2752039.html

(0)
上一篇 2025年3月8日 06:06:24
下一篇 2025年3月8日 02:20:16

AD推荐 黄金广告位招租... 更多推荐

相关推荐

发表回复

登录后才能评论