子应用的路由一定要加对应的前缀吗?
不需要,判断一下是否在qiankun中即可,在的话再加前缀,不在就不加
qiankun的props传参
在主应用的micro-app.js中我们如果写了props,在我们的子应用的mount钩子函数的参数会获取到一个包含props的对象,为什么除了props的数据还有其他属性,这是因为qiankun帮我们将props的数据解构到一个对象中了,这个对象包含以下六个属性,这个对象就是我们最终在子应用mount打印的参数了
区分开发环境与生产环境的地方
1.主应用的micro-app.js需要区分,具体在microApps数组的entry中
2.子应用的路由配置需要区分环境
getGlobalState设计
如果我们直接使用官方的这个示例,那么数据会比较松散且调用复杂,所有子应用都得声明onGlobalStateChange对状态进行监听,再通过setGlobalState进行更新数据。
因此,我们很有必要对数据状态做进一步的封装设计。笔者这里主要考虑以下几点:
主应用要保持简洁简单,对子应用来说,主应用下发的数据就是一个很纯粹的object,以便更好地支持不同框架的子应用,因此主应用不需用到vuex。
vue子应用要做到能继承父应用下发的数据,又支持独立运行。
子应用在mount声明周期可以获取到最新的主应用下发的数据,然后将这份数据注册到一个名为global的vuex module中,子应用通过global module的action动作进行数据的更新,更新的同时自动同步回父应用。
主应用的初始化状态文件
store.js
import {
initGlobalState } from 'qiankun'
import Vue from 'vue'
// 父应用的初始state
// Vue.observable是为了让initialState变成可响应:https://cn.vuejs.org/v2/api/#Vue-observable。
const initialState = Vue.observable({
user: {
name: 'zhangsan'
}
})
const actions = initGlobalState(initialState)
actions.onGlobalStateChange((newState, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log('main change', JSON.stringify(newState), JSON.stringify(prev))
for (const key in newState) {
initialState[key] = newState[key]
}
})
// 定义一个获取state的方法下发到子应用
actions.getGlobalState = (key) => {
// 有key,表示取globalState下的某个子级对象
// 无key,表示取全部
return key ? initialState[key] : initialState
}
export default actions
注意:
1.Vue.observable是让数据变为响应式,不加的话数据会改变,但是页面不会刷新,主要为了实现基座中的子应用修改state,基座在菜单栏会刷新的需求。
2.封装getGlobalState方法获得state,实现onGlobalStateChange的数据监听并改变(set数据的时候会触发)
然后这个getGlobalState方法通过props下发给子应用
vue子应用的状态封装
前面说了,子应用在mount时会将父应用下发的state,注册为一个叫global的vuex module,为了方便复用我们封装一下:
// sub-vue/src/store/global-register.js
/**
*
* @param {vuex实例} store
* @param {qiankun下发的props} props
*/
function registerGlobalModule(store, props = {
}) {
if (!store || !store.hasModule) {
return;
}
// 获取初始化的state
const initState = props.getGlobalState && props.getGlobalState() || {
menu: [],
user: {
}
};
// 将父应用的数据存储到子应用中,命名空间固定为global
if (!store.hasModule('global')) {
const globalModule = {
namespaced: true,
state: initState,
actions: {
// 子应用改变state并通知父应用
setGlobalState({
commit }, payload) {
commit('setGlobalState', payload);
commit('emitGlobalState', payload);
},
// 初始化,只用于mount时同步父应用的数据
initGlobalState({
commit }, payload) {
commit('setGlobalState', payload);
},
},
mutations: {
setGlobalState(state, payload) {
// eslint-disable-next-line
state = Object.assign(state, payload);
},
// 通知父应用
emitGlobalState(state) {
if (props.setGlobalState) {
props.setGlobalState(state);
}
},
},
};
store.registerModule('global', globalModule);
} else {
// 每次mount时,都同步一次父应用数据
store.dispatch('global/initGlobalState', initState);
}
};
export default registerGlobalModule;
main.js中添加global-module的使用:
在子应用mount时去触发这个函数将下发的数据存到vuex中
import globalRegister from './store/global-register'
export async function mount(props) {
console.log('[vue] props from main framework', props)
globalRegister(store, props)
render(props)
}
ps: 该方案也是有缺点的,由于子应用是在mount时才会同步父应用下发的state的。因此,它只适合每次只mount一个子应用的架构(不适合多个子应用共存);若父应用数据有变化而子应用又没触发mount,则父应用最新的数据无法同步回子应用。想要做到多子应用共存且父动态传子,子应用还是需要用到qiankun提供的onGlobalStateChange的api监听才行,有更好方案的同学可以分享讨论一下。该方案刚好符合笔者当前的项目需求,因此够用了,请同学们根据自己的业务需求来封装。
在基座运行的子应用进行点击跳转到另外一个子应用,并且实现基座的顶部菜单栏也切换
需求:
除了点击页面顶部的菜单切换子应用,我们的需求也要求子应用内部跳其他子应用,这会涉及到顶部菜单active状态的展示问题:sub-vue切换到sub-react,此时顶部菜单需要将sub-react改为激活状态。
由于qiankun暂时没有封装子应用向父应用抛出事件的api,所以采用父应用监听history.pushState事件,当发现路由换了,父应用从而知道要不要改变激活状态。
分析:
子应用跳转是通过history.pushState(null, ‘/sub-react’, ‘/sub-react’)的,因此父应用在mounted时想办法监听到history.pushState就可以了。由于history.popstate只能监听back/forward/go却不能监听history.pushState,所以需要额外全局复写一下history.pushState事件。
注意:为什么不用其他跳转方式呢?
a标签跳转会刷新页面,原来的状态都会丢失
使用window.location.href跳转会出现一个一闪而过的白屏,体验不好,同时也不能传参
使用this.$router.push()跳转会带上原有的routerbase,只适合在子应用间控制路由跳转,不适合应用之间的跳转。
解决方法
通过history.pushState()方式跳转
该方法不会刷新页面,只会更改url
问题:虽然这个可以解决基座内子应用内跳子应用,但是无法向基座暴露出路由改变的事件,基座不知道路由跳了,导致一个问题是子应用跳过去了,但是基座的顶部菜单栏没有切换,如下图
解决:父应用在mounted时想办法监听到history.pushState就可以了。由于没有onpushState事件,所以需要额外全局复写一下history.pushState事件。然后使用addEventListener才能做监听
// main/src/App.vue
export default {
methods: {
bindCurrent () {
const path = window.location.pathname
if (this.microApps.findIndex(item => item.activeRule === path) >= 0) {
this.current = path
}
},
listenRouterChange () {
const _wr = function (type) {
const orig = history[type]
return function () {
const rv = orig.apply(this, arguments)
const e = new Event(type)
e.arguments = arguments
window.dispatchEvent(e)
return rv
}
}
history.pushState = _wr('pushState')
window.addEventListener('pushState', this.bindCurrent)
window.addEventListener('popstate', this.bindCurrent)
this.$once('hook:beforeDestroy', () => {
window.removeEventListener('pushState', this.bindCurrent)
window.removeEventListener('popstate', this.bindCurrent)
})
}
},
mounted () {
this.listenRouterChange()
}
}
解析onpushState的实现原理
const _wr = function (type) {
const orig = history[type]
return function () {
console.log(this,'11111111111111111')
const rv = orig.apply(this,arguments)
const e = new Event(type)
e.arguments = arguments
window.dispatchEvent(e)
return rv
}
}
history.pushState = _wr('pushState')
window.addEventListener('pushState', this.bindCurrent)
首先为什么要写成闭包的形式,直接写成 const rv = history[type].apply(this,arguments)不行吗?
不行,因为仔细分析发现当我们执行history.pushState(null, ‘’, ‘/aaa’)这个代码时,才会去调用_wr函数return的函数,就是执行return的函数,所以如果写成那样,执行history[type].apply(this,arguments)时会再次触发history.pushState事件再去执行return的函数,这样就无限循环了。写成闭包只会执行一次apply,并且这次apply调用的history.pushState不是重写过的,是闭包的那个原先的history.pushState,所以不会循环。
其次这里apply的this是指向history的,原因是因为return的函数被调用时是这个形式history.pushState(null, ‘’, ‘/aaa’),this就是history对象
new Event是自定义一个事件对象,window.dispatchEvent(e)是触发这个事件。也就是说每次执行history.pushState(null, ‘’, ‘/aaa’)时都会去dispatchEvent触发事件,这样就实现了监听。
基座中需要路由文件吗?
不需要,那重定向怎么做?
怎么让http://localhost:8080/打开自动访问http://localhost:8080/sub-vue呢?
使用乾坤的setDefaultMountApp即可
在main.js中引入setDefaultMountApp
49行之间指定默认加载的子应用就行
顶部菜单默认的激活效果
需求:我们第一次打开基座的子应用,菜单栏要获取到是哪个子应用并且设置激活的类名
基座的APP.vue写个method
bindCurrent () {
const path = window.location.pathname
if (this.microApps.findIndex(item => item.activeRule === path) >= 0) {
this.current = path
}
在created钩子函数调用即可
菜单导航
item.activeRule === current加类名
<ul class="sub-apps">
<li v-for="item in microApps" :class="{active: item.activeRule === current}" :key="item.name" @click="goto(item)">{
{
item.name }}</li>
</ul>