1.前文
刚开始接触微前端,这条路走得格外艰辛,这几天结合源码,官网,一些大佬的个人博客,总结出这到底是什么,下面这部分就是这几天自学的成果,逐步介绍当前最流行的微前端解决方案qiankun
2.什么是qiankun
qiankun是微前端的一种解决方案。孵化自蚂蚁金融,跟umi来自同一家公司。说句实话不明白为什么叫他qiankun,明明是sing-spa的二次封装,qinkun是什么,它就是微前端的解决方案,那么微前端又是什么?我觉得用我自己的话来说,微前端首先解决了项目后期变得庞大。这个时候,我们首先会想到分模块上线,那么微前端就是将这些上线好的模块组合起来,形成一个项目。如果有一天,我发现我写的某个功能模块挺好的,我想拿下来用到我的新项目中,那么就可以采取微前端的方式。
3.qiankun的特性(官网抄的)
- 📦 基于 single-spa 封装,提供了更加开箱即用的 API。
- 📱 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
- 💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
- 🛡 样式隔离,确保微应用之间样式互相不干扰。
- 🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
- ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
- 🔌 umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。
4.项目搭建
项目需要一个主应用作为项目基座,至少一个子应用作为微应用嵌套在主应用中,官网使用是react作为基座。这里全都主微应用使用vue2.0语法。
4.1 安装
yarn ada qiankun --save
4.2 父应用中配置
registerMicroApps配置
entry
: 子应用的 entry 地址,比如我们现在有两个子应用A和B,那么这里配置的就是他们的资源访问域名或iprender
:本质上是container的转换,container用来定义子应用的容器节点的选择器或者 Element 实例,这里使用的是实际例子activeRule
:子应用的激活规则,即什么路由访问才会去fetch entry配置的域名或ip,我们用了getActiveRule
来完成匹配,我们看看getActiveRule
的实现,该函数通过传入当前 location 作为参数,然后根据函数返回数值来看,若返回值为 true 时则表明当前子应用会被激活,则去调用entry入口配置
- 新建一个until文件夹下registApp.js
import { registerMicroApps, start } from 'qiankun';
function loader(){
console.log('打印loder');
}
registerMicroApps([
{
name: 'vue1', // app name registered
entry: '//localhost:3011',
container: '#container',
activeRule: '/vue1',
// loader,
},
{
name: 'vue2',
entry: '//localhost:3012',
container: '#container',
activeRule:'/vue2',
loader,
},
],
{
beforeLoad:()=>{
console.log('加载前');
},
beforeMount:()=> {
console.log('挂载前');
},
afterMount:()=>{
console.log('挂载后');
}
}
);
start({
sandbox:{
// experimentalStyleIsolation:true
strictStyleIsolation:true
}
});
2.mian.js中引入
import Vue from 'vue'
import App from './App.vue'
import { MessageBox } from "./components/MyUi";
import router from "./routes/routers"
import store from './store/index'
import './until/registApp'
Vue.config.productionTip = false
Vue.use(MessageBox)
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
3.使用展示
<template>
<div id="app">
<div>
<home/>
<router-link to="/vue1" tag="button">使用vue1</router-link>
<router-link to="/vue2" tag="button">使用vue2</router-link>
<!-- 渲染区域 -->
<router-view />
<div id="container"></div>
</div>
</div>
</template>
4.3 子应用配置
1.在main.js中配置
import Vue from 'vue'
import App from './App.vue'
import action from "@/mircros/action"
Vue.config.productionTip = false
let instance = null;
function render(props = {}) {
const { container } = props;
//这里的目的是找到qiankun中根节点挂载以及没有qiankun就直接挂载在根节点上
instance=new Vue({
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
//这里的props接受父应用传递的参数
action.setActions(props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
}
2.vue.config.js
解决跨域问题
module.exports = {
lintOnSave: false,
publicPath:'//localhost:3011',
devServer:{
port:'3011',
//解决跨域,端口号需要与父应用中保持一致
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: `vue1`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
},
},
}
5.沙箱
5.1 css沙箱
5.1.1 前缀式沙箱
给定义的css选择器都增加一个缀,使得css模块化
start({
sandbox:{
//这里使用设置这个属性可以给所有的css属性增加一个前缀
experimentalStyleIsolation:true
}
})
5.1.2 影子Dom沙箱
这种方式最常见的,例如vedio标签在浏览器中,就会使用一层影子包裹
start({
sandbox:{
strictStyleIsolation:true
}
})
5.2 js沙箱
5.2.1 快照沙箱
由主应用切换a应用上的window上挂载完数据后,在由主应用切换到b应用时,此时已经清空了A应用window上的属性数据等,此时的window对象是一个全新的window,是专属于B应用的window
const window = globalThis;
class SnapshotSandbox {
constructor(name) {
// 当前沙箱名称
this.name = name;
this.windowSnapshot = {};
this.modifyPropsMap = {};
this.isRunning = false;
}
active() {
// 1、记录快照
this.windowSnapshot = {};
for (const prop in window) {
if (Object.hasOwnProperty.call(window, prop)) {
const ele = window[prop];
this.windowSnapshot[prop] = ele;
}
}
// 2、恢复之前的变更
for (const key of Object.keys(this.modifyPropsMap)) {
window[key] = this.modifyPropsMap[key];
}
this.isRunning = true;
}
inactive() {
this.modifyPropsMap = {};
// 记录本次运行的变更,利用快照还原window
for (const prop in window) {
if (Object.hasOwnProperty.call(window, prop) && window[prop] !== this.windowSnapshot[prop]) {
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop];
}
}
this.isRunning = false;
}
}
const sandbox1 = new SnapshotSandbox("s1");
window.city = "su zhou";
sandbox1.active();
window.city = 'hang zhou';
console.log('city1', window.city);
sandbox1.inactive();
console.log('city2', window.city);
sandbox1.active();
console.log('city3', window.city);
5.2.2 代理沙箱
通过 proxy 代理 window 对象,记录 window 对象上属性的增删改查可以做多应用沙箱。
- 单例模式直接代理了原生 window 对象,记录原生 window 对象的增删改查,当 window 对象激活时恢复 window 对象到上次即将失活时的状态,失活时恢复 window 对象到初始初始状态
- 多例模式代理了一个全新的对象,这个对象是复制的 window 对象的一部分不可配置属性,所有的更改都是基于这个 fakeWindow 对象,从而保证多个实例之间属性互不影响
将这个 proxy 作为微应用的全局对象,所有的操作都在这个 proxy 对象上。
// 当前活跃的沙箱数量
let activeSandbox = 0;
// 最近操作过属性的沙箱
let curSandbox = null;
// 全局上下文(在浏览器环境下是window,目前在node环境测试使用globalThis)
const globalContext = globalThis;
// 获取当前沙箱
function getCurSandbox() {
return curSandbox;
}
// 设置当前沙箱
function setCurSandbox(sandboxIns) {
curSandbox = sandboxIns;
}
// 沙箱构造函数
class ProxySandbox {
registerRunningApp(name, proxy) {
if (this.isRuning) {
const curApp = getCurSandbox();
if (!curApp || curApp.name !== name) {
setCurSandbox({name:name, window:proxy})
}
}
}
constructor(name) {
this.name = name;
this.isRuning = false;
const fakeWindow = Object.create({});
const proxy = new Proxy(fakeWindow, {
set: (target, prop, value) => {
// 赋值时,仅当沙箱激活时才可赋值
if (this.isRuning) {
target[prop] = value;
}
this.registerRunningApp(this.name, this.proxy);
},
get: (target, prop) => {
// 取值时,先找fakeWindow再找window
this.registerRunningApp(this.name, this.proxy);
return prop in target ? target[prop] : globalContext[prop];
}
});
this.proxy = proxy;
}
active() {
// 激活沙箱当前活跃沙箱加一
if (!this.isRuning) activeSandbox++;
this.isRuning = true;
}
inactive() {
// 让沙箱失活,当前活跃沙箱数量减一
--activeSandbox;
this.isRuning = false;
}
}
const pSandbox1 = new ProxySandbox("p1");
const pSandbox2 = new ProxySandbox("p2");
globalContext.city = "changsha";
globalContext.country = "China";
pSandbox1.active()
pSandbox2.active();
console.log("当前活跃沙箱数量", activeSandbox);
pSandbox1.proxy.city = "hangzhou";
console.log("当前活跃沙箱:", curSandbox.name);
console.log("沙箱1", pSandbox1.proxy.city, pSandbox1.proxy.country);
pSandbox2.proxy.city = "fuzhou";
console.log("当前活跃沙箱:", curSandbox.name);
console.log("沙箱2",pSandbox2.proxy.city, pSandbox2.proxy.country);
console.log("全局上下文city", globalContext.city);
pSandbox1.inactive();
pSandbox2.inactive();
console.log("pSandbox1", pSandbox1.proxy.city);
console.log("pSandbox2", pSandbox2.proxy.city);
console.log("当前活跃沙箱数量", activeSandbox);
6.通信方式
6.1 action模式
6.1.2 父应用中的操作
1.父应用中在src新建micros文件夹action.js文件
import { initGlobalState } from 'qiankun'
// 初始化 state,这里是应用data中心
const initState = {
msgs:""
}
const actions = initGlobalState(initState)
export default actions
2.在需要传递应用数据的父应用的组件中传递数据并监听数据
<template>
<div>
<button @click='parentClick'>父应用传递到子应用的值</button>
</div>
</template>
<script>
import actions from "../../micros/action";
export default {
name: "Home",
data() {
return {
token:'fvifvrvrionvr'
};
},
mounted() {
// 注册一个观察者函数,监听传递到子应用的数据
actions.onGlobalStateChange((state, prevState) => {
console.log("主应用观察者:token 改变前的值为 ", prevState.token);
console.log("主应用观察者:登录状态发生改变,改变后的 token 的值为 ", state.token);
});
},
methods: {
parentClick(){
let token =this.token
//设置需要传递的属性数据
actions.setGlobalState({ token });
}
},
};
</script>
6.1.2.子应用的操作
1.子应用中在src新建micros文件夹action.js文件
function emptyAction() {
// 警告:提示当前使用的是空 Action
console.warn('Current execute action is empty!')
}
class Actions {
// 默认值为空 Action
actions = {
onGlobalStateChange: emptyAction,
setGlobalState: emptyAction
}
// 设置 actions
setActions(actions) {
this.actions = actions
}
// 映射监听
onGlobalStateChange(...args) {
return this.actions.onGlobalStateChange(...args)
}
// 映射设置
setGlobalState(...args) {
return this.actions.setGlobalState(...args)
}
}
const actions = new Actions()
export default actions
2.在子应用的main.js组件渲染时将父应用传递过来的数据传递到reder中
import Vue from 'vue'
import App from './App.vue'
import action from "@/mircros/action"
Vue.config.productionTip = false
let instance = null;
function render(props = {}) {
const { container } = props;
instance=new Vue({
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
//这里是重点
action.setActions(props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
}
3.在需要使用父应用传递过来数据的地方使用
<template>
<div>
<h1>父应用传递过来的值{{ title }}</h1>
</div>
</template>
<script>
import actions from "../mircros/action";
export default {
name: "Home",
data() {
return {
title: "",
};
},
mounted() {
// 注册观察者函数
// onGlobalStateChange 第二个参数为 true,表示立即执行一次观察者函数
actions.onGlobalStateChange((state) => {
//这里可以拿到父应用传递过来的值
console.log(state,'gg');
// const { token } = state;
this.title = state.token;
}, true);
},
};
</script>
6.2 发布订阅模式
1.主应用构建中央数据总线
//eventbus.js
import Vue from 'vue'
Vue.prototype.$eventBus = new Vue()
export const parentEventBus = Vue.prototype.$eventBus
2.将数组总线通过props传递出去
import { registerMicroApps, start } from 'qiankun';
import { parentEventBus } from './eventBus'
function loader(){
console.log('打印loder');
}
registerMicroApps([
{
name: 'vue1', // app name registered
entry: '//localhost:3011',
container: '#container',
activeRule: '/vue1',
props: { // 下发微应用的入口, 如果是固定确认的资源可以维护在应用注册列表
id:'ws',
parentEventBus :parentEventBus
}
},
],
);
start({
sandbox:{
strictStyleIsolation:true
}
});
3.主应用通过bus可以接受数据也可传递数据出去
<template>
<div>
<div v-for='item in list' :key='item'>{{item}}</div>
</div>
</template>
<script>
//引入bus
import { parentEventBus } from '../../until/eventBus'
export default {
name: "Home",
data() {
return {
list:[]
};
},
mounted() {
//接受微应用传递的数据
parentEventBus.$on('handleArr', data => { // 监听
console.log('这里能够获取到arr',data);
this.list=data
})
},
};
</script>
4.微应用main.js中接受props里面解构出主应用传递过来的中央数据总线
import Vue from 'vue'
import App from './App.vue'
import action from "@/mircros/action"
Vue.config.productionTip = false
let instance = null;
function render(props = {}) {
const { container,parentEventBus } = props
Vue.prototype.$eventBus = new Vue() // 子应用的独享的 EventBus
//接受主应用传递过来的数据总线将它挂载到微应用的bus实例上
Vue.prototype.$parentEventBus = parentEventBus
instance=new Vue({
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
// console.log(props,'gggggyy');
action.setActions(props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
}
5.在微应用上使用,通过bus实例传递一个数据
<template>
<div>
<button @click=' tranfromParent'>点击传递给父应用的数据</button>
</div>
</template>
<script>
export default {
name: "Home",
data() {
return {
arr:[1,3,4,6,7]
};
},
methods: {
tranfromParent(){
this.$parentEventBus.$emit('handleArr',this.arr)
}
},
};
</script>
7.数据共享
7.1 props
在父应用中定义一个props,子应用就可以拿到这个props了
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'vue1', // app name registered
entry: '//localhost:3011',
container: '#container',
activeRule: '/vue1',
//这里定义
props: { // 下发微应用的入口, 如果是固定确认的资源可以维护在应用注册列表
id:'ws'
}
},
],
);
start()
在子应用的生命周期mount中可以解构出这个props里的属性
import Vue from 'vue'
import App from './App.vue'
import action from "@/mircros/action"
Vue.config.productionTip = false
let instance = null;
function render(props = {}) {
//这里可以拿到id
console.log(props.id,'gggid')
const { container } = props;
instance=new Vue({
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
action.setActions(props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
}
7.2 window
主应用中的组件需要被微应用引用可以把该组件通过window的方式进行挂载
主应用进行挂载
import About from "./components/About/About.vue"
window.commonComponent = { About }
微应用组件注册使用
<template>
<div>
<about/>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: "App",
components: {
Home,
About: window.__POWERED_BY_QIANKUN__ ? window.commonComponent.About : import('@/components/HelloWorld.vue')
},
};
</script>
8.手动添加微应用
由于registerMicroApps的特性,会导致路由的keep alive 失效,故使用 loadMicroAp 进行来达到自动注册的目的。如果微应用不是直接跟路由关联的时候,选择手动加载微应用的方式会更加灵活
<template>
<div>
<button @click="mout">挂载子应用</button>
</div>
</template>
<script>
import { loadMicroApp } from "qiankun";
export default {
name: "About",
methods: {
mout() {
this.microsApp = loadMicroApp({
name: "vue2",
entry: "//localhost:3012",
container: "#container",
activeRule: "/vue2",
props: {
token: "chedehkeh",
},
});
},
},
beforeDestroy() {
this.microsApp.unmount()
},
};
</script>
9.应用预加载
可以预先请求子应用的 HTML、JS、CSS 等静态资源,等切换子应用时,可以直接从缓存中读取这些静态资源,从而加快渲染子应用。
9.1registerMicroApps 模式
registerMicroApps 模式下,在start函数中配置prefetch即可
- 配置为 all 则主应用 start 后即开始预加载所有微应用静态资源
- 配置为 string[] 则会在第一个微应用 mounted 后开始加载数组内的微应用资源
- 配置为 function 则可完全自定义应用的资源加载时机 (首屏应用及次屏应用
start({
sandbox:{
strictStyleIsolation:true,
//这里进行预加载
prefetch: "all"
}
});
9.2 LoadMicroApps模式
手动预加载指定的微应用静态资源,仅手动加载微应用场景需要
import { prefetchApps } from 'qiankun';
prefetchApps([
{ name: 'app1', entry: '//localhost:7001' },
{ name: 'app2', entry: '//localhost:7002' },
]);