Qiankun

1、目标
  1. 针对事业部前端开发业务场景考虑使用一个项目实例,通过微前端的方式去实现业务功能模块的解耦。
  2. 如果可以实现微应用之间、微应用和主应用之间如何处理通信,通信数量级别
  3. 加载复杂微应用的性能如何
  4. 微前端应用场景考虑
2、什么是微前端
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

微前端架构中,我们应该按业务划分出对应的子应用,而不是通过功能模块划分子应用。

  1. 在微前端架构中,子应用并不是一个模块,而是一个独立的应用,我们将子应用按业务划分可以拥有更好的可维护性和解耦性。
  2. 子应用应该具备独立运行的能力,应用间频繁的通信会增加应用的复杂度和耦合度。

微前端解决的问题

  • 主框架不限制接入应用的技术栈,微应用具备完全自主权
  • 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
  • 在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
  • 每个微应用之间状态隔离,运行时状态不共享-

微前端解决方案

2.1、嵌套 iframe
  • 浏览器历史栈问题前进/后退无论你在iframe里潜行了多深,你退一步就是一万步,这个体验真的很难受
  • iframe 应用更新上线后,打开系统会发现系统命中缓存显示旧内容,需要用时间戳方案解决或强制刷新
  • 有时候主应用可能只想知道子系统的 URL 参数,但是 iframe 应用跟它不同源,你就得想点其他办法去获取参数了,我们最常用的就是 postMessage 了
2.2、MPA + 路由分发

优点:

  1. 多框架开发
  2. 独立部署
  3. 应用完全隔离

缺点:

  1. 体验差,每个独立应用加载时间较长
  2. 因为完全隔离,导致在导航、顶部这些通用的地方改动大,复用性变的很差。
2.3、基座模式

基于路由分发,由一个基座应用监听路由,按照路由规则去加载不同的应用,以实现应用间解耦

2.4、Webpack5 Module Federation

去中心化的微前端方案,可以在实现应用隔离的基础上,轻松实现应用间的资源共享和通信

3、什么是qiankun

qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

3.1、目前已使用qiankun的团队
  1. 美团
  2. 网易
  3. 蚂蚁金服
3.2、优势
  1. 基于 single-spa 封装,提供了更加开箱即用的 API
  2. 技术栈无关,任意技术栈的应用均可 使用/接入
  3. HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单
  4. 样式隔离,确保微应用之间样式互相不干扰
  5. JS 沙箱,确保微应用之间 全局变量/事件 不冲突
  6. 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度

微应用不宜拆分过细,建议按照业务域来做拆分。业务关联紧密的功能单元应该做成一个微应用,反之关联不紧密的可以考虑拆分成多个微应用。 一个判断业务关联是否紧密的标准:看这个微应用与其他微应用是否有频繁的通信需求。如果有可能说明这两个微应用本身就是服务于同一个业务场景,合并成一个微应用可能会更合适。

4、qiankun通信方式总结

基于vue主应用、react、vue子应用交互的例子

4.1、第一种方式

4.1.1、map实例化后赋值给组件data数据

this.map = new minemap.Map

4.1.2、通过qiankun提供的setGlobalState接口通信

actions.setGlobalState({ map: this.map })

子应用可获取到相应的map实例,但在调用该接口时页面卡顿严重,(map实例属性比较多);可以获取到地图相关属性,但是设置属性不生效

4.2、第二种方式

实例化地图组件的时候直接赋值给window变量,子应用通过监听事件onGlobalStateChange触发地图交互

伪代码:

window.map = new minemap.Map

actions.setGlobalState({ map: 'this.map' })

props.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
var center = window.map.getCenter();
window.map.flyTo({
      center: [center.lng + (Math.random() - 0.5) * 0.2,
          center.lat + (Math.random() - 0.5) * 0.2],
      zoom: 14,
      bearing: 10,
      pitch: 30,
      duration: 2000
  });
}, true);

4.3、第三种通过应用之间的通信方式进行交互不需要来回传递数据GlobalState

    // 主应用初始化数据
    const initialState = { actions: null, data: null }
    //actions用于区分各种交互动作
    // 子应用
    actions.setGlobalState({ actions: 'setBearing', data: 90 })
    // 主应用
    actions.onGlobalStateChange((state, prevState)=>{
        // console.log('主应用', state, prevState)
        // 根据不同的actions处理不同交互逻辑
        if(state.actions === 'setZoom'){
            this.map.setZoom(state.data)
        }
        if(state.actions === 'setBearing'){
            this.map.setBearing(state.data)
        }
    })

