qiankun框架技术难点

子应用的路由一定要加对应的前缀吗?

不需要,判断一下是否在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>

猜你喜欢

转载自blog.csdn.net/wyh666csdn/article/details/128651302