如何利用三大前端框架的动态创建组件方法实现微前端的组件嵌入

什么是动态创建组件

在基于三大框架的前端项目中, 我们要使用一个组件一般都是将组件作为一个标签写在html模板中。
框架在解析模板时会为组件创建实例并挂载组件视图, 这时候''创建组件实例和挂载组件视图''这
个过程是由框架来完成的, 而当这个过程是由我们开发者的业务代码来实现时, 那便是动态创建组
件。

三大框架动态创建组件的方法

vue2

......

import Vue from 'vue';
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  mounted() {
    const componentContainer = document.getElementById('hello-world-container');
    const componentConstructor = Vue.extend(HelloWorld); // 生成组件构造函数
    new componentContainer({
        parent: this 
    }).$mount(componentContainer); // 实例化组件并挂载组件视图
  }
}

......

angular

import { ApplicationRef, Component, ComponentFactoryResolver, ElementRef, Injector, ViewChild } from '@angular/core';
import { CpntComponent } from './cpnt/cpnt.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  @ViewChild('container') container: ElementRef = null as any

  title = 'bridge-ng';
  constructor(
    private injector: Injector,
    private applicationRef: ApplicationRef,
    private resolver: ComponentFactoryResolver
  ) {

  }
  ngAfterViewInit() {
    const componentFactory = this.resolver.resolveComponentFactory(CpntComponent); // 创建组件工厂
    const componentRef = componentFactory.create(this.injector); // 生成ComponentRef
    this.applicationRef.attachView(componentRef.hostView); // 缺了这一步组件的changes detection 会失效
    this.container.nativeElement.appendChild(componentRef.location.nativeElement); // 挂载组件视图,就是普通的dom操作,把组件视图的根节点插入到项目的dom结构中
  }
}

react

import * as ReactDom from 'react-dom';
import * as React from 'react';
import { App } from './App';

const appContainer = document.getElementById('app-container')

ReactDom.render(React.createElement(App), appContainer)

利用动态创建组件的方法嵌合主子项目

假如有基于angular的主项目ng_app和基于vue的子项目vue_app, vue_app启动时需要加载js
文件http://localhost:8080/chunk-vendor.jshttp://localhost:8080/app.js

首先,在vue_app中添加以下代码

......

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  created() {
    window.vueComponentFactory = () => {
      let componentConstructor = Vue.extend(HelloWorld);
      return new componentConstructor({
        parent: this
      })
    }
    if (window.loadResolver) window.loadResolver();
  }
}

......

在ng_app中用script标签按顺序加载vue_app的js文件

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  @ViewChild('container') container: ElementRef = null as any

  ......
  
  ngAfterViewInit() {
    this.loadProject().then(res => {
      let vueComponentInstance = (window as any).vueComponentFactory(); // 调用vueComponentFactory获取vue_app的HelloWorld组件实例
      vueComponentInstance.$mount(this.container.nativeElement); // vue_app的HelloWorld组件嵌入到ng_app的app-root组件中

    })
  }

  loadProject() {
    const assetList = [
      'http://localhost:8080/chunk-vendor.js',
      'http://localhost:8080/app.js'
    ]
    return new Promise((resolve, reject) => {
      (window as any).loadResolver = resolve; // 在vue_app中调用window.loadResolver()表明window.vueComponentFactory已添加
      const doLoad = (index: number) => {
          if (index >= assetList.length) {
              return 
          }
          const scriptEle = document.createElement('script');
          const assetPath = assetList[index];

          scriptEle.onload = () => {
              doLoad(index + 1) // vue_app的js资源按顺序逐个加载并运行
          }

          scriptEle.onerror = (err) => {
              reject(err)
          }

          scriptEle.src = assetPath;
          document.body.appendChild(scriptEle);
      }
      doLoad(0)
    })
  }
}

以上就是利用动态创建件组件的方法嵌合主子项目的过程,为了简化以上过程,我封装了一个轻量的库mcr-bridge

npm地址:https://www.npmjs.com/package/mcr-bridge
源码地址: https://github.com/nicholasking0816/micro-bridge

mcr-bridge的使用方法

在主项目中通过mcr-bridge嵌入子项目组件

import 'mcr-bridge';

......

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  @ViewChild('container') container: ElementRef = null as any

  ......

  ngAfterViewInit() {
    const global: any = window;
    global.$mcrBridge.instance.setConfig({
     projects: {
       'vue_app': {
          path: 'http://localhost:8080',
          jsonp: 'loadChunks.js'
       }
     }
   })
   global.$mcrBridge.instance.loadProject('vue_app'/*子项目唯一标识*/).then(() => {
    global.$mcrBridge.instance.mountCpnt(
        'vue_app', 
        'helloWorld', // 子项目组件唯一标识 
        this.container.nativeElement)
   });
  }
}

在子项目中注册组件到mcr-bridge中(子项目中不需要引入mcr-bridge)