image.png

4.4、通过全局的window实现发布订阅模式


image.png

4.4.1. 借助CustomEvent对象

// 创建事件对象
    let event = new CustomEvent('custom', {
        // 这里可直接传入 自定义的事件参数
        detail: {
            height: 100,
            widht: 100,
            rect: 10000
        }
    })
    // 同样 我们也可以直接在事件对象上绑定 参数
    vent.name = 'custom-event'
    window.dispatchEvent(event)
    

4.4.2. 自定义类实现发布订阅模式

通过window共享类

4.5、通过postmessage方式

/**
 * 监听事件
 * @param {*} callback
 */
export function on(callback = () => {}) {
  window.addEventListener(
    'message',
    (event, ...arg) => {
      // 解析数据,将object进行JSON
      let { type, payload } = event?.data || {}
      if (type && payload) {
        // json
        try {
          event.data.payload = JSON.parse(payload)
        } catch (error) {}
      }
      callback(event, ...arg)
    },
    false
  )
}

/**
 * 派发事件
 * @param {*} data 数据
 * @param {*} origin  目标地址 默认location.origin
 * @param {*} source 派发数据源 默认window
 */
export function emit(data, origin, source) {
  // 解析数据,将object进行JSON
  let { type, payload } = data || {}
  if (type && payload && typeof payload === 'object') {
    // json
    data.payload = JSON.stringify(payload)
  }
  ;(source || window).postMessage(data, origin || window.location.origin)
}

4.6、以上解决办法适合通信较少的业务场景,状态池无法跟踪,通信场景较多时,容易出现状态混乱、维护困难等问题;
优点:

  • 子应用无法随意污染主应用的状态池,只能通过主应用暴露的 shared 实例的特定方法操作状态池,从而避免状态池污染产生的问题。
  • 子应用将具备独立运行的能力

缺点:

  • 暂时没有想到。。。。
    4.6.1父子应用共用store
//step1:主应用向微应用传递store实例
{
    name: "chai-project",
    entry: "//localhost:8080",
    container: '#yourContainer',
    activeRule: "/chaiQiankunTest/ffff",
    props: {
          store //共享主应用的store实例
      }
}
//step2:微应用使用主应用共享的store实例
import Vuex from 'vuex'
Vue.use(Vuex);
function render (props) {
  const store = props.store;

  // 挂载应用
  new Vue({
    store,//主应用共享的store实例
    render: (h) => h(App)
  }).$mount('#app');
}
//step3:验证主应用和微应用之间是否可以完成通信,状态同步。
//不论是点击主应用的按钮,还是点击微应用的按钮,主应用的computed属性成功被触发,微应用始终未能正常监听到状态值得改变,computed属性从未被触发。
//在微应用中将共享的store实例进行响应式设置,这是Vue现有的API方法Vue.observable(store)
//如此处理之后,不论主应用还是微应用中,修改共享store的state状态值,在主应用和微应用中都能够实时感知,并对其做出响应的反馈

4.6.2父子应用store分离方案
这里基于父应用已经共享自己的store,并且主应用和子应用之间已经能够完成对于主应用的state状态变化的响应。

Vue.prototype.microStore = microStore;

子应用的各个页面都能够通过this.microStore访问自身的store


5、实践案例中发现的问题(主应用基座vue2.,子应用分别为react、vue2.)

5.1、在开发环境中react子应用的热刷新存在bug加载子应用后子应用的ws会请求到主应用导致主应用挂掉

解决办法:

  1. 通过配置子应用取消热更新
  2. 单独指定子应用热更新请求端口(实践未成功)

5.2、在主应用的某个路由下渲染页面需要将路由设置成以下形式重点在 *

const routes = [
  {
    path: '/portal/*',
    name: 'portal',
    component: () => import('../views/Portal.vue'),
  },
];

5.3、调用qiankun的onGlobalStateChange接口监听不到时需要设置第二个参数为true

5.4、子应用调用setGlobalState的参数必须在主应用初始化的时候声明了

5.5、create-react-app生成的项目ie11加载失败 (脚手架生成项目默认不再支持ie11)

解决办法:

