qiankun - 基本配置

276 阅读7分钟

qiankun实践,实践之前就是分别对主应用和子应用进行改造,进而将主应用(基座)和子应用合并起来。

1.主应用需要做的事情
因为主应用主要就起到一个基座的作用,所以一般来说都会开启一个新的项目来当作主应用基座。所以这里主应用的所有操作可以归为3个步骤:创建主应用vue项目;下载qiankun;注册子应用(核心步骤); 配置:只需完成子应用相关注册

// qiankun基座的主文件中需要做:
// 1.对即将接入的主应用使用 “registerMicroApps” 函数进行注册,这样主应用才能找到子应用,需要进行注册的字段是:最主要的是三块:访问子应用的入口获取子应用资源,获取到子应用挂载到哪一个dom节点(要在主文件中存在对应id的dom),这个dom节点在哪个路由下,这样访问这个路由之后就立马加载子应用并挂载。
// 然后就是传参配置和name配置
import { registerMicroApps, start } from 'qiankun';

registerMicroApps(
[ // 参数1为注册的子应用,数组形式
  {
    
    entry: '//localhost:7100', // 子应用加载的入口
    container: '#yourContainer', // 挂载点
    activeRule: '/yourActiveRule', // 当主应用的路由发生变化时候,qiankun会监听到,然后出发匹配机制,查找注册每个子应用对象中的activeRule字符串
    name: 'react app', // 子应用名称
    props: {} // `object` - 可选,主应用需要传递给微应用的数据
  },
],
{ // 参数2为主应用的生命周期,对象形式
  beforeLoad: function() {}
  afterMount: function() {}
}
);

start(); // 启动qiankun

2.子应用需要做的配置:主要有两个地方需要配置,一个是子应用的入口文件的配置,还有一个是打包的配置文件,这两个配置一个是配合主应用调用子应用的创建时机,另一个是配合打包文件类型。 修改入口文件,并导出几个钩子函数 主要对应以下几步: 1.修改主应用入口文件,导出可提供主应用调用的钩子函数
提供三个生命函数钩子供主应用调用bootstrap、mount、unmount

/ 微应用/scr/main.js  
  
import './public-path.js'  
import Vue from 'vue'  
import App from './App.vue'  
import router from './router'  
import store from './store'  
  
let instance = null  

// 判断是否在乾坤环境下,非乾坤环境下独立运行  
if (!window.__POWERED_BY_QIANKUN__) {  
  render()  
}  

// 1. 将注册方法用函数包裹,供后续主应用与独立运行调用  
function render(props = {}) {  
  const { container } = props  
  instance = new Vue({  
    router,  
    store,  
    renderh => h(App),  
  }).$mount(container ? container.querySelector('#app-micro') : '#app-micro')  
}  
 
// 2. 导出的生命周期  
/**  
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。  
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。  
 */  
export async function bootstrap() {  
  console.log('[vue] vue app bootstraped')  
}  
  
/**  
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法  
 */  
export async function mount(props) {  
  console.log('[vue] props from main framework', props)  
  render(props);  
}  
  
/**  
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例  
 */  
export async function unmount() {  
  instance.$destroy()  
  instance.$el.innerHTML = ''  
  instance = null  
}  
  
/**  
 * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效  
 */  
export async function update(props) {  
  console.log('update props', props)  
}

2.创建public-path.js文件
首先来看下为什么需要创建一个public-path.js文件?在此之前先了解一下,webpack中存在publicj-path的地方有3个:
1 ) output 中的 public-path

output: {
  path: path.resolve(__dirname, 'dist'),
  filename: 'bundle.js',
  publicPath: '/packed/', // html文件中引入的bundle.js文件的src值为:/packed/bundle.js
}

