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响应时,匹配子应用路由并加载渲染子应用。
基座配置
csharp
//main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { registerApplication, start } from 'single-spa'
Vue.config.productionTip = false
const mountApp = (url) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = url
script.onload = resolve
script.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标签的方式挂载子应用
子应用配置
csharp
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue'
Vue.config.productionTip = false
const appOptions = {
el: '#microApp',
router,
render: h => h(App)
}
// 支持应用独立运行、部署,不依赖于基座应用
// 如果不是微应用环境,即启动自身挂载的方式
if (!process.env.isMicro) {
delete appOptions.el
new 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打包方式
csharp
//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、资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
基座配置
csharp
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('第一个微应用已挂在,可以后续操作');
});
子应用配置
csharp
// 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 库格式
csharp
const { name } = require('./package');
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
// 把微应用打包成 umd 库格式
libraryTarget: 'umd',
// webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
jsonpFunction: `webpackJsonp_${name}`
},
},
};
这里特别说明下 qiankun 加载微应用的方式除了上面那种外,如果微应用不是直接跟路由关联的时候,也可以选择手动加载微应用的方式:
csharp
import { loadMicroApp } from 'qiankun';
loadMicroApp({
name: 'app',
entry: '//localhost:7100',
container: '#yourContainer',
});
生命周期钩子注释说明
csharp
/**
* 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 可以触发所有的 观察者 函数,从而达到组件间通信的效果。
下面就是注册了一个观察者
csharp
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
csharp
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 注册文件
csharp
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 传给了子应用。
子应用:
csharp
let instance = null
let router = null
function render (props) {
console.log(props.parentActions);
// 在子应用中使用就可以访问到这个parentActions了
props.parentActions.setGlobalState({ num: 2 })
// 调用挂载在 actions 上的自定义方法,获取当前的全局 state
props.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 实例大概是这样:
csharp
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;
同样的传入方式
csharp
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();