// 1、npm install -S react-app-polyfill
// 2、入口文件头部引入
import './public-path';
import 'react-app-polyfill/ie11';
// 3、修改package.json 或者.browserslistrc文件有关支持浏览器配置
"browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all",
      "ie 11"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version",
      "ie 11"
    ]
  },

5.6、加载vue-cli2.x版本生成的vue2.0项目报错信息:

Application died in status LOADING_SOURCE_CODE: You need to export the functional lifecycles in xxx entry
问题原因:入口js文件没有在html的最后引入




解决办法:
通过htmlwebpackplugin的chunksSortMode选项控制引入chunk顺序
'dependency' 不用说,按照不同文件的依赖关系来排序。
'auto' 默认值,插件的内置的排序方式,具体顺序这里我也不太清楚...
'none' 默认无序
{function} 提供一个函数

5.7、主应用开启严格样式隔离的情况下

startQiankun({
      sandbox: { strictStyleIsolation: true }
    })

子应用使用document会报错
子应用在加载 font-face,还有svg不生效、事件代理等问题
shadow dom 实践下来并不是可以开箱即用的方案,建议现阶段用 experimentalStyleIsolation 来解决样式隔离问题类似 scoped style 的思路

官方回复:shadow dom 后续版本不再推荐

5.8、strictStyleIsolation: true下子应用使用iconfont.js引入的图标丢失
同问题七

5.9、qiankun默认情况下微应用之间样式是隔离的,主应用与微应用之间的样式隔离可以通过手动方式进行处理

5.10、在设置默认挂载时需要注意微应用的 activeRule 需要包含主应用的这个路由 path相应子应用base选项也需要修改成基于此activeRule

setDefaultMountApp(appLink)
activeRule: '/apps/app-vue',

5.11、activeRule不能直接调用函数类似vueApp子应用

const isActive = (location) => location.pathname.includes('/MApps')
export const apps = [
  {
    name: 'reactIe',
    entry: '//localhost:3000',
    container: '#container',
    activeRule: () => isActive(location)
  },
  {
    name: 'vueApp',
    entry: '//localhost:8088',
    container: '#container',
    activeRule: isActive(location)
  }
]

5.12、同时激活两个微应用的

  1. 必须将单实例模式关闭
singular: false
  1. 子应用baseurl必须为主应用对应的路由
vue设置:
base: window.__POWERED_BY_QIANKUN__ ? '/MApps' : '/',
react设置:

同时激活两个微应用的情况下两个微应用的路由必须同步否则会出现子应用无内容渲染的情况

5.13、渲染同一个微应用的不同子页面需要在主应用配置路由跳转


    
        
        
            分组一
            vue子应用主页面
            vue子应用主line页面
        
    
 

5.14、实现以组件形式手动渲染微应用







5、15 qiankun子应用路由跳转不影响主应用路由需设置子应用路由模式

mode: 'abstract',
// 该模式下路由  ‘/’不会主动加载需要在实例化vue之后设置
router.push(data.defaultPath);

5.16主应用加载子应用不同路由页面设置

// 主应用设置通过props告诉子应用要加载的页面
loadMicroApp({
      name: 'app-vue-hash', 
      entry: 'http://localhost:1111', 
      container: '#appContainer1', 
      props: { data : { defaultPath: '/about' } }
    });
    loadMicroApp({
      name: 'app-vue-hash', 
      entry: 'http://localhost:1111', 
      container: '#appContainer2', 
      props: { data : { defaultPath: '/' } }
    })
// 子应用默认路由跳转设置
function render({ data = {} , container } = {}) {
  router = new VueRouter({
    mode: 'abstract',
    routes,
  });
  instance = new Vue({
    router,
    store,
    render: h => h(App),
  }).$mount(container ? container.querySelector('#appVueHash') : '#appVueHash');
  if (data?.defaultPath) {
    router.push(data.defaultPath);
  }
}
6、总结

1、ie下不支持多实例,没有办法做到一个页面同时加载两个子应用
2、在开箱即用上上手难度不大
3、qiankun重点关注的是技术栈无关的价值。所以在应用交互上的支持不多,数据交互较大的需求背景下需要根据自己的实际应用场景进行通信方式的选择(目前官方只有全局的state api 没有事件交互api)
4、目前两种使用qiankun的方式路由的方式和组件的方式组件的方式更加灵活

你可能感兴趣的:(Qiankun)