• 微前端二:qiankun


    qiankun是基于Single-spa开发的框架,所以我们先来看下Single-spa是怎么做的:

    Single-spa 是最早的微前端框架,兼容多种前端技术栈,是一个将多个单页面应用聚合为一个整体应用的 JavaScript 微前端框架;

    优点:
    1、敏捷性 - 独立开发、独立部署,微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新;
    2、技术栈无关,主框架不限制接入应用的技术栈,微应用具备完全自主权;
    3、增量升级,在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
    4、更快交付客户价值,有助于持续集成、持续部署以及持续交付;
    5、维护和 bugfix 非常简单,每个团队都熟悉所维护特定的区域;

    缺点:
    1、无通信机制
    2、不支持 Javascript 沙箱
    3、样式冲突
    4、无法预加载

    Single-spa实现原理
    首先在基座应用中注册所有App的路由,single-spa保存各子应用的路由映射关系,充当微前端控制器Controler。URL响应时,匹配子应用路由并加载渲染子应用。

    基座配置

    //main.js
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import { registerApplication, start } from 'single-spa'
    
    Vue.config.productionTip = false
    
    const mountApp = (url) => {
      return new Promise((resolve, reject) => {
        const script = document.createElement('script')
        script.src = url
    
        script.onload = resolve
        script.onerror = reject
    
        // 通过插入script标签的方式挂载子应用
        const firstScript = document.getElementsByTagName('script')[]
        // 挂载子应用
        firstScript.parentNode.insertBefore(script, firstScript)
      })
    }
    
    const loadApp = (appRouter, appName) => {
    
      // 远程加载子应用
      return async () => {
        //手动挂载子应用
        await mountApp(appRouter + '/js/chunk-vendors.js')
        await mountApp(appRouter + '/js/app.js')
        // 获取子应用生命周期函数
        return window[appName]
      }
    }
    
    // 子应用列表
    const appList = [
      {
        // 子应用名称
        name: 'app1',
        // 挂载子应用
        app: loadApp('http://localhost:8083', 'app1'),
        // 匹配该子路由的条件
        activeWhen: location => location.pathname.startsWith('/app1'),
        // 传递给子应用的对象
        customProps: {}
      },
      {
        name: 'app2',
        app: loadApp('http://localhost:8082', 'app2'),
        activeWhen: location => location.pathname.startsWith('/app2'),
        customProps: {}
      }
    ]
    
    // 注册子应用
    appList.map(item => {
      registerApplication(item)
    })
     
    // 注册路由并启动基座
    new Vue({
      router,
      mounted() {
        start()
      },
      render: h => h(App)
    }).$mount('#app')
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    构建基座的核心是:配置子应用信息,通过registerApplication注册子应用,在基座工程挂载阶段start启动基座。
    我们通过代码也发现 Single-spa 是通过插入script标签的方式挂载子应用

    子应用配置

    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import singleSpaVue from 'single-spa-vue'
    
    Vue.config.productionTip = false
    
    const appOptions = {
      el: '#microApp',
      router,
      render: h => h(App)
    }
    
    // 支持应用独立运行、部署,不依赖于基座应用
    // 如果不是微应用环境,即启动自身挂载的方式
    if (!process.env.isMicro) {
      delete appOptions.el
      new Vue(appOptions).$mount('#app')
    }
    // 基于基座应用,导出生命周期函数
    const appLifecycle = singleSpaVue({
      Vue,
      appOptions
    })
    
    // 抛出子应用生命周期
    // 启动生命周期函数
    export const bootstrap = (props)  => {
      console.log('app2 bootstrap')
      return appLifecycle.bootstrap(() => { })
    }
    // 挂载生命周期函数
    export const mount = (props) => {
      console.log('app2 mount')
      return appLifecycle.mount(() => { })
    }
    // 卸载生命周期函数
    export const unmount = (props) => {
      console.log('app2 unmount')
      return appLifecycle.unmount(() => { })
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    配置子应用为umd打包方式

    //vue.config.js
    const package = require('./package.json')
    module.exports = {
      // 告诉子应用在这个地址加载静态资源,否则会去基座应用的域名下加载
      publicPath: '//localhost:8082',
      // 开发服务器
      devServer: {
        port: 8082
      },
      configureWebpack: {
        // 导出umd格式的包,在全局对象上挂载属性package.name,基座应用需要通过这个
        // 全局对象获取一些信息,比如子应用导出的生命周期函数
        output: {
          // library的值在所有子应用中需要
          library: package.name,
          libraryTarget: 'umd'
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    子应用配置的核心是用singleSpaVue生成子路由配置后,必须要抛出其生命周期函数。
    用以上方式便可轻松实现一个简单的微前端应用了。

    那么我们有single-spa这种微前端解决方案,qiankun 又在此基础上做了什么呢

    相比于single-spa,qiankun他解决了JS沙盒环境,不需要我们自己去进行处理。在single-spa的开发过程中,我们需要自己手动的去写调用子应用JS的方法(如上面的 createScript方法),而qiankun不需要,乾坤只需要你传入响应的apps的配置即可,会帮助我们去加载。

    Qiankun

    1、基于 single-spa 封装,提供了更加开箱即用的 API。
    2、技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
    3、HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
    4、样式隔离,确保微应用之间样式互相不干扰。
    5、JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
    6、资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。

    基座配置

    import { registerMicroApps, start, runAfterFirstMounted, setDefaultMountApp } from 'qiankun';
    
    const microApps = [
    	{
        name: 'reactApp',
        entry: '//localhost:3000',
        container: '#container',
        activeRule: '/app-react',
        loader: (loading: boolean) => void - 可选,loading 状态发生变化时会调用的方法。
    	props: {} - 可选,主应用需要传递给微应用的数据。
      },
      {
        name: 'vueApp',
        entry: '//localhost:8080',
        container: '#container',
        activeRule: '/app-vue',
      },
      {
        name: 'angularApp',
        entry: '//localhost:4200',
        container: '#container',
        activeRule: '/app-angular',
      }
    ]
    /**
    	beforeLoad - Lifecycle | Array - 可选
    	beforeMount - Lifecycle | Array - 可选
    	afterMount - Lifecycle | Array - 可选
    	beforeUnmount - Lifecycle | Array - 可选
    	afterUnmount - Lifecycle | Array - 可选
    */
    registerMicroApps(
      microApps, 
      {
         beforeLoad: (app) => console.log('before load', app.name),
         beforeMount: [(app) => console.log('before mount', app.name)],
      }
    );
    // 启动 qiankun
    start();
    // 设置主应用启动后默认进入的微应用。
    setDefaultMountApp('/app-react');
    // 第一个微应用 mount 后需要调用的方法,比如开启一些监控或者埋点脚本。
    runAfterFirstMounted(() => {
      console.log('第一个微应用已挂在,可以后续操作');
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    子应用配置

    // main.ts
    function render(props: any = {}) {
      if (Object.keys(props).length === 0) return;
      const { userInfo } = props;
      store.commit('SetGlobalObj', props);
      store.commit('SetUserInfo', userInfo);
      instance = new Vue({
        router,
        store,
        render: h => h(App)
      }).$mount(container ? container.querySelector('#app') : '#app');
    }
    
    let win:any = window;
    // 判断是否是qiankun环境,兼容非微前端环境
    if (!win.__POWERED_BY_QIANKUN__) {
      const router = getRoute('/dashboard/detail/information');
      new Vue({
        el: "#app",
        router,
        store,
        render: h => h(App)
      });
    }
    
    export async function bootstrap() {
      console.log('[vue] vue app bootstraped');
    }
    
    export async function mount(props: any) {
      render(props);
    }
    
    export async function unmount() {
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    修改 webpack 配置

    这个跟Single-spa 差不多,主要是 把微应用打包成 umd 库格式

    const { name } = require('./package');
    module.exports = {
      devServer: {
        headers: {
          'Access-Control-Allow-Origin': '*',
        },
      },
      configureWebpack: {
        output: {
          library: `${name}-[name]`,
          // 把微应用打包成 umd 库格式
          libraryTarget: 'umd', 
          // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
          jsonpFunction: `webpackJsonp_${name}`
        },
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这里特别说明下 qiankun 加载微应用的方式除了上面那种外,如果微应用不是直接跟路由关联的时候,也可以选择手动加载微应用的方式

    import { loadMicroApp } from 'qiankun';
    
    loadMicroApp({
      name: 'app',
      entry: '//localhost:7100',
      container: '#yourContainer',
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    生命周期钩子注释说明

    /**
     * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
     * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
     */
    export async function bootstrap() {
      console.log('react app bootstraped');
    }
    
    /**
     * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
     */
    export async function mount(props) {
      ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
    }
    
    /**
     * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
     */
    export async function unmount(props) {
      ReactDOM.unmountComponentAtNode(
        props.container ? props.container.querySelector('#root') : document.getElementById('root'),
      );
    }
    
    /**
     * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
     */
    export async function update(props) {
      console.log('update props', props);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    qiankun的通信方式

    1、localStorage/sessionStorage
    2、通过路由参数共享
    3、官方提供的 props
    4、官方提供的 actions
    5、使用vuex或redux管理状态,通过shared分享

    1、localStorage/sessionStorage

    有人说这个方案必须主应用和子应用是同一个域名下。其实不是的,子应用使用不同的域名也是可以的,因为在 qiankun 中,主应用是通过 fetch 来拉取子应用的模板,然后渲染在主应用的 dom 上的,说白了还是运行在主应用上,所以还是运行在同一个域名上,也就是主应用的域名。

    父传子
    主应用 main.js
    localStorage.setItem(‘token’, ‘123’)
    console.log(‘在main中设置了token’)

    子应用app1 main.js
    const token = localStorage.getItem(‘token’)
    console.log(‘app1中打印token:’, token)

    子传父
    同理app1修改token,main也可以看到,这里就不再赘述

    2、通过路由参数共享

    这个也很好理解,因为只有一个 url,不管子应用还是主应用给 url 上拼接一些参数,那么父子应用都可以通过 route 来获取到。

    3、官方提供的 props

    这个在前面已经说过了就是registerMicroApps注册微应用时,通过props传送信息

    4、官方提供的 actions

    就一个 API initGlobalState
    qiankun 内部提供了 initGlobalState 方法用于注册 MicroAppStateActions 实例用于通信,该实例有三个方法,分别是:
    setGlobalState:设置 globalState - 设置新的值时,内部将执行 浅检查,如果检查到 globalState 发生改变则触发通知,通知到所有的 观察者 函数。
    onGlobalStateChange:注册 观察者 函数 - 响应 globalState 变化,在 globalState 发生改变时触发该 观察者 函数。
    offGlobalStateChange:取消 观察者 函数 - 该实例不再响应 globalState 变化

    在这里插入图片描述

    我们从上图可以看出,我们可以先注册 观察者 到观察者池中,然后通过修改 globalState 可以触发所有的 观察者 函数,从而达到组件间通信的效果。

    下面就是注册了一个观察者

    const actions: MicroAppStateActions = initGlobalState({});
    actions.onGlobalStateChange((state, prev) => {
      // state: 变更后的状态; prev 变更前的状态
    });
    setTimeout(() => {
      actions.setGlobalState(Object.assign({ username: 'Lee', obj: { token: 222 } }));
    }, 1000);
    // actions.offGlobalStateChange();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们来看具体应用:
    父传子
    主应用:
    actions.js

    import { initGlobalState, MicroAppStateActions } from 'qiankun';
    
    const state = {
      num: 1
    };
    
    // 初始化 state
    const actions: MicroAppStateActions = initGlobalState(state);
    
    actions.onGlobalStateChange((state, prev) => {
      // state: 变更后的状态; prev 变更前的状态
      console.log('主应用检测到state变更:', state, prev);
    });
    
    // 你还可以定义一个获取state的方法下发到子应用
    actions.getGlobalState = function () {
      return state
    }
    
    export default actions;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    index.js 注册文件

    import {
      registerMicroApps,
      start,
    } from "qiankun";
    import actions from './actions.js'
    const apps = [
      {
        name: "App1MicroApp",
        entry: '//localhost:9001',
        container: "#app1",
        activeRule: "/app1",
        props: {
          parentActions: actions
        }
      }
    ];
    
    registerMicroApps(apps);
    start();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这样就把这个 actions 传给了子应用。
    子应用:

    let instance = null
    let router = null
    
    function render (props) {
      console.log(props.parentActions);
      // 在子应用中使用就可以访问到这个parentActions了
      props.parentActions.setGlobalState({ num: 2 })
      // 调用挂载在 actions 上的自定义方法,获取当前的全局 state
      props.parentActions.getGlobalState();
      router = new VueRouter({
        base: '',
        mode: 'history',
        routes: routes
      })
      new Vue({
        router,
        store,
        render: (h) => h(App)
      }).$mount('#app')
    }
    
    export async function mount(props) {
      render(props);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    5、shared 方案

    就是父应用通过 vuex 或者 redux 正常使用维护一个 state,然后创建一个 shared 实例,这个实例提供对 state 的增删改查,然后通过 props 把这个 shared 实例传给子应用,子应用使用就行。其实和上面挺相似的。
    不过可以看出上面4中方案,比较适合各个应用通信比较少的情况,实时上这也是微应用的使用原则-尽量减少他们之间的通信。
    但如果我们通过 vuex 或者 redux 正常使用维护一个 state,那可扩展的就多了,也比较适合较多通信的情况。
    主应用:
    这个 shared 实例大概是这样:

    import store from "./store";
    
    class Shared {
      /**
       * 获取 Username
       */
      public getUsername(): string {
        const state = store.getState();
        return state.username || "";
      }
    
      /**
       * 设置 Username
       */
      public setUsername(token: string): void {
        // 将 token 的值记录在 store 中
        store.dispatch({
          type: "set_username",
          payload: username
        });
      }
    }
    const shared = new Shared();
    export default shared;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    同样的传入方式

    import {
      registerMicroApps,
      start,
    } from "qiankun";
    import shared from './shared'
    const apps = [
      {
        name: "App1",
        entry: '//localhost:9001',
        container: "#app1",
        activeRule: "/app1",
        props: {
          parentShared: shared
        }
      }
    ];
    
    registerMicroApps(apps);
    start();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
  • 相关阅读:
    在线社交网络分析 github,在线社交网络分析软件
    告警收敛杂谈
    7-32 说反话-加强版
    Ubuntu下QT操作Mysql数据库
    【Freeradius】使用Freeradius、LDAP和Google Authenticator实现双因素身份验证
    2023 年度国家科学技术奖励公布
    实时频谱 TFN 手持式频谱分析仪 RMT716A 9KHz-6.3GHz 高性能全功能
    VBA实战(11) - 工作表(Sheet) 操作汇总
    测试十大法则
    第六章 字符串对象的基本概念
  • 原文地址:https://blog.csdn.net/weixin_44384273/article/details/133878783