output.path是控制打包文件的输出路径,即打包文件放在哪里。
output.publicPath则是控制浏览器访问打包文件的src访问路径,即通过HtmlWebpackPlugin插件生成html模版后,里面引入的静态资源(打包后的js文件、css文件、img文件)对应的src引入地址路径值。也就是说,设置 publicPath 的目的在于使 webpack 导出的打包文件中生成的请求 URL 可以和你的服务器设置一致。

注意:output.publicPath默认值为'',也就是说如果你不设置publicpath的话,那么根据默认值得到的src值为‘’ + filename 即为:<script type="text/javascript" src="bundle.js"></script>,如果部署的网址名为www.example.com, 那么对应发出的js请求为:www.example.com / bundle.js 。

2 ) loader 中的 options.publicPath

  • loader 中的 options.outputPath
    指定 loader 处理文件的输出文件夹,一般是相对于输出路径 output.path 的相对路径。
  • loader 中的 options.publicPath
    单独指定 loader 处理文件在浏览器中的访问路径。

这里 outputPath 指定了 loader 的输出文件的路径,通常是相对于 output.path 的路径。 这里的 publicPath 则单独指定了该 loader 处理的文件在浏览器中的访问路径。例如全局的publicPath为 /packed/,但我想单独设置图片的请求 URL,则可以指定该项,实现对不同的文件的请求路径分别设置。比如我希望 bundle.js 的请求路径为 http://www.example.com/packed/bundle.js,而图片为 http://www.example.com/images/xxx.png,则可设置处理图片的 loader 的 publicPath 为 /images/

3 ) devSever 中的 public-path

  • devServer.contentBase
    指定不受 Webpack 处理的静态文件所在目录用于直接访问,推荐使用绝对路径。
  • devServer.publicPath
    指定 webpack 打包文件如果在浏览器中的访问需要使用的路径。这里设置的是服务器 (webpack-dev-server) 如何处理请求。如果未设置以 output.publicPath 为值。

devServer.contentBase 指定的是使用 webpack-dev-server 访问非打包静态文件的方式,例如指定该值为项目根目录下的 data 目录,则其中有一个文件 1.txt 使用 webpack-dev-server 可以通过路径 http://localhost:8080/1.txt 直接访问到该文件。该参数默认值为项目根目录,使用默认值时,使用 http://localhost:8080/data/1.txt 可以访问刚刚那个文件。

例如将 devServer.publicPath 值设为 /serve/,则如果要访问 bundle.js 时,需要访问的路径为 http://localhost:8080/serve/bundle.js。显然,如果 output.path 为 /packed/,则打包时,文件中的路径为 /packed/bundle.js,此时发起的请求是 http://localhost:8080/packed/bundle.js,但是,服务器 (webpack-dev-server) 却是在 http://localhost:8080/serve/bundle.js 处理请求,因此访问 http://localhost:8080/packed/bundle.js 显然无法获得相应文件。

为了使 webpack-dev-server 正确工作,一般将 devServer.publicPath 设置为 output.publicPath

总结:outPath和publicPath的区别 path是影响文件打包之后存入的地址,而publicPath是浏览器引入静态文件的src地址。打包后的js文件和css文件最终在html中src的引用地址,静态文件的最终引用地址为:output.publicPath + output.filname 2.影响import导入的动态文件地址。对于动态文件,webpack中都是通过创建script标签来导入的,那么script标签中的src地址也就需要publicPath来确定。


const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
      ... ...
      plugins: [
         new HtmlWebpackPlugin({
             template: resolve(__dirname, 'public', 'index.html')
         }),
+        new MiniCssExtractPlugin({
+            filename: 'css/main.css'
+        })
     ],
+    module: {
+        rules: [{
+            test: /.css/,
+            use: [MiniCssExtractPlugin.loader, 'css-loader']
+        }]
+    }
}

