最新文章專題視頻專題問答1問答10問答100問答1000問答2000關鍵字專題1關鍵字專題50關鍵字專題500關鍵字專題1500TAG最新視頻文章推薦1 推薦3 推薦5 推薦7 推薦9 推薦11 推薦13 推薦15 推薦17 推薦19 推薦21 推薦23 推薦25 推薦27 推薦29 推薦31 推薦33 推薦35 推薦37視頻文章20視頻文章30視頻文章40視頻文章50視頻文章60 視頻文章70視頻文章80視頻文章90視頻文章100視頻文章120視頻文章140 視頻2關鍵字專題關鍵字專題tag2tag3文章專題文章專題2文章索引1文章索引2文章索引3文章索引4文章索引5123456789101112131415文章專題3
問答文章1 問答文章501 問答文章1001 問答文章1501 問答文章2001 問答文章2501 問答文章3001 問答文章3501 問答文章4001 問答文章4501 問答文章5001 問答文章5501 問答文章6001 問答文章6501 問答文章7001 問答文章7501 問答文章8001 問答文章8501 問答文章9001 問答文章9501
當前位置: 首頁 - 科技 - 知識百科 - 正文

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

來源:懂視網 責編:小采 時間:2020-11-27 22:15:19
文檔

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

Angular Renderer (渲染器)的具體使用:Angular 其中的一個設計目標是使瀏覽器與 DOM 獨立。DOM 是復雜的,因此使組件與它分離,會讓我們的應用程序,更容易測試與重構。另外的好處是,由于這種解耦,使得我們的應用能夠運行在其它平臺 (比如:Node.js、WebWorkers、NativeScript 等
推薦度:
導讀Angular Renderer (渲染器)的具體使用:Angular 其中的一個設計目標是使瀏覽器與 DOM 獨立。DOM 是復雜的,因此使組件與它分離,會讓我們的應用程序,更容易測試與重構。另外的好處是,由于這種解耦,使得我們的應用能夠運行在其它平臺 (比如:Node.js、WebWorkers、NativeScript 等

Angular 其中的一個設計目標是使瀏覽器與 DOM 獨立。DOM 是復雜的,因此使組件與它分離,會讓我們的應用程序,更容易測試與重構。另外的好處是,由于這種解耦,使得我們的應用能夠運行在其它平臺 (比如:Node.js、WebWorkers、NativeScript 等)。

為了能夠支持跨平臺,Angular 通過抽象層封裝了不同平臺的差異。比如定義了抽象類 Renderer、Renderer2 、抽象類 RootRenderer 等。此外還定義了以下引用類型:ElementRef、TemplateRef、ViewRef 、ComponentRef 和 ViewContainerRef 等。

本文的主要內容是分析 Angular 中 Renderer (渲染器),不過在進行具體分析前,我們先來介紹一下平臺的概念。

平臺

什么是平臺

平臺是應用程序運行的環(huán)境。它是一組服務,可以用來訪問你的應用程序和 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);

如你所見,引導過程由兩部分組成:創(chuàng)建平臺和引導模塊。在這個例子中,我們導入 BrowserModule 模塊,它是瀏覽器平臺的一部分。應用中只能有一個激活的平臺,但是我們可以利用它來引導多個模塊,如下所示:

const platformRef: PlatformRef = platformBrowserDynamic();
platformRef.bootstrapModule(AppModule1);
platformRef.bootstrapModule(AppModule2);

由于應用中只能有一個激活的平臺,單例的服務必須在該平臺中注冊。比如,瀏覽器只有一個地址欄,對應的服務對象就是單例。此外如何讓我們自定義的 UI 界面,能夠在瀏覽器中顯示出來呢,這就需要使用 Angular 為我們提供的渲染器。

渲染器

什么是渲染器

渲染器是 Angular 為我們提供的一種內置服務,用于執(zhí)行 UI 渲染操作。在瀏覽器中,渲染是將模型映射到視圖的過程。模型的值可以是 JavaScript 中的原始數(shù)據類型、對象、數(shù)組或其它的數(shù)據對象。然而視圖可以是頁面中的段落、表單、按鈕等其他元素,這些頁面元素內部使用 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),我們發(fā)現(xiàn)抽象類中定義了很多抽象方法,用來創(chuàng)建元素、文本、設置屬性、添加樣式和設置事件監(jiān)聽等。

渲染器如何工作

在實例化一個組件時,Angular 會調用 renderComponent() 方法并將其獲取的渲染器與該組件實例相關聯(lián)。Angular 將會在渲染組件時通過渲染器執(zhí)行對應相關的操作,比如,創(chuàng)建元素、設置屬性、添加樣式和訂閱事件等。

使用 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 等
 }
 }
 }
 // ...
}

通過以上代碼,我們發(fā)現(xiàn)當我們在組件類的構造函數(shù)中聲明相應的依賴對象時,如 Renderer2 和 ElementRef,Angular 內部會調用 resolveDep() 方法,實例化 Token 對應依賴對象。

