• [qiankun]微前端实现方案


    微前端的实现方案-qiankun

    2018年 Single-SPA诞生了, single-spa是一个用于前端微服务化的JavaScript前端解决方案 (本身没有处理样式隔离、js执行隔离) 实现了路由劫持和应用加载;

    2019年 qiankun基于Single-SPA, 提供了更加开箱即用的 API (single-spa + sandbox + import-html-entry),它 做到了技术栈无关,并且不推荐抽取公共方法,工具等,它推荐的是每个微服务是独立的,并且不相互影响的,并且接入简单。

    微前端的框架现在比较成熟的是 “Single-spa” 和 “QianKun”, 而"QianKun"是基于Single-spa实现的,所以使用"QianKun"可能会更加的简单些,所以采用了笔者采用该方案去实现

    实践环境

     "@vue/cli-service": "~5.0.0-beta.6",
     "ant-design-vue": "^2.2.8",
     "vue": "^3.2.20",
    
      nginx:1.19.0
      node:V12.18.2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    主应用与子应用都是vue3.x开发的,下面开始实现的具体过程

    实践代码

    因为是在现有的vue3.x项目的基础上去实现微服务,因此,整个实践过程并不是从零开始的,也即对现有的可运行项目的改造过程

    对于项目的整个构想如下图所示
    在这里插入图片描述
    主应用基本是作为一个加载器的功能,只有登录,登录后获取到当前用户的所有菜单,然后展示所有的菜单,用于导航,在主应用注册所有的微服务,并提供公共信息给微服务,上图中的公共设置主要是404等展现的公共路由等

    基本所有的导航都是微服务

    端口号是否必须不同?

    本地测试的时候,因为是本地启动的项目,都是localhost,只能通过端口号区分不同的微服务,所以端口必须不相同;
    但是线上子应用,是可以单独部署的,所以端口号可能相同,也可能不相同,此时就不影响,只要部署成功即可

    路由模式是否必须是history?
    虽然网上大部分都是推荐的使用history路由模式,但是也支持使用哈希模式–createWebHashHistory

    本次实践过程中所有的项目都是vue3.x开发的,以下是对项目的改造

    主应用改造

    1. 引入qiankun
    npm i qiankun
    
    • 1
    1. 设置qiankun的启动标志

    window[“qiankunStarted”]=false;

    该标志主要是用于确定是否已经启动qiankun服务,因为不能重复注册与启动,所以设置了该标志

    1. 注册微服务

    history路由的注册方式

    registerMicroApps([
      {
        name: 'child1', //子应用的名称,和子应用的ouput名称必须一致
        entry: '//localhost:8081',//微服务的入口
        container: '#sonApp',//主应用中用于展示微服务的container
        activeRule: '/child/child1',         // 子应用触发规则(路径)
      },
    ]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    hash路由的注册方式

    // 注册子应用
    registerMicroApps([
      {
        name: 'child1', 
        entry: '//localhost:8081',//微服务的入口
        container: '#sonApp',
        activeRule: '#/micro/xxx/cli', // hash路由的注册方式
      },
    ]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    通过实践发现注册子应用不是必须在main.ts文件中,可以是真正呈现子应用的页面中进行注册,只要确保加载微服务的时候已经注册了就可以了

    container的值是否有什么要求?是否#sonApp必须是直接在App.vue中,还是在App.vue的组件中也可以?

    container的id是用来加载子应用的dom的Id,因此只要保证加载微服务的时候该值存在即可,可以是App.vue文件中直接存在,也可以是用来展示微服务的其它vue单文件中的dom的id值

    1. 启动微服务
      start({
         prefetch:false//是否开启预加载
      })
    
    • 1
    • 2
    • 3

    如果注册了多个微服务,开启预加载会加载其它微服务,该值默认是开启状态,如果微服务存在有问题的,打开预加载其实增加了调试的难度,所以可以选择关闭

    完整代码

    
          // 注册子应用 && 启动微服务
          if(!window["qiankunStarted"]){
            registerMicroApps([
              {
                name: 'cli5-beta6-test', //和导出微服务的名称相同
                entry: 'http://localhost:8081',
                container: '#micro',
                activeRule: '#/micro/cli',         // 子应用触发规则(路径)
              },
              {
                name:'micro-permission-web', 
                entry: 'http://localhost:8083',
                container: '#micro',
                activeRule: '#/micro/permission',         // 子应用触发规则(路径)
              },
            ],{
              beforeLoad: [
                app => {
                   console.log(`${app.name}的beforeLoad阶段`)
                  return Promise.resolve();
                }
              ],
              beforeMount: [
                app => {
                   console.log(`${app.name}的beforeMount阶段`)
                  return Promise.resolve();
                }
              ],
              afterMount: [
                app => {
                   console.log(`${app.name}的afterMount阶段`)
                  return Promise.resolve();
                }
              ],
              beforeUnmount: [
                app => {
                   console.log(`${app.name}的beforeUnmount阶段`)
                  return Promise.resolve();
                }
              ],
              afterUnmount: [
                app => {
                  console.log(`${app.name}的afterUnmount阶段`)
                  return Promise.resolve();
                }
              ]
            });
            
            start({// 开启服务
              prefetch:false//是否开启预加载,开启预加载会加载其它微服务
            })
    
            addGlobalUncaughtErrorHandler((event) => {
              //捕获微服务加载失败的错误
              if(undefined!=event["error"].appOrParcelName){
                //子应用加载失败跳转404
                router.push("/404")
              }
            });
    
            //启动qiankun的标志
            window["qiankunStarted"]=true;
          }
    
    • 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

    子应用改造

    子应用需要做的改造包括:

    1. 微服务的导出

    vue.config.js
    添加

    const config = require('./package');
    
    • 1

    module.exports的devServer中添加

    headers: {
        'Access-Control-Allow-Origin': '*',
     },
    
    • 1
    • 2
    • 3

    否则本地测试的时候,主应用会报跨域的错误

    module.exports的根节点中添加配置

    configureWebpack:{
            output: {
              library: {
                name:`${config.name}`,//这里的导出值必须和主应用注册的时候的name值相同,否则会导致引入问题
                type:'umd' // 把子应用打包成 umd 库格式
              },
              // jsonpFunction: `webpackJsonp_${config.name}`,//该值发现随着webpack的升级没有了,而且该值不影响跨域等问题,可以忽略该值
            }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ${config.name}必须和主应用注册的微服务名称相同,否则加载失败

    1. 获取当前子应用被加载的环境

    public-path.ts

    在src目录下直接添加 public-path.ts文件

    if ((window as any).__POWERED_BY_QIANKUN__) {
        __webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
    
    
    • 1
    • 2
    • 3
    • 4

    该文件主要是用获取当前微服务所处的环境,是浏览器直接打开的,还是作为微服务被加载的
    如果当前环境是微服务,则(window as any).INJECTED_PUBLIC_PATH_BY_QIANKUN qiankun的注入值是true
    设定__webpack_public_path__的值为当前环境值

    1. 微服务的入口文件

    main.ts

    main.ts的修改包括最上面添加:

    import "./public-path";
    
    • 1

    用于获取当前环境值,然后在引入其它文件后,添加

      const isQiankun = (window as any).__POWERED_BY_QIANKUN__
    
    
    function render(props:any = {}) {
        const { container } = props;
        const app = createApp(App)
        app.use(store).use(router).mount(container ? container.querySelector("#child") : "#child")//注意该值不能和主应用的值相同,该值为子应用index.html中的id值
    }
    /**
     * 每次应用作为微服务被进入时都会调用 bootstrap 方法,并且只有第一次加载微服务会触发bootstrap
     */
    export async function bootstrap() {
        console.log('app bootstraped');
    }
    /**
     * 每次应用作为微服务被进入时都会调用 mount 方法,通常我们在这里触发应用的渲染方法
     */
    export async function mount(props: any) {
        console.log("child application mount",props)
        render(props)
    }
    
    /**
     * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例,
     * 这里需要根据具体项目进行卸载,否则二次进入的时候会有问题
     */
    export async function unmount(props: any) {
        console.log(props)
    }
    
    /**
     * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
     */
    export async function update(props: any) {
        console.log('update props', props);
    }
    
    //独立运行时
    isQiankun || render();
    
    
    • 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

    子应用之间的id是否可以重复?

    子应用id之间是可以重复的,因为子应用之间嵌入在主应用内部的,如果不存在同时在一个页面加载多个微服务,则没有影响,但是主应用和子应用最好不要相同,因为可能会存在于同一个页面,主应用的挂载Id可以变更

    子应用配置完成后是否可以直接运行查看?

    可以的,通过标志位判断当前微服务的运行环境,是运行在qiankun的环境下,还是独立运行,若是独立运行则直接渲染

    isQiankun || render();
    
    • 1

    判断是否是在启动qiankun的环境下,如果不是则执行render(),启动项目,方便调试

    注意需要设置unmount卸载内容,否则导致重复加载,已挂载,资源已注册等等问题

    vue3.x 中的卸载方式

    export async function unmount(props: any) {
        console.log("micro application unmount", props)
        app.unmount();
        app = null;
        router = null;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    1. 微服务的路由前缀

    用于区分其它微服务,尤其是本地测试的时候,也可以根据主应用传递的值设置微服务的路由前缀

    const router = createRouter({
      history: createWebHashHistory((window as any).__POWERED_BY_QIANKUN__ ? "/#/micro/xxx" : "#"),
      routes
    });
    
    • 1
    • 2
    • 3
    • 4

    动态设置微服务的前缀,微服务的卸载,比较完整的改造,子应用的完整代码:

    const isQiankun = (window as any).__POWERED_BY_QIANKUN__
    
    let app = null;
    let router = null;
    
    /**
     * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
     */
    function render(props: any = {}) {
        const { container } = props;
        console.log("application render",store)
        app=createApp(App, {
            setup() {
            	//全局设置国际化,使用可以直接用$t('page.button.add')
                const { t } = useI18n({ useScope: 'global' })
                return { t }
            }
        });
        const activeRule=store.state.activeRule;
        router = createRouter({
       		//动态设置前缀
            history: createWebHashHistory(isQiankun? activeRule : ""),
            routes
        });
        app.use(i18n).use(store).use(router).mount(container ? container.querySelector("#app") : "#app")
    }
    
    export async function bootstrap() {
        console.log('app bootstraped');
    }
    
    export async function mount(props: any) {
        // props为注册该微服务时主应用传递的值
        // console.log("micro application mount", props)
        props.onGlobalStateChange((state, prev) => {
          console.log("micro onGlobalStateChange mount");
          // state: 变更后的状态; prev 变更前的状态
        //   console.log(state, prev);
    
        });
        //vuex改变微服务的路由前缀值
        store.commit("setProjectActiveRule", props.activeRule)
        // props.setGlobalState(state);
        render(props)
    }
    
    /**
     * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
     */
    export async function unmount(props: any) {
        console.log("micro application unmount", props)
        app.unmount();
        app = null;
        router = null;
    }
    
    /**
     * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
     */
    export async function update(props: any) {
        // console.log('micro application update props', props);
    }
    
    //独立运行时
    isQiankun || render();
    
    • 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

    通信问题

    主应用传递信息

    目前比较简单的通讯方式,并且是主应用传递信息给微服务

    registerMicroApps([{
                    name:projectName, 
                    entry:projectDns,
                    container: '#micro',
                    activeRule:projectActiveRule,         // 子应用触发规则(路径)
                    props:{
                      user:{ },
                      activeRule:projectActiveRule//用于动态设置子应用的路由前缀
                    }
                }
              }])
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    以上就是简单的微服务实现过程,因为在实现的过程中遇到的问题过多,内容过长因此进行了拆分,如果此时以满足你的需求你就可以忽视以下内容了

    微服务接受信息方式参考上面 4. 微服务的路由前缀

  • 相关阅读:
    CF385D Bear and Floodlight
    python format详解
    SpringBoot 条件注解之:自定义条件注解
    【JavaEE 学习笔记】JavaScript(WebAPI)附代码案例,猜数字网页版(完整版源码)
    mysql以逗号分隔的字段作为查询条件怎么查——find_in_set()函数
    HazelEngine 学习记录 - 2D Renderer
    C语言小项目 -- 通讯录(静态版+动态版+文件版)
    七、Docker网络模式详解
    LLM-TAP随笔——语言模型训练数据【深度学习】【PyTorch】【LLM】
    Flutter 上传文件和保存在应用程序存储目录
  • 原文地址:https://blog.csdn.net/tjj3027/article/details/121661321