什么是动态创建组件
在基于三大框架的前端项目中, 我们要使用一个组件一般都是将组件作为一个标签写在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.js和http://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)
把子项目的组件挂载带主项目中。
总结
使用这种方式来实现微前端架构中主子项目的嵌合其优点是简单灵活, 只需要少量额外的代码,适合那些历史包袱较重,不能为了实现微前端架构
而作太多改造的项目。它的缺点是过于简单粗暴,有很多问题需要开发者自己解决,比较硬核,而且只对基于三大框架的项目管用。