Home>Article>Web Front-end> Detailed explanation of using Angular Renderer
This time I will bring you a detailed explanation of the use of the Angular Renderer renderer. What are theprecautionswhen using the Angular Renderer renderer? The following is a practical case, let's take a look.
One of Angular’s design goals is to make the browser and DOM independent. The DOM is complex, so separating components from it will make our applications easier to test and refactor. Another benefit is that due to this decoupling, our applications can run on other platforms (such as Node.js, WebWorkers, NativeScript, etc.).
In order to support cross-platform, Angular encapsulates the differences of different platforms through an abstraction layer. For example,Abstract classRenderer, Renderer2, abstract class RootRenderer, etc. are defined. In addition, the following reference types are defined: ElementRef, TemplateRef, ViewRef, ComponentRef, ViewContainerRef, etc.
The main content of this article is to analyze the Renderer in Angular, but before carrying out the specific analysis, let us first introduce the concept of the platform.
Platform
What is a platform
A platform is the environment in which an application runs. It is a set of services that can be used to access the built-in functionality of your application and the Angular framework itself. Since Angular is primarily a UI framework, one of the most important features provided by the platform is page rendering.
Platform and Bootstrap Application
Before we start building a custom renderer, let’s take a look at how to set up the platform, and bootstrap the application.
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {BrowserModule} from '@angular/platform-browser'; @NgModule({ imports: [BrowserModule], bootstrap: [AppCmp] }) class AppModule {} platformBrowserDynamic().bootstrapModule(AppModule);
As you can see, the boot process consists of two parts: creating the platform and booting the module. In this example, we import the BrowserModule module, which is part of the browser platform. There can only be one activated platform in the application, but we can use it to boot multiple modules, as shown below:
const platformRef: PlatformRef = platformBrowserDynamic(); platformRef.bootstrapModule(AppModule1); platformRef.bootstrapModule(AppModule2);
Since there can only be one activated platform in the application, the singleton service must be in that Register in the platform. For example, the browser has only one address bar, and the corresponding service object is a singleton. In addition, how to make our customized UI interface display in the browser requires the use of the renderer provided by Angular.
Renderer
What is a renderer
The renderer is provided by Angular for us A built-in service that performs UI rendering operations. In a browser, rendering is the process of mapping a model to a view. Model values can be primitivedata types, objects, arrays, or other data objects inJavaScript. However, views can be other elements such as paragraphs, forms, buttons, etc. on the page. These page elements are represented internally by 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; }
It should be noted that in Angular For version 4.x, we useRenderer2
instead ofRenderer
. By observing the abstract classes related to Renderer (Renderer, Renderer2), we found that there are many abstract methods defined in the abstract class, which are used to create elements, text, set attributes, add styles, and set event listening, etc.
How the renderer works
When instantiating a component, Angular will call therenderComponent()
method and get the renderer with This component instance is associated with. Angular will perform corresponding operations through the renderer when rendering the component, such as creating elements, setting properties, adding styles, and subscribing to events.
Using Renderer
@Component({ selector: 'exe-cmp', template: `Exe Component
` }) export class ExeComponent { constructor(private renderer: Renderer2, elRef: ElementRef) { this.renderer.setProperty(elRef.nativeElement, 'author', 'semlinker'); } }
In the above code, we use constructor injection to inject Renderer2 and ElementRef instances. Some readers may ask how the injected instance object is generated. Here we only briefly introduce the relevant knowledge and will not go into details. The specific code is as follows:
TokenKey
// packages/core/src/view/util.ts const _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.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(); 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.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
属性值,来创建组件的渲染器。接下来我们先来看一下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.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; 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
渲染器。
When rendering a component view, Angular will use the API provided by therenderer
associated with the component to create nodes in the view or perform view-related operations, such as creating elements. (createElement), create text (createText), set style (setStyle) and set event listening (listen), etc.
I believe you have mastered the method after reading the case in this article. For more exciting information, please pay attention to other related articles on the php Chinese website!
Recommended reading:
Detailed explanation of the advantages and disadvantages of using Webpack path and publicPath
JS HTML5 makes mouse-bound particle flow Animation
The above is the detailed content of Detailed explanation of using Angular Renderer. For more information, please follow other related articles on the PHP Chinese website!