• 微前端架构qiankun常见问题及解决方案


    一、qiankun 常见报错

    1、子项目未 export 需要的生命周期函数

      先检查下子项目的入口文件有没有 export 生命周期函数,再检查下子项目的打包,最后看看请求到的子项目的文件对不对。

    2、子项目加载时,容器未渲染好

      检查容器 div 是否是写在了某个路由里面,路由没匹配到所以未加载。如果只在某个路由页面加载子项目,可以在页面的 mounted 周期里面注册子项目并启动。

    二、主项目路由只能用history模式吗?

      由于 qiankun 是通过 location.pathname 值来判断当前应该加载哪个子项目的,所以需要给每个子项目注入不同的路由 path,而 hash 模式子项目路由跳转不改变 path,所以无影响,history 模式子项目路由设置 base 属性即可。

      如果主项目使用 hash 模式,那么得用 location.hash 值来判断当前应该加载哪个子项目,并且子项目都得是 hash 模式,还需要给子项目所有的路由都添加一个前缀,子项目的路由跳转如果之前使用的是 path 也需要修改,用 name 跳转则不用。

      如果主项目是 hash 模式,子项目为 history 模式,那么跳转到子项目之后,无法跳转到另一个 history 模式的子项目,也无法回到主项目的页面。

      vue 项目 hash 模式改 history 模式也很简单:

    1、new Router 时设置 modehistory

    2、webpack 打包的配置( vue.config.js ) :

    3、一些资源会报 404,相对路径改为绝对路径:<img src="./img/logo.jpg"> 改为 <img src="/img/logo.jpg"> 即可

    三、css 污染问题及加载 bug

    1、qiankun 只能解决子项目之间的样式相互污染,不能解决子项目的样式污染主项目的样式

      主项目要想不被子项目的样式污染,子项目是 vue 技术,样式可以写 css-scoped ,如果子项目是 jQuery 技术呢?所以主项目本身的 id/class 需要特殊一点,不能太简单,被子项目匹配到。

    2、从子项目页面跳转到主项目自身的页面时,主项目页面的 css 未加载的 bug

      产生这个问题的原因是:在子项目跳转到父项目时,子项目的卸载需要一点点的时间,在这段时间内,父项目加载了,插入了 css,但是被子项目的 css 沙箱记录了,然后被移除了。父项目的事件监听也是一样的,所以需要在子项目卸载完成之后再跳转。我原本想在路由钩子函数里面判断下,子项目是否卸载完成,卸载完成再跳转路由,然而路由不跳转,子项目根本不会卸载。

      临时解决办法:先复制一下 HTMLHeadElement.prototype.appendChildwindow.addEventListener ,路由钩子函数 beforeEach 中判断一下,如果当前路由是子项目,并且去的路由是父项目的,则还原这两个对象。

    const childRoute = ['/app-vue-hash','/app-vue-history'];
    const isChildRoute = path => childRoute.some(item => path.startsWith(item))
    const rawAppendChild = HTMLHeadElement.prototype.appendChild;
    const rawAddEventListener = window.addEventListener;
    router.beforeEach((to, from, next) => {
      // 从子项目跳转到主项目
      if(isChildRoute(from.path) && !isChildRoute(to.path)){
        HTMLHeadElement.prototype.appendChild = rawAppendChild;
        window.addEventListener = rawAddEventListener;
      }
      next();
    });

    四、路由跳转问题

      在子项目里面如何跳转到另一个子项目/主项目页面呢,直接写 <router-link> 或者用 router.push/router.replace 是不行的,原因是这个 router 是子项目的路由,所有的跳转都会基于子项目的 base 。写 <a> 链接可以跳转过去,但是会刷新页面,用户体验不好。

      解决办法也比较简单,在子项目注册时将主项目的路由实例对象传过去,子项目挂载到全局,用父项目的这个 router 跳转就可以了。

      但是有一丢丢不完美,这样只能通过 js 来跳转,跳转的链接无法使用浏览器自带的右键菜单(如图:Chrome 自带的链接右键菜单)

    五、项目通信问题

      项目之间的不要有太多的数据依赖,毕竟项目还是要独立运行的。通信操作需要判断是否 qiankun 模式,做兼容处理。

      通过 props 传递父项目的 Vuex ,如果子项目是 vue 技术栈,则会很好用。假如子项目是 jQuery/react/angular ,就不能很好的监听到数据的变化。

      qiakun 提供了一个全局的 GlobalState 来共享数据。主项目初始化之后,子项目可以监听到这个数据的变化,也能提交这个数据。

    // 主项目初始化
    import { initGlobalState } from 'qiankun';
    const actions = initGlobalState(state);
    // 主项目项目监听和修改
    actions.onGlobalStateChange((state, prev) => {
      // state: 变更后的状态; prev 变更前的状态
      console.log(state, prev);
    });
    actions.setGlobalState(state);
    
    // 子项目监听和修改
    export function mount(props) {
      props.onGlobalStateChange((state, prev) => {
        // state: 变更后的状态; prev 变更前的状态
        console.log(state, prev);
      });
      props.setGlobalState(state);
    }

      vue 项目之间数据传递还是使用共享父组件的 Vuex 比较方便,与其他技术栈的项目之间的通信使用 qiankun 提供的 GlobalState

    六、子项目之间的公共插件如何共享

      如果主项目和子项目都用到了同一个版本的 Vue/Vuex/Vue-Router 等,主项目加载一遍之后,子项目又加载一遍,就很浪费。

      要想复用公共依赖,前提条件是子项目必须配置 externals ,这样依赖就不会打包进 chunk-vendors.js ,才能复用已有的公共依赖。

      按需引入公共依赖,有两个层面:

    (1)没有使用到的依赖不加载

    (2)大插件只加载需要的部分,例如 UI 组件库的按需加载、echarts/lodash 的按需加载。

      webpackexternals 是支持大插件的按需引入的:

    subtract : {
       root: ['math', 'subtract']
    }

      subtract 可以通过全局 math 对象下的属性 subtract 访问(例如 window['math']['subtract'])。

    1、single-spa 可以按需引入子项目的公共依赖

      single-spa 是使用 systemJs 加载子项目和公共依赖的,将公共依赖和子项目一起配置到 systemJs 的配置文件 importmap.json ,就可以实现公共依赖的按需加载:

    {
     "imports": {
       "appVueHash": "http://localhost:7778/app.js",
       "appVueHistory": "http://localhost:7779/app.js",
       "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js",
       "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js",
       "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js",
       "echarts": "https://cdn.bootcss.com/echarts/4.2.1-rc1/echarts.min.js"
     }
    }

    2、qiankun 如何按需引入公共依赖

      巨无霸应用的公共依赖和公共函数被太多的页面使用,导致升级和改动困难,使用微前端可以让各个子项目独立拥有自己的依赖,互不干扰。而我们想要复用公共依赖,这与微前端的理念是相悖的。

      所以我的想法是:父项目提供公共依赖,子项目可以自由选择用或者不用。

      这个也很好实现,父项目先加载好依赖,然后在注册子项目时,将 Vue/Vuex/Vue-Router 等通过 props 传过去,子项目可以选择用或者不用。

      主项目:

    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'
    import { registerMicroApps, start } from 'qiankun';
    import Vuex from 'vuex';
    import VueRouter from 'vue-router';
    
    new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount("#app");
    
    registerMicroApps([
      { 
        name: 'app-vue-hash', 
        entry: 'http://localhost:1111', 
        container: '#appContainer', 
        activeRule: '/app-vue-hash', 
        props: { data : { store, router, Vue, Vuex, VueRouter } }
      },
    ]);
    
    start();

      子项目:

    import Vue from 'vue'
    
    export async function bootstrap() {
      console.log('vue app bootstraped');
    }
    
    export async function mount(props) {
      console.log('props from main framework', props);
      const { VueRouter, Vuex } = props.data;
      Vue.use(VueRouter);
      Vue.use(Vuex);
      render(props.data);
    }
    
    export async function unmount() {
      instance.$destroy();
      instance = null;
      router = null;
    }

      这样做不太可行,原因有两个:

    (1)子项目独立运行时,Vue-Router/Vuex这些依赖从哪里来?子项目是只部署一份的,既可以独立运行,也可以被 qiankun 集成。

    (2)父项目只能传递它自己已经有的依赖,如何确定子项目需要哪些依赖?不满足按需引入的需求

      配置 webpackexternals 之后,子项目独立运行时,这些依赖的来源有且仅有 index.html 中的外链 script 标签。

      在这个前提下,子项目和主项目的 vue 版本一致的情况下,使用同一份服务器文件。即使无法共享,也是可以做 http 缓存的。

      那么 qiankun 能否做到,某个依赖加载了之后,不再加载,直接复用呢?比如说子项目 A 请求了服务器上的 2.6 版本 vue,切换到子项目 B,B 项目也用了这个 vue 文件,能否不再次加载,直接复用呢?

      其实是可以的,可以看到 qiankun 将子项目的外链 script 标签,内容请求到之后,会记录到一个全局变量中,下次再次使用,他会先从这个全局变量中取。这样就会实现内容的复用,只要保证两个链接的url一致即可。

    const fetchScript = scriptUrl => scriptCache[scriptUrl] ||
            (scriptCache[scriptUrl] = fetch(scriptUrl).then(response => response.text()));

      所以只要子项目配置了 webpackexternals,并在 index.html 中使用外链 script 引入这些公共依赖,只要这些公共依赖在同一台服务器上,便可以实现子项目的公共依赖的按需引入,一个项目使用了之后,另一个项目使用不再重复加载,可以直接复用这个文件。

    3、qiankun 更完美的按需引入

      虽然 qiankun 不会重复请求相同 url 的公共依赖,但是这也仅比 http 缓存强了一丢丢。有缺陷的地方在于:

    (1)主项目中的公共依赖没有记录到这个缓存中,也就不会被其他的项目复用

    (2)只是没有重复请求,还是需要重复执行一次。能否不执行,直接复用?。js 沙箱在子项目卸载时,会移除 window 上新增的变量,而 webpackexternals恰恰是将这些公共依赖挂载在 window 上,能否看情况移除这些公共依赖?

    (3)相同版本的依赖会复用,版本不同但是使用无差别,能否做到也复用?(版本不同 url 也就不同,就不会复用)但是这里可能会有一些疑问,既然使用无差别,为什么不升级插件?

      这些问题可能需要去改动 qiankun 的源码。

    七、jQuery 老项目的资源加载问题

      子项目的内容标签插到父项目的 index.html 后,其中的资源( img/video/audio 等)路径都是相对的,导致资源无法正确显示。上面我列举了三种解决方案。

      一般来说,jQuery 项目是不经过 webpack 打包的,所以没法通过修改 publicPath 来注入路径前缀。后面两种方法操作起来比较麻烦,或者说我们应该优先从框架本身解决这个问题,而不是其他方法。所以我想了如下三种方案:

    方案一:动态插入 <base> 标签

      html 有一个原生标签 <base>,这个标签只能放在 <head> 里面,它的 href 属性是一个 url 值。

      mdn 地址: base 文档根 URL 元素

      设置了 <base> 标签之后,页面上所有的链接和 url 都基于它的 href。例如页面访问地址是 https://www.taobao.com ,设置 <base href="https://www.baidu.com"> 之后,页面中原本的图 <img src="./img/jQuery1.png" alt=""> 的实际请求地址会变成 https://www.baidu.com/img/jQuery1.png ,页面上的 <a> 链接:<a href="/about"></a>,点击之后,页面会跳转到:https://www.baidu.com/about

      可以看到,<base> 标签和 webpackpublicPath 有一样的效果,那么能否在 jQuery 项目加载之前,把 jQuery 项目的地址赋给 <base> 标签,然后插入到 <head> ?这样就可以解决 jQuery 项目的资源加载问题。

      做法也很简单,在 qiankun 提供的 beforeLoad 生命周期,判断当前是否是 jQuery 项目:

    beforeLoad: app => {
       if(app.name === 'purehtml'){
           const baseTag = document.createElement('base');
           baseTag.setAttribute('href',app.entry);
           console.log(baseTag);
           document.head.appendChild(baseTag);
       }
    },
    beforeUnmount: app => {
       if(app.name === 'purehtml'){
          const baseTag = document.head.querySelector('base');
          document.head.removeChild(baseTag);
       }
    }

      这样做子项目资源可以正确加载,但是 <base> 标签的威力太强大了,会导致所有的路由无法正常跳转,跳转到其他的子项目时,<a> 链接是基于 <base> 的,会跳转到 jQuery 子项目的不存在的路由。解决了一个 bug ,又出现了新的 bug ,这样是不行的。所以这个方案可行性特别小。

    方案二:劫持标签插入函数

      这个方案分两步:

    (1)对于 HTML 中已有的 img/audio/video 等标签,qiankun 支持重写 getTemplate 函数,可以将入口文件 index.html 中的静态资源路径替换掉

    (2)对于动态插入的 img/audio/video 等标签,劫持 appendChildinnerHTMLinsertBefore 等事件,将资源的相对路径替换成绝对路径

      前面我们说到,对于子项目是 HTML entry 的,qiankun 拿到入口文件 index.html 之后,会用正则匹配到 <body> 标签及其内容,<head> 中的 link/style/script/meta 等标签,然后插入到父项目的容器中。

      我们可以传递一个 getTemplate 函数,将图片的相对路径转为绝对路径,它会在处理模板时使用:

    start({
      getTemplate(tpl,...rest) {
        // 为了直接看到效果,所以写死了,实际中需要用正则匹配
        return tpl.replace('<img src="./img/jQuery1.png">', '<img src="http://localhost:3333/img/jQuery1.png">');
      }
    });

      对于动态插入的标签,劫持其插入 DOM 的函数,注入前缀。

      假如子项目动态插入一张图:

    const render = $ => {
      $('#purehtml-container').html('<p>Hello, render with jQuery</p><img src="./img/jQuery2.png">');
      return Promise.resolve();
    };

      主项目劫持 jQueryhtml 方法:

    beforeMount: app => {
       if(app.name === 'purehtml'){
           // jQuery 的 html 方法是一个挺复杂的函数,这里只是为了看效果,简写了
           $.prototype.html = function(value){
              const str = value.replace('<img src="/img/jQuery2.png">', '<img src="http://localhost:3333/img/jQuery2.png">')
              this[0].innerHTML = str;
           }
       }
    }

      当然了,还有个简单粗暴的写法,给 jQuery 项目的图片路径写成绝对路径,但是不建议这么做,换个服务器部署就不能用了。

    方案三:给 jQuery 项目加上 webpack 打包

      这个方案的可行性不高,都是陈年老项目了,没必要这样折腾。

    老项目的资源加载总结

      qiankun 本身就对接入 jQuery 多页应用比较乏力,一般使用场景就是,一个大项目只接入某个/某几个页面,这样的话使用方案二比较合理。

    八、qiankun 使用总结

    1、只有一个子项目时,要想启用预加载,必须使用start({ prefetch: 'all' })

    2、js 沙箱并不能解决所有的 js 污染,例如我用 onclickaddEventListener<body> 添加了一个点击事件,js 沙箱并不能消除它的影响,所以说,还得靠代码规范和自己自觉

    3、qiankun 框架不太好实现 keep-alive 需求,因为解决 css/js 污染的办法就是删除子项目插入的 css 标签和劫持 window 对象,卸载时还原成子项目加载前的样子,这与 keep-alive 相悖: keep-alive 要求保留这些,仅仅是样式上的隐藏。

    4、qiankun 无法很好嵌入一些老项目。

      虽然 qiankun 支持 jQuery 老项目,但是似乎对多页应用没有很好的解决办法。每个页面都去修改,成本很大也很麻烦,但是使用 iframe 嵌入这些老项目就比较方便。

    5、安全和性能的问题

      qiankun 将每个子项目的 js/css 文件内容都记录在一个全局变量中,如果子项目过多,或者文件体积很大,可能会导致内存占用过多,导致页面卡顿。

      另外,qiankun 运行子项目的 js,并不是通过 script 标签插入的,而是通过 eval 函数实现的,eval 函数的安全和性能是有一些争议的:MDN的eval介绍

    6、微前端调试时,每次都需要分别进入子项目和主项目运行和打包,非常麻烦,可以使用 npm-run-all 插件来实现:一个命令,运行所有项目。

    {
      "scripts": {
        "install:hash": "cd app-vue-hash && npm install",
        "install:history": "cd app-vue-history && npm install",
        "install:main": "cd main && npm install",
        "install:purehtml": "cd purehtml && npm install",
        "install-all": "npm-run-all install:*",
        "start:hash": "cd app-vue-hash && npm run serve ",
        "start:history": "cd app-vue-history && npm run serve",
        "start:main": "cd main && npm run serve",
        "start:purehtml": "cd purehtml && npm run serve",
        "start-all": "npm-run-all --parallel start:*",
        "serve-all": "npm-run-all --parallel start:*",
        "build:hash": "cd app-vue-hash && npm run build",
        "build:history": "cd app-vue-history && npm run build",
        "build:main": "cd main && npm run build",
        "build-all": "npm-run-all --parallel build:*"
      }
    }

      其中 --parallel 参数表示并行,没有这个参数则是等上一个命令执行完才会执行下一个命令。

    结尾

      不要对 iframe 抱有偏见,它也是微前端的一种实现方式,如果页面上无弹窗、无全屏等操作,iframe 也是很好用的。配置缓存和 cdn 加速,如果是内网访问,也不会很慢。

      iframeqiankun 可以并存,jQuery 多页应用使用 iframe 接入就挺好,什么时候什么场景该用哪种方案,具体情况具体分析。

      文章内容有一些补充,但是这篇文章写不下了,请看:qiankun 微前端实践总结(二)

    附录

      single-spaqiankundemo如何实现以及部分原理浅析,可以看这三篇文章:

    1. 从0实现一个前端微服务(上)
    2. 从0实现一个single-spa的前端微服务(中)
    3. 从0实现一个single-spa的前端微服务(下)

      第三篇文章是今年3月份写的,里面涉及的 qiankun 源码是 1.0 版本,qiankun 在4月份发布了2.0版本,但是基本原理大致没变。

      行业内其他前端团队对微前端的看法和实践:

    1. 每日优鲜供应链前端团队微前端改造
    2. 微前端在美团外卖的实践
    3. 前端微服务在字节跳动的打磨与应用
    4. 微前端在小米 CRM 系统的实践
    5. 标准微前端架构在蚂蚁的落地实践

      qiankun 的在线案例

    1. tech.antfin.com/partners
    2. www.zstack.io/
    作者:沉末_,链接:https://juejin.cn/post/6844904185910018062
  • 相关阅读:
    java SE :文件基本处理 File、FileFilter、FileNameFilter
    java SE :标准输入/输出
    java EE :GenericServlet 抽象类、ServletConfig 接口
    java EE :Servlet 接口
    java EE : http 协议响应头部信息验证
    java EE : http 协议之请求报文、响应报文
    java EE : tomacat 基础
    06 java 基础:java 循环 递归
    05 java 基础:运算符、程序结构
    04 java 基础:数据类型
  • 原文地址:https://www.cnblogs.com/goloving/p/14881461.html
Copyright © 2020-2023  润新知