1.基于vue的子项目注册组件
import Vue from 'vue';
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  created() {
    if (window.$mcrBridge) {
      const componentFactory = window.$mcrBridge.resolveVueCpntFactory(Vue.extend(HelloWorld), this);
      window.$mcrBridge.instance.registerCpnt('vue_app', 'helloWorld', componentFactory);
      global.$mcrBridge.instance.loaded('vue_app') // 通知主项目该子项目已加载完成  
    }
  }
}
2.基于react的子项目注册组件
import * as ReactDom from 'react-dom';
import * as React from 'react';
import { App } from './App';

// ReactDom.render(<App name="hello"></App>, document.getElementById('app'));
const global: any = window as any;
if (global.$mcrBridge) {
    const componentFactory = global.$mcrBridge.resolveReactCpntFactory(App, React, ReactDom);
    global.$mcrBridge.instance.registerCpnt('React_app', 'app', componentFactory);
    global.$mcrBridge.instance.loaded('React_app') // 通知主项目该子项目已加载完成
}
3.基于angular的子项目注册组件
低于angular13的版本
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  ......

  constructor(
    private injector: Injector,
    private applicationRef: ApplicationRef,
    private resolver: ComponentFactoryResolver,
    private zone: NgZone
  ) {

  }
  ngAfterViewInit() {
    const global: any = window;
    if (global.$mcrBridge) {
      const factory = global.$mcrBridge.resolveNgCpntFactory(CpntComponent, this.resolver, this.applicationRef, this.injector, this.zone);
      global.$mcrBridge.instance.registerCpnt(
        'ng_app', // 子项目的唯一标识
        'cpnt', // 子项目组件的唯一标识
        factory
      )
      global.$mcrBridge.instance.loaded('ng_app') // 通知主项目该子项目已加载完成
    }
  }
}
angular13以上的版本
import { 
    ......

    ɵNG_COMP_DEF, 
    ɵRender3ComponentFactory, 

    ......    

} from '@angular/core';

......

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  constructor(
      private injector: Injector,
      private applicationRef: ApplicationRef,
      private resolver: ComponentFactoryResolver,
      private zone: NgZone
  ) {
  
  }
  ......

  ngAfterViewInit() {
    const global: any = window;
    if (global.$mcrBridge) {
      const factory = global.$mcrBridge.resolveNgCpntFactory2(
        CpntComponent[ɵNG_COMP_DEF], 
        ɵRender3ComponentFactory, 
        this.applicationRef, 
        this.injector,
        this.zone
      );
      global.$mcrBridge.instance.registerCpnt(
        'ng_app', // 子项目的唯一标识
        'cpnt', // 子项目组件的唯一标识
        factory
      )
      global.$mcrBridge.instance.loaded('ng_app') // 通知主项目该子项目已加载完成
    }
  }
}

在子项目中生成loadChunks.js文件

子项目中需要一个loadChunks.js文件让主项目能够通过jsonp的方式知道该子项目需要加载哪一些资源

loadChunks.js

window.$mcrBridge.instance.loadProjectJsonp("vue_app", {
    js:["js/chunk-vendors.f4b13c22.js", 'js/app.8c4d252c.js'],
    css:["css/app.30bf6194.css"]}
);

通过webpack生成模式打包编译生成的js文件都会拼上hash代码,这就导致每一次打包编译生成的文件名都不一样,
而每次打包后都手动更改loadChunks.js的内容显然是很麻烦的事情。

使用mcr-bridge-plugin.js 自动生成loadChunks.js文件

const MicroBridgePlugin = require('mcr-bridge/plugin/mcr-bridge-plugin')

module.exports = {
    plugins: [
        new MicroBridgePlugin({
            projectName: 'vue_app' //项目标识,
            js: {
                files: ['lodash.js'] // 添加除了打包生成的额外js文件,
                exclude: ['zone.js', /\.shared.js$/],
                sort: function(file1, file2) {
                    if (file1 === 'lodash.js') return 1;
                    if (file2 === 'lodash.js') return -1;
                    return 0    
                } 
            },
            css: {}
        })
    ]
}

API

window.$mcrBridge.instance.setConfig(config: Object)

设置配置项。

window.$mcrBridge.instance.getConfig()

获取配置项。

window.$mcrBridge.instance.loadProjecttConfig(projectName: string): Promise

加载子项目。

window.$mcrBridge.instance.loaded(projectName)

在子项目中调用,告诉主项目自已已加载完成。

window.$mcrBridge.instance.registerCpnt(projectName: string, componentId: string, componentFactory: Function)

在子项目中注册组件到mcr-bridge。

window.$mcrBridge.instance.mountCpnt(projectName: string, componentId: string, componentAnchor: HTMLElement, props?: any)

把子项目的组件挂载带主项目中。

总结

使用这种方式来实现微前端架构中主子项目的嵌合其优点是简单灵活, 只需要少量额外的代码,适合那些历史包袱较重,不能为了实现微前端架构
而作太多改造的项目。它的缺点是过于简单粗暴,有很多问题需要开发者自己解决,比较硬核,而且只对基于三大框架的项目管用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容