qiankun是基于Single-spa开发的框架,所以我们先来看下Single-spa是怎么做的:
Single-spa 是最早的微前端框架,兼容多种前端技术栈,是一个将多个单页面应用聚合为一个整体应用的 JavaScript 微前端框架;
优点:
1、敏捷性 - 独立开发、独立部署,微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新;
2、技术栈无关,主框架不限制接入应用的技术栈,微应用具备完全自主权;
3、增量升级,在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
4、更快交付客户价值,有助于持续集成、持续部署以及持续交付;
5、维护和 bugfix 非常简单,每个团队都熟悉所维护特定的区域;
缺点:
1、无通信机制
2、不支持 Javascript 沙箱
3、样式冲突
4、无法预加载
Single-spa实现原理
首先在基座应用中注册所有App的路由,single-spa保存各子应用的路由映射关系,充当微前端控制器Controler。URL响应时,匹配子应用路由并加载渲染子应用。
基座配置
//main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { registerApplication, start } from 'single-spa'Vue.config.productionTip = falseconst mountApp = (url) => {return new Promise((resolve, reject) => {const script = document.createElement('script')script.src = urlscript.onload = resolvescript.onerror = reject// 通过插入script标签的方式挂载子应用const firstScript = document.getElementsByTagName('script')[]// 挂载子应用firstScript.parentNode.insertBefore(script, firstScript)})
}const loadApp = (appRouter, appName) => {// 远程加载子应用return async () => {//手动挂载子应用await mountApp(appRouter + '/js/chunk-vendors.js')await mountApp(appRouter + '/js/app.js')// 获取子应用生命周期函数return window[appName]}
}// 子应用列表
const appList = [{// 子应用名称name: 'app1',// 挂载子应用app: loadApp('http://localhost:8083', 'app1'),// 匹配该子路由的条件activeWhen: location => location.pathname.startsWith('/app1'),// 传递给子应用的对象customProps: {}},{name: 'app2',app: loadApp('http://localhost:8082', 'app2'),activeWhen: location => location.pathname.startsWith('/app2'),customProps: {}}
]// 注册子应用
appList.map(item => {registerApplication(item)
})// 注册路由并启动基座
new Vue({router,mounted() {start()},render: h => h(App)
}).$mount('#app')
构建基座的核心是:配置子应用信息,通过registerApplication注册子应用,在基座工程挂载阶段start启动基座。
我们通过代码也发现 Single-spa 是通过插入script标签的方式挂载子应用
子应用配置
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue'Vue.config.productionTip = falseconst appOptions = {el: '#microApp',router,render: h => h(App)
}// 支持应用独立运行、部署,不依赖于基座应用
// 如果不是微应用环境,即启动自身挂载的方式
if (!process.env.isMicro) {delete appOptions.elnew Vue(appOptions).$mount('#app')
}
// 基于基座应用,导出生命周期函数
const appLifecycle = singleSpaVue({Vue,appOptions
})// 抛出子应用生命周期
// 启动生命周期函数
export const bootstrap = (props) => {console.log('app2 bootstrap')return appLifecycle.bootstrap(() => { })
}
// 挂载生命周期函数
export const mount = (props) => {console.log('app2 mount')return appLifecycle.mount(() => { })
}
// 卸载生命周期函数
export const unmount = (props) => {console.log('app2 unmount')return appLifecycle.unmount(() => { })
}
配置子应用为umd打包方式
//vue.config.js
const package = require('./package.json')
module.exports = {// 告诉子应用在这个地址加载静态资源,否则会去基座应用的域名下加载publicPath: '//localhost:8082',// 开发服务器devServer: {port: 8082},configureWebpack: {// 导出umd格式的包,在全局对象上挂载属性package.name,基座应用需要通过这个// 全局对象获取一些信息,比如子应用导出的生命周期函数output: {// library的值在所有子应用中需要library: package.name,libraryTarget: 'umd'}}
子应用配置的核心是用singleSpaVue生成子路由配置后,必须要抛出其生命周期函数。
用以上方式便可轻松实现一个简单的微前端应用了。
那么我们有single-spa这种微前端解决方案,qiankun 又在此基础上做了什么呢
相比于single-spa,qiankun他解决了JS沙盒环境,不需要我们自己去进行处理。在single-spa的开发过程中,我们需要自己手动的去写调用子应用JS的方法(如上面的 createScript方法),而qiankun不需要,乾坤只需要你传入响应的apps的配置即可,会帮助我们去加载。
Qiankun
1、基于 single-spa 封装,提供了更加开箱即用的 API。
2、技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
3、HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
4、样式隔离,确保微应用之间样式互相不干扰。
5、JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
6、资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
基座配置
import { registerMicroApps, start, runAfterFirstMounted, setDefaultMountApp } from 'qiankun';const microApps = [{name: 'reactApp',entry: '//localhost:3000',container: '#container',activeRule: '/app-react',loader: (loading: boolean) => void - 可选,loading 状态发生变化时会调用的方法。props: {} - 可选,主应用需要传递给微应用的数据。},{name: 'vueApp',entry: '//localhost:8080',container: '#container',activeRule: '/app-vue',},{name: 'angularApp',entry: '//localhost:4200',container: '#container',activeRule: '/app-angular',}
]
/**beforeLoad - Lifecycle | Array<Lifecycle> - 可选beforeMount - Lifecycle | Array<Lifecycle> - 可选afterMount - Lifecycle | Array<Lifecycle> - 可选beforeUnmount - Lifecycle | Array<Lifecycle> - 可选afterUnmount - Lifecycle | Array<Lifecycle> - 可选
*/
registerMicroApps(microApps, {beforeLoad: (app) => console.log('before load', app.name),beforeMount: [(app) => console.log('before mount', app.name)],}
);
// 启动 qiankun
start();
// 设置主应用启动后默认进入的微应用。
setDefaultMountApp('/app-react');
// 第一个微应用 mount 后需要调用的方法,比如开启一些监控或者埋点脚本。
runAfterFirstMounted(() => {console.log('第一个微应用已挂在,可以后续操作');
});
子应用配置
// main.ts
function render(props: any = {}) {if (Object.keys(props).length === 0) return;const { userInfo } = props;store.commit('SetGlobalObj', props);store.commit('SetUserInfo', userInfo);instance = new Vue({router,store,render: h => h(App)}).$mount(container ? container.querySelector('#app') : '#app');
}let win:any = window;
// 判断是否是qiankun环境,兼容非微前端环境
if (!win.__POWERED_BY_QIANKUN__) {const router = getRoute('/dashboard/detail/information');new Vue({el: "#app",router,store,render: h => h(App)});
}export async function bootstrap() {console.log('[vue] vue app bootstraped');
}export async function mount(props: any) {render(props);
}export async function unmount() {
}
修改 webpack 配置
这个跟Single-spa 差不多,主要是 把微应用打包成 umd 库格式
const { name } = require('./package');
module.exports = {devServer: {headers: {'Access-Control-Allow-Origin': '*',},},configureWebpack: {output: {library: `${name}-[name]`,// 把微应用打包成 umd 库格式libraryTarget: 'umd', // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobaljsonpFunction: `webpackJsonp_${name}`},},
};
这里特别说明下 qiankun 加载微应用的方式除了上面那种外,如果微应用不是直接跟路由关联的时候,也可以选择手动加载微应用的方式:
import { loadMicroApp } from 'qiankun';loadMicroApp({name: 'app',entry: '//localhost:7100',container: '#yourContainer',
});
生命周期钩子注释说明
/*** bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。*/
export async function bootstrap() {console.log('react app bootstraped');
}/*** 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法*/
export async function mount(props) {ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}/*** 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例*/
export async function unmount(props) {ReactDOM.unmountComponentAtNode(props.container ? props.container.querySelector('#root') : document.getElementById('root'),);
}/*** 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效*/
export async function update(props) {console.log('update props', props);
}
qiankun的通信方式
1、localStorage/sessionStorage
2、通过路由参数共享
3、官方提供的 props
4、官方提供的 actions
5、使用vuex或redux管理状态,通过shared分享
1、localStorage/sessionStorage
有人说这个方案必须主应用和子应用是同一个域名下。其实不是的,子应用使用不同的域名也是可以的,因为在 qiankun 中,主应用是通过 fetch 来拉取子应用的模板,然后渲染在主应用的 dom 上的,说白了还是运行在主应用上,所以还是运行在同一个域名上,也就是主应用的域名。
父传子
主应用 main.js
localStorage.setItem(‘token’, ‘123’)
console.log(‘在main中设置了token’)
子应用app1 main.js
const token = localStorage.getItem(‘token’)
console.log(‘app1中打印token:’, token)
子传父
同理app1修改token,main也可以看到,这里就不再赘述
2、通过路由参数共享
这个也很好理解,因为只有一个 url,不管子应用还是主应用给 url 上拼接一些参数,那么父子应用都可以通过 route 来获取到。
3、官方提供的 props
这个在前面已经说过了就是registerMicroApps注册微应用时,通过props传送信息
4、官方提供的 actions
就一个 API initGlobalState
qiankun 内部提供了 initGlobalState 方法用于注册 MicroAppStateActions 实例用于通信,该实例有三个方法,分别是:
setGlobalState:设置 globalState - 设置新的值时,内部将执行 浅检查,如果检查到 globalState 发生改变则触发通知,通知到所有的 观察者 函数。
onGlobalStateChange:注册 观察者 函数 - 响应 globalState 变化,在 globalState 发生改变时触发该 观察者 函数。
offGlobalStateChange:取消 观察者 函数 - 该实例不再响应 globalState 变化
我们从上图可以看出,我们可以先注册 观察者 到观察者池中,然后通过修改 globalState 可以触发所有的 观察者 函数,从而达到组件间通信的效果。
下面就是注册了一个观察者
const actions: MicroAppStateActions = initGlobalState({});
actions.onGlobalStateChange((state, prev) => {// state: 变更后的状态; prev 变更前的状态
});
setTimeout(() => {actions.setGlobalState(Object.assign({ username: 'Lee', obj: { token: 222 } }));
}, 1000);
// actions.offGlobalStateChange();
我们来看具体应用:
父传子
主应用:
actions.js
import { initGlobalState, MicroAppStateActions } from 'qiankun';const state = {num: 1
};// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);actions.onGlobalStateChange((state, prev) => {// state: 变更后的状态; prev 变更前的状态console.log('主应用检测到state变更:', state, prev);
});// 你还可以定义一个获取state的方法下发到子应用
actions.getGlobalState = function () {return state
}export default actions;
index.js 注册文件
import {registerMicroApps,start,
} from "qiankun";
import actions from './actions.js'
const apps = [{name: "App1MicroApp",entry: '//localhost:9001',container: "#app1",activeRule: "/app1",props: {parentActions: actions}}
];registerMicroApps(apps);
start();
这样就把这个 actions 传给了子应用。
子应用:
let instance = null
let router = nullfunction render (props) {console.log(props.parentActions);// 在子应用中使用就可以访问到这个parentActions了props.parentActions.setGlobalState({ num: 2 })// 调用挂载在 actions 上的自定义方法,获取当前的全局 stateprops.parentActions.getGlobalState();router = new VueRouter({base: '',mode: 'history',routes: routes})new Vue({router,store,render: (h) => h(App)}).$mount('#app')
}export async function mount(props) {render(props);
}
5、shared 方案
就是父应用通过 vuex 或者 redux 正常使用维护一个 state,然后创建一个 shared 实例,这个实例提供对 state 的增删改查,然后通过 props 把这个 shared 实例传给子应用,子应用使用就行。其实和上面挺相似的。
不过可以看出上面4中方案,比较适合各个应用通信比较少的情况,实时上这也是微应用的使用原则-尽量减少他们之间的通信。
但如果我们通过 vuex 或者 redux 正常使用维护一个 state,那可扩展的就多了,也比较适合较多通信的情况。
主应用:
这个 shared 实例大概是这样:
import store from "./store";class Shared {/*** 获取 Username*/public getUsername(): string {const state = store.getState();return state.username || "";}/*** 设置 Username*/public setUsername(token: string): void {// 将 token 的值记录在 store 中store.dispatch({type: "set_username",payload: username});}
}
const shared = new Shared();
export default shared;
同样的传入方式
import {registerMicroApps,start,
} from "qiankun";
import shared from './shared'
const apps = [{name: "App1",entry: '//localhost:9001',container: "#app1",activeRule: "/app1",props: {parentShared: shared}}
];registerMicroApps(apps);
start();