问题2: 如何动态设置webpack中的public-path?
我们知道打包之后的html文件需要引入打包好的js和css文件,那么scr中就需要设置好对应css、js静态资源的地址。这里的地址就需要public-path和pathname的值来设置。但是上面所写的public-path的值都是一个常量的值,但是我们日常开发中可能会有多个环境:开发、测试、线上,那么不同的环境的域名是不同的,那么我们的public-path是常量,我们不可能每次上不同环境的时候都要改变一下public-path值,然后打包发布吧,那也太麻烦了,造成不必要的打包时间浪费。因此我们需要根据环境来动态设置public-path值。webpack中给我们留了一个__webpack_public_path__变量来动态设置。
注意1: 如果设置了__webpack_public_path__,那么配置中的public-path字段就是失效
注意2: 如果__webpack_public_path__设置是在单独的文件例如public-path.js中,那么主文件导入public-path.js要在最上面,其他文件导入之前,如下:

// entry.js
import './public-path';
import './app';

(参考文献:zhuanlan.zhihu.com/p/595701909…
(2)webpack-dev-server
3.设置history模式的路由base
4.修改 webpack配置:umd 打包 && 允许开发环境跨域

const { name } = require('./package.json')  
  
module.exports = {  
  devServer: {  
      port8081// 父应用配置微应用端口,要与微应用端口一致  
      disableHostChecktrue// 关闭主机检查,使微应用可以被 fetch  
      headers: {  
          'Access-Control-Allow-Origin''*' //因为qiankun内部请求都是fetch来请求资源,所以子应用必须允许跨域  
      }  
  },  
  configureWebpack: {  
      output: {  
          library`${name}-[name]`// 微应用的包名,这里与主应用中注册的微应用名称一致  
          libraryTarget'umd'// 这里设置为umd意思是在 AMD 或 CommonJS 的 require 之后可访问。  
          jsonpFunction`webpackJsonp_${name}` // webpack用来异步加载chunk的JSONP 函数。  
      }  
  }  
}

关于4的相关问题 (1) webpack中的library和libraryTarget的作用 (2)commonJS、AMD、CMD、UMD打包规范&区别 commonJS解决了js代码模块化的问题。存在问题:本质上模块的加载还是同步的,后面的代码要等到模块加载完成才能执行
AMD解决了commonJS中的加载同步问题,将模块的加载变成异步形式,这样后面代码的执行不需要等待前面模块的加载。但是在使用语法上有要求:就是所有的依赖必须提前声明,不能在要使用的地方再引入
CMD解决了AMD的依赖前置语法规则,解放了异步模块加载的语法规定,让我们在想要加载的地方再加载(amd/cmd区别: juejin.cn/post/684490…
UMD统一了模块的语法定义,兼容了以上的CommonJS、AMD、ES Module使用方式,也就是Vue脚手lib模式打包的这种模式。 参考文献:www.jianshu.com/p/741bd9ff1… 为了避免根 id #root 与其他的 DOM 冲突,需要限制查找范围。

问题1:微前端中如何利用主应用通信解决子应用重复登录的问题,即当主应用登录后,再进去微应用发现还需要登录,相当于登录状态没有同步。

qiankun的通信方式:action通信

qiankun加载的基本流程: 监听路由变化 找到相应子应用 加载子应用: 这里就是通过fetch去加载子应用,加载到的子应用返回的是html文件,返回的是html。注意,拿到html之后直接挂在到容器div中是没有办法渲染出来的。 渲染子应用

使用中遇到的问题
参考文献:juejin.cn/post/685457…

1.css隔离问题
2.子应用之间切换加载问题
3.子应用的publichPath到底应该怎么配置?

zhuanlan.zhihu.com/p/595701909…

三、qiankun中使用keep-alive问题

业务背景
对于单页来说,我们需要实现多页签对用户的访问具体页面或者组件进行状态缓存,而不用查看之前标签的时候再次重新渲染组件&接口刷新。 image.png

1.前置知识:keep-alive基本使用

(1)vue中keep-alive的使用规则
keep-alive本身就是用来做组件持久化。也就是说:keep-alive包含的组件不会被再次初始化,这意味着组件不会重新走组件的生命周期。当然,keep-alive会重新给两个active和deactive额外的生命周期来记录组件的重新挂载。

keep-alive的使用主要有两种场景
1.缓存动态组件

// 使用方式1: 直接缓存动态组件
<keep-alive>
  <component :is="componentName">
</keep-alive>

2.缓存路由组件

// 使用方式2: 缓存router组件(router-view也是一个组件,那么对应路径下的视图组件都会被缓存)
// 指定home组件和about组件被缓存
<keep-alive include="home,about" >
    <router-view></router-view>
</keep-alive>

在《keep-alive》标签内部的《roter-view》或者动态组件《component》在不展示的时候都不会立即销毁,而是缓存起来。等到下次希望再次展示这个组件的时候,直接将缓存中的拿出来直接渲染(缓存中组件的状态还是保持上次“离开”前的状态)。 注意:keep-valie几个额外配置属性

  1. keep-alive 下的组件对应的生命周期钩子 - activated(活跃),deactive(不活跃)
    虽然缓存的组件不会被销毁,但是我们需要知道缓存的组件什么时候需要展示,什么时候进入缓存队列中,这样我们可以知道什么时候去拿最新的数据。这里缓存并不是意味着我们数据也不需要更新,我们更多的是保持不用组件重新创建以及【部分】数据状态,但是仍然会存在一部分数据需要在再次展示的时候更新。所以知道keep-alive特定的生命周期也很有用。 进入的时候会执行activated,离开执行deactive,此外初始化的时候先执行created,mounted,然后才执行actived,销毁的时候deactive是在beforeDestroy执行之前。

  2. keep-alive 有选择性的缓存部分组件 - include、exclude
    如果我们希望keep-alive下同一层级的组件并不需要全部缓存,只是缓存需要的,那么可以使用include属性等于组件名称

(2)keep-alive原理
1.LRU缓存策略
2.缓存原理
3.渲染流程-初始化渲染 && 缓存渲染

const patternTypes: Array<Function> = [String, RegExp, Array] // 接收:字符串,正则,数组
 
export default {
  name: 'keep-alive',
  abstract: true, // 抽象组件,是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。
 
  props: {
    include: patternTypes, // 匹配的组件,缓存
    exclude: patternTypes, // 不去匹配的组件,不缓存
    max: [String, Number], // 缓存组件的最大实例数量, 由于缓存的是组件实例(vnode),数量过多的时候,会占用过多的内存,可以用max指定上限
  },
 
  created() {
    // 用于初始化缓存虚拟DOM数组和vnode的key
    this.cache = Object.create(null)
    this.keys = []
  },
 
  destroyed() {
    // 销毁缓存cache的组件实例
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },
 
  mounted() {
    // prune 削减精简[v.]
    // 去监控include和exclude的改变,根据最新的include和exclude的内容,来实时削减缓存的组件的内容
    this.$watch('include', (val) => {
      pruneCache(this, (name) => matches(val, name))
    })
    this.$watch('exclude', (val) => {
      pruneCache(this, (name) => !matches(val, name))
    })
  },
}

3.qiankun中使用keep-alive和vue中直接使用的区别

首先明确一下我们的需求:我们需要缓存一个个子应用,因此知道我们要缓存的不再是一个组件,而是一个个子应用。即从组件级别的vnode缓存到应用级别的缓存。因此存在以下区别和问题:
1.keep-alive使用的地方不同:因为缓存的是子应用,因此应该是主应用使用,而不是在子应用之中使用。
2.之前使用keep-alive的方式不再适用:对应的keep-alive标签中的子元素就不应该是roter-view或者component动态组件。即:

<keep-alive include="home,about" >
    <router-view></router-view> // 改为 
    <子应用></子应用>
</keep-alive>

但是问题在于,qiankun中的子应用并不是以标签的形式加载和渲染的的,而是通过html-entry-plugin来获取html文件后加载后续的js、css文件,进而渲染的。因此不存在keep-alive中放置子应用标签的使用形式。 那么对于第二点带来问题有:
1.触发时机问题:因为qiankun中加载流程是:主应用的方式是通过路由更改去触发,然后通过html-entry去加载对应的url的主文件,进而得到js和css,最后渲染。那么之前的触发都是通过路由监听去触发,但是现在是在同一个页面,因此我们现在需要标签点击立即【触发】:卸载上一个子应用,加载下一个子应用。
2.子应用加载问题:子应用的加载可以分为自动加载和手动加载

自动注册模式:registerMicroApps + start
手动加载模式:使用 loadMicroApp 手动注册微应用 & 手动
手动模式下需要通过loadMicroApp来手动加载,并且需要手动调用unmount来手动卸载。对应的使用场景是在一个页面加载多个子应用的时候,而手动加载需要微应用的功能由函数loadMicroApp,例如

const apps = [ 
{ name: 'app-vue-hash', entry: 'http://localhost:1111', container: '#appContainer1', props: { data : { store, router } } },
{ name: 'app-vue-history', entry: 'http://localhost:2222', container: '#appContainer2', props: { data : store } }
]

mounted() { // 优先加载当前的子项目 
    const path = this.$route.path; 
    const currentAppIndex = apps.findIndex(item => path.includes(item.name));    
    if(currentAppIndex !== -1) { 
        const currApp = apps.splice(currentAppIndex, 1)[0]; 
        apps.unshift(currApp);
     } // loadMicroApp 返回值是 app 的生命周期函数数组
     const loadApps = apps.map(item => loadMicroApp(item)) // 当 tab 页关闭时,调用 loadApps 中 app 的 unmount 函数即可 
 },

(分析原理:cloud.tencent.com/developer/a…

因为我们使用的是自动注册registerMicroApp模式,因此子应用的加载和卸载是通过监听路由的进入和退出执行的。如果我们希望切换后依然保存子应用的项目(进而减省重新加载渲染时间),那么就需要在路由退出的时候并不卸载子应用,因此我们就不能使用带有自动卸载的自动注册模式。而改成我们手动去控制卸载(调用unmount)和加载(loadMicroApp)。
这里是通过v-show来控制dom的显隐,子应用dom不会消失,但是子应用不卸载会带来什么问题呢?子应用不卸载就意味着DOM不清除,而随着保留的子应用越来越多,dom就会越来越多,那么必然会带来页面的卡顿。

这里提一嘴,有人会问:keep-alive不是也是缓存嘛,难道keep-alive就不会存在dom过多的问题嘛?答:不会。因为keep-alive缓存的是组件实例render生成的vnode,这个是存在js缓存中的。也就是说这里虽然不如直接保存dom来的快,缓存vnode的确多了一个渲染的过程。但是综合空间因素考虑,我们减少了组件compile的过程(compile: 对template进行编译,将AST转化后生成render function;可以分为三个阶段:解析,优化,生成render函数体)的过程。(生成render函数体之后后面就是执行render生成vnode,虚拟dom下一步再通过createNode生成真实dom)

回到我们的问题:v-show配合手动控制卸载,然后缓存不卸载子应用带来的dom过多问题,怎么解决呢? 答:缓存Dom肯定是不行的,不然也不会提出vnode的概念。因此这里的解决方案就是向keep-alive看齐,同样是缓存vnode。缓存谁的v弄的?答:缓存子应用整个vue实例,具体一点就是需要在子应用卸载之前,缓存整个子应用的vnode

注意:vue实例和vue组件的区别 之前keep-alive缓存的是vue组件的vnode,而现在我们需要缓存的是整个vue实例的vnode。那么就有来那个问题,vnode怎么拿?在哪里拿?

主应用需要做的事情:拿到子应用的render函数体或者vnode虚拟dom
这里的疑问点在于,主应用中如何拿到子应用的render函数体?

// 父应用提供unmountCache方法
function unmountCache() {
  // 此处永远只会保存首次加载生成的实例
  const needCached = this.instance?.cachedInstance || this.instance;
  const cachedInstance = {};
  cachedInstance._vnode = needCached._vnode;
  // keepalive设置为必须 防止进入时再次created,同keep-alive实现
  if (!cachedInstance._vnode.data.keepAlive) cachedInstance._vnode.data.keepAlive = true;
  // 省略其他代码...

  // loadedApplicationMap用于是key-value形式,用于保存当前应用的实例
  loadedApplicationMap[this.cacheKey] = cachedInstance;
  // 省略其他代码...

  // 卸载实例
  this.instance.$destroy();
  // 设置为null后可进行垃圾回收
  this.instance = null;
}

// 子应用在qiankun框架提供的卸载方法中,调用unmountCache
export async function unmount() {
  console.log('[vue] system app unmount');
  mainService.unmountCache();
}


子应用需要做的事情:拿到之后缓存的vnode之后,我们应该做什么?
可以将这个缓存的vnode通过新建一个vue实例挂载在对应的子应用的id节点dom上,如下:在子应用项目中,路由进入到子应用路径,然后子应用开始进入main.js文件中进行初始化,这个时候一般来说是需要执行new Vue然后开始遍历子组件模版解析,但是因为主应用中缓存了子应用的render函数,那么就可以通过父子应用通信来使子应用拿到这个render函数体,这样再次初始化的时候就不用执行之前的初始化过程了(逐个解析子组件模版),而是跳过解析直接渲染之前的render函数即可。

// 创建子应用实例,有缓存的vnode则使用缓存的vnode
function newVueInstance(cachedNode) {
  const config = {
    router: this.router,
    store: this.store,
    render: cachedNode ? () => cachedNode : instance.render, // 优先使用缓存vnode
  });
  return new Vue(config);
}

// 实例化子应用实例,根据是否有缓存vnode确定是否传入cachedNode
this.instance = newVueInstance(cachedNode);
this.instance.$mount('#app');

问题 :

  1. 我们这里缓存的是之前的状态,也就是说数据可能会改变,那么这里的render是数据更新之后render也会自动更新吗,还是说render不更新,vnode更新,如果是这种情况的话,是不是我们拿到render没有用,而必须拿到vnode才能保证状态为上一状态。 答:数据的更新肯定会导致vnode更新,而vnode的更新是因为render函数的更新,然后执行之后才会生成新的vnode,所以render和vnode是一体的,因此不存在vnode更新,render不更新的情况。所以保存render实际上就是更新后的render(可以获得更新后的vnode)。

vue更新过程:

  • 修改 data,触发 setter(此前在 getter 中已被监听)
  • 重新执行 render 函数,生成 newVnode
  • 执行 diff 算法中的 patch(vnode,newVnode)

知识点1:new一个vue实例语法

const vm = new Vue({
   el: '#root', //el用于指定当前Vue实例为哪个容器服务,值通常为css选择器字符串
   data: { //data中用于存储数据,数据供el所指定的容器去使用
       name: '我恁爹'
   }
});

通过qiankun中使用keep-alive的应用我们可以学到什么? 1.不应该仅仅沉迷于vue的组件语法,而是应该知道vue组件加载的过程,以及在这个过程中render函数的作用,vnode的作用,如果我们拿到render函数体我们可以做什么 --- 通过拿到更新之后的render就相当于拿到了更新之后的vnode。此时如果直接拿vnode去渲染,就节省了template模版解析成render函数体这段解析的时间。显然render是更快的方式。(keep-alive的做法就是利用这一点)

keep-alive重点参考文献: 1.www.cnblogs.com/vivotech/p/… 2.blog.csdn.net/qq_37252401…