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"external nofollow" target="_blank" href="https://github.com/angular/angular/blob/master/packages/core/src/render/api.ts#L147">Renderer2export abstract class Renderer2 { abstract createElement(name: string, namespace"text-align: center">使用 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; } } } }上面代码中的
EmulatedEncapsulationDomRenderer2
和ShadowDomRenderer
类都继承于DefaultDomRenderer2
类,接下来我们再来看一下 DefaultDomRenderer2 类的内部实现:class DefaultDomRenderer2 implements Renderer2 { constructor(private eventManager: EventManager) {} // 省略 Renderer2 抽象类中定义的其它方法 createElement(name: string, namespace"external nofollow" target="_blank" href="https://github.com/angular/angular/blob/master/packages/platform-browser/src/browser.ts#L79">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<any>; 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<any>, context"htmlcode">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
渲染器的相关基础知识已介绍完毕。接下来,我们做一个简单总结:
- Angular 应用程序启动时会创建 RootView (生产环境下通过调用 createProdRootView() 方法)
- 创建 RootView 的过程中,会创建 RootData 对象,该对象可以通过 ViewData 的 root 属性访问到。基于 RootData 对象,我们可以通过
renderer
访问到默认的渲染器,即 DefaultDomRenderer2 实例,此外也可以通过rendererFactory
访问到RendererFactory2
实例。- 在创建组件视图 (ViewData) 时,会根据
componentRendererType
的属性值,来设置组件关联的renderer
渲染器。- 当渲染组件视图的时候,Angular 会利用该组件关联的
renderer
提供的 API,创建该视图中的节点或执行视图的相关操作,比如创建元素 (createElement)、创建文本 (createText)、设置样式 (setStyle) 和 设置事件监听 (listen) 等。后面如果有时间的话,我们会介绍如何自定义渲染器,有兴趣的读者,可以先查阅 "参考资源" 中的链接。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
DDR爱好者之家 Design By 杰米
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
稳了!魔兽国服回归的3条重磅消息!官宣时间再确认!
昨天有一位朋友在大神群里分享,自己亚服账号被封号之后居然弹出了国服的封号信息对话框。
这里面让他访问的是一个国服的战网网址,com.cn和后面的zh都非常明白地表明这就是国服战网。
而他在复制这个网址并且进行登录之后,确实是网易的网址,也就是我们熟悉的停服之后国服发布的暴雪游戏产品运营到期开放退款的说明。这是一件比较奇怪的事情,因为以前都没有出现这样的情况,现在突然提示跳转到国服战网的网址,是不是说明了简体中文客户端已经开始进行更新了呢?
更新日志
- 凤飞飞《我们的主题曲》飞跃制作[正版原抓WAV+CUE]
- 刘嘉亮《亮情歌2》[WAV+CUE][1G]
- 红馆40·谭咏麟《歌者恋歌浓情30年演唱会》3CD[低速原抓WAV+CUE][1.8G]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[320K/MP3][193.25MB]
- 【轻音乐】曼托凡尼乐团《精选辑》2CD.1998[FLAC+CUE整轨]
- 邝美云《心中有爱》1989年香港DMIJP版1MTO东芝首版[WAV+CUE]
- 群星《情叹-发烧女声DSD》天籁女声发烧碟[WAV+CUE]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[FLAC/分轨][748.03MB]
- 理想混蛋《Origin Sessions》[320K/MP3][37.47MB]
- 公馆青少年《我其实一点都不酷》[320K/MP3][78.78MB]
- 群星《情叹-发烧男声DSD》最值得珍藏的完美男声[WAV+CUE]
- 群星《国韵飘香·贵妃醉酒HQCD黑胶王》2CD[WAV]
- 卫兰《DAUGHTER》【低速原抓WAV+CUE】
- 公馆青少年《我其实一点都不酷》[FLAC/分轨][398.22MB]
- ZWEI《迟暮的花 (Explicit)》[320K/MP3][57.16MB]