在大多數(shù)情況下,我們開發(fā)的 Angular 應用程序是運行在瀏覽器平臺,接下來我們來了解一下該平臺下的默認渲染器 - DefaultDomRenderer2。

DefaultDomRenderer2

在瀏覽器平臺下,我們可以通過調用 DomRendererFactory2 工廠,根據不同的視圖封裝方案,創(chuàng)建對應渲染器。

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) {
 // 創(chuàng)建默認的DOM渲染器
 this.defaultRenderer = new DefaultDomRenderer2(eventManager);
 };

 createRenderer(element: any, type: RendererType2|null): Renderer2 {
 if (!element || !type) {
 return this.defaultRenderer;
 }
 // 根據不同的視圖封裝方案,創(chuàng)建不同的渲染器
 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 類的內部實現(xiàn):

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 在創(chuàng)建組件視圖時,會根據 nodeDef.element 對象的 componentRendererType 屬性值,來創(chuàng)建組件的渲染器。接下來我們先來看一下 NodeDef 、 ElementDefRendererType2 接口定義:

// packages/core/src/view/types.ts
// 視圖中節(jié)點的定義
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() 方法創(chuàng)建 renderer 對象。

通過上面分析,我們發(fā)現(xiàn)不管走哪條分支,我們都需要使用 parentView.root 對象,然而該對象是什么特殊對象?我們發(fā)現(xiàn) parentView 的數(shù)據類型是 ViewData ,該數(shù)據接口定義如下:

// 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 的接口定義,我們終于發(fā)現(xiàn)了 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;
}

那好,現(xiàn)在問題來了:

  1. 什么時候創(chuàng)建 RootData 對象?
  2. 怎么創(chuàng)建 RootData 對象?

什么時候創(chuàng)建 RootData 對象?

當創(chuàng)建根視圖的時候會創(chuàng)建 RootData,在開發(fā)環(huán)境會調用 debugCreateRootView() 方法創(chuàng)建 RootView,而在生產環(huán)境會調用 createProdRootView() 方法創(chuàng)建 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);
}

// 創(chuàng)建根視圖
export function createRootView(root: RootData, def: ViewDefinition, 
 context?: any): ViewData {
 // 創(chuàng)建ViewData對象
 const view = createView(root, root.renderer, null, null, def);
 initView(view, context, context);
 createViewNodes(view);
 return view;
}

上面代碼中,當創(chuàng)建 RootView 的時候,會調用 createRootData() 方法創(chuàng)建 RootData 對象。最后一步就是分析 createRootData() 方法。

怎么創(chuàng)建 RootData 對象?

通過上面分析,我們知道通過 createRootData() 方法,來創(chuàng)建 RootData 對象。createRootData() 方法具體實現(xiàn)如下:

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);
 // 創(chuàng)建RootRenderer
 const renderer = rendererFactory.createRenderer(null, null); 
 return {
 ngModule,
 injector: elInjector,
 projectableNodes,
 selectorOrNode: rootSelectorOrNode, 
 sanitizer, 
 rendererFactory, 
 renderer,
 errorHandler
 };
}

此時瀏覽器平臺下, Renderer 渲染器的相關基礎知識已介紹完畢。接下來,我們做一個簡單總結:

  1. Angular 應用程序啟動時會創(chuàng)建 RootView (生產環(huán)境下通過調用 createProdRootView() 方法)
  2. 創(chuàng)建 RootView 的過程中,會創(chuàng)建 RootData 對象,該對象可以通過 ViewData 的 root 屬性訪問到?;?RootData 對象,我們可以通過 renderer 訪問到默認的渲染器,即 DefaultDomRenderer2 實例,此外也可以通過 rendererFactory 訪問到 RendererFactory2 實例。
  3. 在創(chuàng)建組件視圖 (ViewData) 時,會根據 componentRendererType 的屬性值,來設置組件關聯(lián)的 renderer 渲染器。
  4. 當渲染組件視圖的時候,Angular 會利用該組件關聯(lián)的 renderer 提供的 API,創(chuàng)建該視圖中的節(jié)點或執(zhí)行視圖的相關操作,比如創(chuàng)建元素 (createElement)、創(chuàng)建文本 (createText)、設置樣式 (setStyle) 和 設置事件監(jiān)聽 (listen) 等。

后面如果有時間的話,我們會介紹如何自定義渲染器,有興趣的讀者,可以先查閱 "參考資源" 中的鏈接。

聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯(lián)系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com

文檔

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

Angular Renderer (渲染器)的具體使用:Angular 其中的一個設計目標是使瀏覽器與 DOM 獨立。DOM 是復雜的,因此使組件與它分離,會讓我們的應用程序,更容易測試與重構。另外的好處是,由于這種解耦,使得我們的應用能夠運行在其它平臺 (比如:Node.js、WebWorkers、NativeScript 等
推薦度:
  • 熱門焦點

最新推薦

猜你喜歡

熱門推薦

專題
Top