Maison  >  Article  >  interface Web  >  Partage de cas d'utilisation du moteur de rendu angulaire

Partage de cas d'utilisation du moteur de rendu angulaire

php中世界最好的语言
php中世界最好的语言original
2018-06-01 11:40:531571parcourir

Cette fois, je vais partager avec vous les cas d'utilisation d'Angular Renderer. Quelles sont les précautions lors de l'utilisation d'Angular Renderer. Ce qui suit est un cas pratique, jetons un coup d'œil.

L'un des objectifs de conception d'Angular est de rendre le navigateur indépendant du DOM. Le DOM est complexe, donc en séparer les composants rendra nos applications plus faciles à tester et à refactoriser. Un autre avantage est que grâce à ce découplage, nos applications peuvent fonctionner sur d'autres plateformes (telles que Node.js, WebWorkers, NativeScript, etc.).

Afin de prendre en charge plusieurs plates-formes, Angular encapsule les différences des différentes plates-formes à travers des couches d'abstraction. Par exemple, la classe abstraite Renderer, Renderer2, la classe abstraite RootRenderer, etc. sont définies. De plus, les types de référence suivants sont définis : ElementRef, TemplateRef, ViewRef, ComponentRef, ViewContainerRef, etc.

Le contenu principal de cet article est d'analyser le moteur de rendu en Angular, mais avant de procéder à l'analyse spécifique, introduisons d'abord le concept de plateforme.

Plateforme

Qu'est-ce qu'une plate-forme

Une plate-forme est l'environnement dans lequel les applications s'exécutent . Il s'agit d'un ensemble de services qui peuvent être utilisés pour accéder aux fonctionnalités intégrées de votre application et au framework Angular lui-même. Étant donné qu'Angular est avant tout un framework d'interface utilisateur, l'une des fonctionnalités les plus importantes fournies par la plateforme est le rendu des pages.

Plateforme et application d'amorçage

Avant de commencer à créer un moteur de rendu personnalisé, examinons la configuration de la plate-forme et le démarrage de l'application.

import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {BrowserModule} from '@angular/platform-browser';
@NgModule({
 imports: [BrowserModule],
 bootstrap: [AppCmp]
})
class AppModule {}
platformBrowserDynamic().bootstrapModule(AppModule);

Comme vous pouvez le constater, le processus d'amorçage se compose de deux parties : la création de la plateforme et l'amorçage du module. Dans cet exemple, nous importons le module BrowserModule, qui fait partie de la plateforme du navigateur. Il ne peut y avoir qu'une seule plateforme activée dans l'application, mais nous pouvons l'utiliser pour amorcer plusieurs modules, comme indiqué ci-dessous :

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

Puisqu'il ne peut y avoir qu'une seule plateforme activée dans l'application, le service singleton doit Inscrivez-vous sur la plateforme. Par exemple, le navigateur n'a qu'une seule barre d'adresse et l'objet de service correspondant est un singleton. De plus, comment afficher notre interface utilisateur personnalisée dans le navigateur nécessite l'utilisation du moteur de rendu fourni par Angular.

Renderer

Qu'est-ce qu'un renderer

Le renderer est fourni par Angular pour nous A service intégré qui effectue des opérations de rendu de l'interface utilisateur. Dans un navigateur, le rendu est le processus de mappage d'un modèle à une vue. Les valeurs du modèle peuvent être des types de données primitifs en JavaScript, des objets, des tableaux ou d'autres objets de données. Cependant, les vues peuvent être d'autres éléments tels que des paragraphes, des formulaires, des boutons, etc. sur la page. Ces éléments de page sont représentés en interne par DOM (Document Object Model).

Rendu angulaire

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;
}
A noter que dans la version Angular 4.x+, nous utilisons

au lieu de Renderer2. En observant les classes abstraites liées à Renderer (Renderer, Renderer2), nous avons constaté qu'il existe de nombreuses méthodes abstraites définies dans la classe abstraite, qui sont utilisées pour créer des éléments, du texte, définir des attributs, ajouter des styles et définir l'écoute d'événements, etc. Renderer

Comment fonctionnent les moteurs de rendu

Lors de l'instanciation d'un composant, Angular appelle la méthode

et associe le moteur de rendu qu'il obtient à cette instance de composant. Angular effectuera les opérations correspondantes via le moteur de rendu lors du rendu du composant, telles que la création d'éléments, la définition de propriétés, l'ajout de styles et l'abonnement à des événements. renderComponent()

Utilisation de 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');
 }
}
Dans le code ci-dessus, nous utilisons l'injection de constructeur pour injecter les instances Renderer2 et ElementRef. Certains lecteurs peuvent se demander comment l'objet instance injecté est généré. Nous ne présentons ici que brièvement les connaissances pertinentes et n'entrerons pas dans les détails. Le code spécifique est le suivant :

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. Lors du rendu d'une vue de composant, Angular utilisera l'API fournie par renderer associée au composant pour créer des nœuds dans la vue ou effectuer des opérations liées à la vue, telles que la création d'éléments (createElement), Créez du texte (createText), définissez le style (setStyle) et définissez l'écoute des événements (écouter), etc.

Je pense que vous maîtrisez la méthode après avoir lu le cas dans cet article. Pour des informations plus intéressantes, veuillez prêter attention aux autres articles connexes sur le site Web chinois de php !

Lecture recommandée :

Un résumé de la méthode d'utilisation de JS pour déterminer le contenu contenu dans une chaîne

JS + Liaison de souris réelle HTML5 Animation de particules d'événements

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn