前言
你是否遇到过一下几种情况
- 某些和项目契合度不高的页面,因为业务需要,也被放到项目中去开发,造成体积臃肿
- 跨团队协作开发的时候,对方要塞几个页面到你的项目里
- 高复用页面,很多项目都有类似的几个页面,但是要维护好几份代码
- 大炮打蚊子,接入微前端,其实只有简单的几个页面
浅析思考
微前端的加载逻辑是什么?其实他就是通过一个入口去请求资源,也就是我们配置的entry
。然后主应用提供一个容器,把子应用的代码运行在这个容器下。
微前端其实去加载的就是一个html
文件,然后通过html
文件去加载对应的css
和js
文件。
那如果我们自己去手撸一个加载器呢?我们只需要通过script
去加载一个bundle
文件,是否就能实现页面级的微前端呢?
写在前面
目前我使用的是Vue3框架,目前跨Vue2,和Vue3都是可以实现的。对于React的用户,可能方式需要再调整一下,但是整体思路应该都是相通的。
加载器代码
html
<template>
<component
v-if="mode"
:is="mode"
:extraData="extraData"
v-bind="{ ...attrs, ...$attrs }"
></component>
<div v-if="isError">
<b>
{{ pluginData.libraryName }}
</b>
加载失败!!尝试点击
<el-button type="text" @click="openUrl(pluginData.url)">
刷新
</el-button>
查看是否为浏览器权限问题!
</div>
</template>
<script>
import { markRaw, defineComponent } from 'vue';
export default defineComponent({
name: 'RemotePlugin',
emits: ['loaded'],
props: {
// 插件需要的数据
pluginData: {
type: Object,
default: () => {},
},
// 额外需要传输的数据
extraData: {
type: Object,
default: () => {},
},
},
data() {
return {
mode: '',
attrs: {},
isError: false,
};
},
mounted() {
this.load();
},
methods: {
asyncScript(url, name) {
// 动态script
return new Promise((resolve, reject) => {
const script = document.createElement('script');
const target = document.head;
script.type = 'text/javascript';
script.src = url;
script.id = name;
script.onload = function () {
resolve(window);
};
script.onerror = () => {
this.isError = true;
console.error('加载失败');
window[name] = null;
reject();
};
target.parentNode?.appendChild(script);
});
},
load() {
this.loadScript(this.currentLoadConfig);
},
/**
* 加载js
* @param url js文件地址
* @param libraryName 包名
* @return {Promise<void>}
*/
loadScript(config) {
const { url, libraryName } = this.pluginData;
let cp = window[libraryName];
this.asyncScript(url, libraryName).then(() => {
cp = window[libraryName];
if (cp.AsyncPluginComp) cp = cp.AsyncPluginComp;
cp.inheritAttrs = true;
this.mode = markRaw(cp);
this.attrs = {
[cp.__scopeId]: '',
};
this.$emit('loaded');
});
},
},
});
</script>
通信问题
可能有的朋友会问了,微前端通过setGlobalState
可以进行父子应用通信,我们这种方式怎么通信呢?
我们是通过组件加载的方式,去加载的子页面,所以天生就支持原生的父子组件通信方式
。可以通过props
和emits
,比微前端更便捷。
如何使用
html
<template>
<remote-plugin :pluginData="pluginData" :extraData="extraData"></remote-plugin>
</template>
<script setup>
import RemotePlugin from '@/components/RemotePlugin.vue';
const extraData = {
foo:'bar'
};
const libraryName = 'libraryName';
const pluginData = {
url: `http://localhost:8888/demo.js`,
libraryName,
};
</script>
关于主实例
既然我们上面说了,子页面是通过组件的方式去加载的。那么对于Vue的实例,页面在打包时,就不能使用自己的Vue实例,而是需要通过globals
加载主应用对外暴露的Vue实例。 所以子页面项目在打包的时候需要配置下
js
rollupOptions: {
external: ['vue', 'vue-router'],
output: {
globals: {
vue: 'globalVue',
'vue-router': 'globalVueRouter',
}
}
}
这样子页面在使用import {ref} from 'vue'
的时候,指向的就是主应用的Vue实例对象。
这样,只要在主应用注册过的公共组件,插件,在子页面就可以丝滑的使用,没有任何负担。
对了,子页面使用globals
去加载,其实就是去window.globalVue
去找,这个时候,主应用也需要适配下,通过window对外暴露
js
import * as Vue from 'vue';
import * as VueRouter from 'vue-router';
window.globalVue = Vue;
window.globalVueRouter = VueRouter;
关于持久化和路由的支持
关于持久化,无论你是使用vueX
还是Pinia
都可以做到支持。目前我使用的是Pinia
可以通过props
传入,然后在子页面使用。比如:
html
主应用代码
<template>
<remote-plugin :pluginData="pluginData" :extraData="extraData"></remote-plugin>
</template>
<script setup>
import RemotePlugin from '@/components/RemotePlugin.vue';
import {
usePublicStore,
useInstanceStore,
useAppStore,
} from '@/store';
const extraData = {
storeData: {
usePublicStore,
useInstanceStore,
useAppStore
}
};
const libraryName = 'libraryName';
const pluginData = {
url: `http://localhost:8888/demo.js`,
libraryName,
};
</script>
js
子页面代码
import { defineProps,provide } from 'vue'
const props = defineProps({
extraData: {
required: true,
type: Object,
default:() => {}
}
})
const { usePublicStore,useInstanceStore,useAppStore } = props.storeData.storeData
const publicStore = usePublicStore()
const instanceStore = useInstanceStore()
const appStore = useAppStore()
//向下注入 方便使用
provide('publicStore',publicStore)
provide('instanceStore',instanceStore)
provide('appStore',appStore)
关于路由,如果你的子页面下面还区分路由,那么在使用的时候需要把主应用对应页面的路由传下去,然后子页面基于主应用传入的路由,使用addRoute
进行注册。比如:
js
主应用代码
<template>
<remote-plugin :pluginData="pluginData" :extraData="extraData"></remote-plugin>
</template>
<script setup>
import RemotePlugin from '@/components/RemotePlugin.vue';
const extraData = {
parentRouteName:'aaa',
parentRoutePath: '/aaa',
};
const libraryName = 'libraryName';
const pluginData = {
url: `http://localhost:8888/demo.js`,
libraryName,
};
</script>
js
子页面代码
import { defineProps } from 'vue'
import {useRouter,useRoute} from 'vue-router'
const props = defineProps({
extraData: {
type: Object
}
})
const { parentRouteName, parentRoutePath } = props.extraData
const router = useRouter()
const route = useRoute()
const routes = [
{
path:'first',
name: 'first',
component:() => import('./first.vue')
},
{
path:'second',
name:'second',
component:() => import('./second.vue')
}
]
routes.forEach(child => {
router.addRoute(parentRouteName, child)
})
关于Vue2和Vue3兼容问题
本人亲测,不管是Vue2打包出来的bundle
在Vue3项目下,还是Vue3项目打包出来的bundle
放在Vue2下运行,都是可以实现的。
如果是React项目想在Vue下运行,也不是不可以,只不过麻烦的点在于不能去使用组件注册的方式了,而是需要有一个根节点,去使用mount
的方式加载,子页面暴露出去的也不再是一个页面,而是需要暴露出去一个React实例。
关于qiankun兼容性问题
如果是在qiankun下使用,需要使用window.proxy
去访问Vue实例对象