先看效果

项目地址
用Angular做一个简单的全局Message组件 - 图1

  1. import { DOCUMENT } from "@angular/common";
  2. import { ApplicationRef, ComponentFactoryResolver, ComponentRef, Inject, Injectable, Injector } from "@angular/core";
  3. import { ComponentPortal } from "./ComponentPortal";
  4. import { messageCointerComponent } from "./message-container.component";
  5. let globalCounter = 0;
  6. @Injectable({
  7. providedIn: 'root',
  8. })
  9. export class messageService {
  10. /** 组件前缀 */
  11. protected componentPrefix = 'message-';
  12. /** Document */
  13. protected _document: Document;
  14. /** 根(host)dom元素 */
  15. private out: any;
  16. /** 已经添加加上的元素 */
  17. public _attachedPortal: any;
  18. /** A function that will permanently dispose this host. */
  19. /** 一个将永久地处置这个host的函数。*/
  20. private _disposeFn!: (() => void) | null;
  21. /** 缓存 */
  22. public mapCache = new Map();
  23. constructor(
  24. public app: ApplicationRef,
  25. @Inject(DOCUMENT) public document: Document,
  26. public _componentFactoryResolver: ComponentFactoryResolver,
  27. public injector: Injector
  28. ) {
  29. this._document = document;
  30. }
  31. /** 获取ID */
  32. protected getInstanceId(): string {
  33. return `${this.componentPrefix}-${globalCounter++}`;
  34. }
  35. /** 固定组件实例 */
  36. public shoeMessage(options: any) {
  37. // 固定组件
  38. // 动态创建组件
  39. let containerInstance: messageCointerComponent = this.withContainer<messageCointerComponent>(messageCointerComponent);
  40. // 订阅组件销毁
  41. containerInstance.destory$.subscribe(() => {
  42. this.destory()
  43. })
  44. // 创建message 内容
  45. containerInstance.create({
  46. ...options,
  47. id: this.getInstanceId()
  48. })
  49. // 触发更新
  50. containerInstance.readyInstances();
  51. return containerInstance;
  52. }
  53. /** 动态创建component */
  54. public show(component: any/* 组件类 */, options: any/** 参数 */) {
  55. let containerInstance = this.withContainer(component);
  56. // containerInstance.name = '哈哈';
  57. containerInstance.readyInstances();
  58. // 创建message 内容
  59. containerInstance.create({
  60. ...options,
  61. id: this.getInstanceId()
  62. })
  63. // 返回
  64. return containerInstance;
  65. }
  66. /** 创建根(host)dom元素且添加到body最后(appendChild) */
  67. private _createHostElement(): HTMLElement {
  68. // 创建div元素作为host
  69. const host = this._document.createElement('div');
  70. // 添加类
  71. host.classList.add('container')
  72. // 获取容器,并将host元素添加进容器元素
  73. this._document.body.appendChild(host);
  74. // 返回host元素
  75. return host;
  76. }
  77. /** 创建根(host)dom元素和赋值给out */
  78. public create() {
  79. // 创建
  80. let out = this._createHostElement();
  81. // 赋值
  82. this.out = out;
  83. // 返回
  84. return out;
  85. }
  86. /** Gets the root HTMLElement for an instantiated component. */
  87. /** 获取实例化组件的根 HTML 元素。*/
  88. private _getComponentRootNode(componentRef: any): HTMLElement {
  89. return componentRef.hostView.rootNodes[0] as HTMLElement;
  90. }
  91. /** 动态创建组件实例 */
  92. public withContainer<T>(ctor: typeof messageCointerComponent) {
  93. // 查看缓存中是否存在当前创建的组件实例
  94. let cache = this.mapCache.get(this.componentPrefix);
  95. if (cache /* 如果有 */) {
  96. return cache; // 直接返回
  97. }
  98. if (!this.out/* 如果没有 */) {
  99. this.create() // 在此函数中调用其他函数,创建和append到body
  100. this.out.style.zIndex = '1010' // 设置层叠样式
  101. }
  102. const componentPortal = new ComponentPortal(ctor, null, this.injector); // 这个类,纯粹就是为了保存一些参数,没有实际意义
  103. const componentRef = this.attachComponentPortal<T>(componentPortal); // 将给定的ComponentPortal附加到DOM元素
  104. this.mapCache.set(this.componentPrefix, componentRef.instance) // 加入缓存
  105. return componentRef.instance; // 返回
  106. }
  107. /**
  108. * Attaches content, given via a Portal, to the overlay.
  109. * If the overlay is configured to have a backdrop, it will be created.
  110. *
  111. * @param portal Portal instance to which to attach the overlay.
  112. * @returns The portal attachment result.
  113. */
  114. /**
  115. * 将通过Portal给定的内容附加到覆盖物上。
  116. * 如果覆盖层被配置为有一个背景,它将被创建。
  117. *
  118. *param portal 要附加到覆盖层的Portal实例。
  119. * @returns 门户附件的结果。
  120. */
  121. attach<T>(portal: ComponentPortal) {
  122. return this.attachComponentPortal<T>(portal);
  123. }
  124. /**
  125. * Attach the given ComponentPortal to DOM element using the ComponentFactoryResolver.
  126. * @param portal Portal to be attached
  127. * @returns Reference to the created component.
  128. */
  129. /**
  130. *使用ComponentFactoryResolver将给定的ComponentPortal附加到DOM元素。
  131. * @param portal 要附加的Portal
  132. * @returns 对创建的组件的引用。
  133. */
  134. attachComponentPortal<T>(portal: ComponentPortal) {
  135. const resolver = portal.componentFactoryResolver || this._componentFactoryResolver;
  136. // 一个简单的注册表,它将 Components 映射到生成的 ComponentFactory 类,
  137. // 该类可用于创建组件的实例。用于获取给定组件类型的工厂,然后使用工厂的
  138. // create() 方法创建该类型的组件。
  139. const componentFactory = resolver.resolveComponentFactory<T>(portal.component);
  140. let componentRef: ComponentRef<T>;
  141. // If the portal specifies a ViewContainerRef, we will use that as the attachment point
  142. // for the component (in terms of Angular's component tree, not rendering).
  143. // When the ViewContainerRef is missing, we use the factory to create the component directly
  144. // and then manually attach the view to the application.
  145. // 如果 门户 指定了一个ViewContainerRef,我们将使用它作为组件的连接点。
  146. // 作为该组件的附着点(就Angular的组件树而言,而不是渲染)。
  147. // 当ViewContainerRef缺失时,我们会使用工厂直接创建组件。
  148. // 然后手动将视图附加到应用程序中。
  149. if (portal.viewContainerRef /* 存在viewContainerRef,则使用它作为组件的连接点 */) {
  150. // componentRef = portal.viewContainerRef.createComponent(
  151. // componentFactory,
  152. // portal.viewContainerRef.length,
  153. // portal.injector || portal.viewContainerRef.injector,
  154. // );
  155. componentRef = portal.viewContainerRef.createComponent(
  156. portal.component,
  157. {
  158. index: portal.viewContainerRef.length,
  159. injector: portal.injector || portal.viewContainerRef.injector
  160. }
  161. );
  162. this.setDisposeFn(() => componentRef.destroy());
  163. } /* viewContainerRef缺失 */ else {
  164. // 我们会使用工厂直接创建组件。
  165. componentRef = componentFactory.create(portal.injector);
  166. // 然后手动将视图附加到应用程序中。
  167. /**
  168. * 附上一个视图,这样它就会被脏检查。
  169. * 当视图被销毁时,它将被自动分离。
  170. * 如果视图已经被附加到一个ViewContainer上,这将被抛出。
  171. */
  172. this.app.attachView(componentRef.hostView);
  173. this.setDisposeFn(() => {
  174. /**
  175. *将一个视图再次从脏检查中分离出来。
  176. */
  177. this.app.detachView(componentRef.hostView);
  178. componentRef.destroy();
  179. });
  180. }
  181. // At this point the component has been instantiated, so we move it to the location in the DOM
  182. // where we want it to be rendered.
  183. // 此时,组件已被实例化,因此我们将其移动到 DOM 中的位置
  184. // 我们希望它在哪里渲染。
  185. this.out.appendChild(this._getComponentRootNode(componentRef));
  186. this._attachedPortal = portal;
  187. return componentRef;
  188. }
  189. /** 设置处理方式 */
  190. setDisposeFn(fn: () => void) {
  191. this._disposeFn = fn;
  192. }
  193. destory() {
  194. this.detach()
  195. }
  196. detach(): void {
  197. if (this._attachedPortal) {
  198. // this._attachedPortal.setAttachedHost(null);
  199. this._attachedPortal = null;
  200. }
  201. this._invokeDisposeFn();
  202. }
  203. private _invokeDisposeFn() {
  204. if (this._disposeFn) {
  205. this._disposeFn();
  206. this._disposeFn = null;
  207. }
  208. }
  209. }

后续加注释和思路。
项目地址中注释已添加,主要是如何在Angular中动态创建一个组件,然后添加到Dom中去。
待续~