微前端在vue2、vue3中的实战开发

2,442 阅读8分钟

为了业务在原有系统中,集成新项目,我们经常会使用qiankun这套微前端解决方案,来解决集成诉求。

vue2,vue3在qiankun中的使用实际差别不大,主要路由处理上有些区别,也会在文中指出,会以【vue3】作为标识。

通过这边文章,可以了解到,如何在vue(2/3)技术栈里使用微前端集成新老项目,主要内容包括:

  1. 不同的路由模式(history, hash),主、子应用分别在开发、生产环境的代码改造、部署;
  2. 在进入子应用前,如何处理鉴权逻辑;
  3. 主、子应用样式隔离的处理;
  4. 主、子应用间的通信;
  5. 在应用某个路由下加载微应用;
  6. QA,开发过程中遇到的问题汇总。

常用API

下面是常用的两个qiankun API,已给出链接,可以在官网查看具体用法:

registerMicroApps: 主应用注册微前端应用

start: 启动微前端应用

1、 不同的路由模式,主、子应用分别在开发、生产环境的代码改造、部署;

a. 主、子应用路由都为hash模式的改造

开发环境,主应用改造:

import { registerMicroApps, start } from 'qiankun';
registerMicroApps(
  [
    {
      // 微前端应用名
      name: 'app1',
      
      // 微前端启动地址
      entry: '//localhost:8100/',
      
      // 微前端挂载dom
      container: '#app',
      
      // 微前端触发路由
      activeRule: '#/app1',
      
      // 主应用向子应用传递的静态值
      props: {
        name: 'yuxiaoyu',
      },
    },
  ],
);

start();

开发环境,子应用改造:

// 入口文件main.js
// 子应用并不用引入qiankun,只要暴露响应的声明周期钩子给主应用使用就ok
// 挂载实例
function render(props: any = {}) {
  const { container } = props;
  app = createApp(App);
  app.use(router);
  app.use(store);
  router.isReady().then(() => {
    app.mount(container ? container.querySelector('#container') : '#container');
  });
}

// 微应用在主应用运行时,主应用会在微应用中挂载window.__POWERED_BY_QIANKUN__,可以用于判断环境
// 官方提供了下面这个webpack注入publicPath的方法, 开发环境我们这么使用,生产改到vue.config.js中,后面再介绍。
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

// 如果是独立运行 window.__POWERED_BY_QIANKUN__=undefined 直接render
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

// 最后暴露的三个方法是固定的,加载渲染以及销毁
export async function bootstrap() { }

export async function mount(props: any) {
  render(props);
}
export async function unmount() {
  app.unmount();
  app._container.innerHTML = "";
  app = null;
  
  // 这里reload的原因: 因为在项目中,微应用和主应用没有共用导航等信息
  // 相当于两个独立的页面,所以就共用了<div id="app">
  // 在卸载微应用后,为了再把主应用渲染出来,就重新reload了一遍。
  location.reload();
}
// router改造
const router = createRouter({
  history: createWebHashHistory(),
  routes,
});

// 因为主应用在激活子应用时,有一个activeRule前缀,所以在hash模式下,我们需要给每个一级路由都添加activeRule的前缀,主应用为'#/app1',那么子应用前我们就加'/app1'就可以了。
export default [
  {
    path: '/app1/fujidaohang',
    redirect: '/app1/fujidaohang/zijidaohang',
    component: BothLayout,
    name: 'fujidaohang',
    meta: {
      title: '父级导航',
      navPosition: 'top',
    },
    children: [
      {
        path: 'zijidaohang',
        component: () => import('@/apps/fujidaohang/views/zijidaohang.vue'),
        name: 'zijidaohang',
        meta: {
          title: '子级导航',
          navPosition: '',
        },
      },
    ],
  },
  {
    path: '/app1/course',
    component: Course,
    name: 'course',
    children: [],
  }
];
// vue.config.js改造
const packageName = require('./package.json').name;

module.exports = {
  ...
  
  // 用于主应用识别子应用,固定写法
  configureWebpack: {
    output: {
      library: 'app1',
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${packageName}`,
    },
  }
}

生产构建部署:

构建部署可以选择两种方式:

同域名、不同域名部署。

同域名部署:

1.可以考虑将子应用打包放在static路径下,作为主应用的静态资源引入。这种处理方式的适用场景包含:子应用不需要独立访问、子应用不需要独立部署。这种方式的好处在于,不用运维配合去Nginx配置,本地打包,并使用原有的部署方式。当然,它同时也失去了微前端,主、子独立部署的优点。

作为静态资源引入.png

2.还有一种方式,可以将打包后的静态资源在同一台服务器进行部署,然后针对于项目新增部署和Nginx相关配置。在主应用访问的时候可以通过域名加载,也可以通过相对路径进行加载(因为我们同台机器部署)。这个相对于上面的方案,增加了运维成本,但保留了主子独立发布的特性。

不用域名部署

这里的方案和上面的2差不多,区别在于资源可能放在不同的服务器上,所以只能通过域名进行资源的加载(也就是registerMicroApps中配置的entry需要是域名,不能是相对路径了,和开发环境差不多),记得处理跨域。

当然也可以根据需要选用相应的部署方式,官方都有给出详细的介绍: 如何部署

生产构建,主应用改造:

// main.js
// 注册微应用
registerMicroApps(
  [
    {
      name: 'app1',
      
      // 路径改为部署后,微应用要存放在主应用的目录,其余不变
      entry: '/static/index.html',
      container: '#app',
      activeRule: '#/app1',
      props: {
        name: 'kuitos',
      },
    },
  ],
);

生产构建,微应用改造:

// main.js
// 去掉qiankun的__webpack_public_path__注入
// if (window.__POWERED_BY_QIANKUN__) {
//   __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
// }

// vue.config.js
module.exports = {
  ...
  
  // 这里新增打包资源存放路径,与上面主应用相对应
  publicPath: '/static'
}

b. 主、子应用路由都为history模式的改造

开发模式:

这里我们和hash-dev模式进行对比,只列举差异的部分。

// 主应用
// main.js, 注册微应用有变化
registerMicroApps(
  [
    {
      name: 'app1',
      entry: '//localhost:8101',
      container: '#app',
      
      // 改变点:激活路由由hash模式变为history模式
      activeRule: '/app1',
      props: {
        name: 'yuxiaoyu',
      },
    },
  ],
);

// router.js 

const router = new VueRouter({

  // 由hash改为history模式
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});
// 子应用
// router/index.js
// 【vue3】因为子应用为vue3版本,这里是vue2和vue3本身路由处理上的区别,我们只是使用了vue3的写法加上了需要的前缀app1
const router = createRouter({
  
  // 改为history模式,并且加activeRule前缀
  history: createWebHistory('/app1'),
  routes,
});

// router.js

// 去掉这里的app1前缀
export default [
  {
    path: '/fujidaohang',
    redirect: '/fujidaohang/zijidaohang',
    component: BothLayout,
    name: 'fujidaohang',
    meta: {
      title: '父级导航',
      navPosition: 'top',
    },
    children: [
      {
        path: 'zijidaohang',
        component: () => import('@/apps/fujidaohang/views/zijidaohang.vue'),
        name: 'zijidaohang',
        meta: {
          title: '子级导航',
          navPosition: '',
        },
      },
    ],
  },
  {
    path: '/course',
    component: Course,
    name: 'course',
    children: [],
  }
];

生产构建部署:

这里与hash模式改动点是一致的,只描述不同点。

主应用改造:

// main.js
// 注册微应用
registerMicroApps(
  [
    {
      // 路径改为部署后,微应用要存放在主应用的目录,其余不变
      entry: '/static/index.html',
    },
  ],
);

微应用改造:

// main.js
// 去掉qiankun的__webpack_public_path__注入
// if (window.__POWERED_BY_QIANKUN__) {
//   __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
// }

// vue.config.js
module.exports = {
  ...
  
  // 这里新增打包资源存放路径,与上面主应用相对应
  publicPath: '/static'
}

2、在进入子应用前,如何处理鉴权逻辑

当进入微应用时,我们鉴定微应用是否登录时,我们可以考虑在微应用做鉴权,也可以在主应用做鉴权。

这里介绍一种在主应用内鉴权的方案。

qiankun里在注册微应用时,registerMicroApps提供了第二个参数,lifeCycles- 可选,全局的微应用生命周期钩子

registerMicroApps(
    [{...}], {
    
    // 这里采用在进入微前端之前进行鉴权,确保进入微前端时,已经登录。
    beforeLoad: [
      () => {
        if (!auth.check()) {
            vm.$router.push(loginPath);
        }
      },
    ],
  },);

3、主、子应用样式隔离的处理

a. 主、子应用都使用antd

这里官方提供了修改antd前缀的方法,如将ant改为dida-ant, 如何确保主应用跟微应用之间的样式隔离

这里ant-design-vue文档里虽然没有提供prefixCls的参数,但是可以使用的,源码里相应的处理。

b. 主应用自定义样式与子应用冲突

这里我们可以采用官方提供的start(options?),将微应用放入浏览器所支持的shadow dom中。

start({
  sandbox: {
    // 主应用 & 子应用样式隔离
    strictStyleIsolation: true, // 放入shadow dom中
  }
});

image.png

这里我们可以看到微前端被放入了shadow-root里,对于shadow dom可以通过这里进行了解。

这样隔离后,在我们使用ant design这种外部库时会有一些问题,例如popup组件,原本实现是挂在document.body中的,我们将子应用放到了shadow dom中,那就需要将popup也挂进去。ant design官方提供了方法,搜索getPopupContainer;

4、 主、子应用间的通信

a. 静态值传递

我们可以通过注册微应用时,通过props参数进行数据的传递,在mountedrender生命周期中,可以拿到props数据。

registerMicroApps(
  [
    {
      name: 'app1',
      entry: '//localhost:8100',
      container: '#app',
      activeRule: '/app1',
      props: {
        id: 1,
      },
    },
  ],
);

b. 通信机制

qiankun官方提供了创建主、子应用通信的定义方法。initGlobalState(state)文档链接,初始化后会返回三个方法,onGlobalStateChangesetGlobalStateoffGlobalStateChange,分别是监听,设置和移除。

// 主应用
// 初始化 state
// 调用initGlobalState后,会挂在props中进行传递
const initialState = {
  userInfo: {}, // 用户信息
};
const actions = initGlobalState(initialState);
actions.onGlobalStateChange((newState, oldState) => {

  // newState: 变更后的状态; oldState 变更前的状态
  console.log('mainapp: global state changed', newState, oldState);
});
actions.setGlobalState({
  userInfo: {
    name: 'Zhangsan',
  },
});
actions.offGlobalStateChange();
export async function mount(props: any) {
  render(props);
  
  // props 会注入onGlobalStateChange、setGlobalState方法。
  console.log('props :>> ', props);

  app.config.globalProperties.$onGlobalStateChange =props.onGlobalStateChange;
  app.config.globalProperties.$setGlobalState = props.setGlobalState;
  props.onGlobalStateChange((newState: any, oldState: any) => {

    // newState: 变更后的状态; oldState 变更前的状态
    console.log('microapp: global state changed', newState, oldState);
  });
  
  window.document.addEventListener('click', () => {
    props.setGlobalState({
      userInfo: {
        name: Math.random(),
      },
    });
  });
}

5、在应用某个路由下加载微应用

背景:

vue3作为主应用, 原系统作为子应用(vue2)。也就是vue3(主)接vue2(子)。在已有路由或组件中引入子应用。 如图:

172*36.png

需要将子应用嵌套在layout组件内

官方提供了一种在应用某个路由下面接入方法

官方提供的方法是在vue2为主应用时接入。所以这里写法上有会一些区别。

{
  path: '/huodongguanli',
  redirect: '/trace_micro/project/projectList',
  component: SideLayout,
  name: 'huodongguanli',
  meta: {
    title: '活动管理',
    navPosition: 'side',
    icon: () => {
      return h(ContactsOutlined);
    },
  },
  children: [
    {
      path: '/trace_micro/project/projectList', // 需要嵌套在二级菜单,且路径不需要一级的path拼接的,这里写绝对路径(这里为业务逻辑,不用关注)
      component: { default: '' }, // 因为这里只是为了声明一个menu项,并不需要组件,(组件内容在子应用中),这里给个含有default的空对象
      name: 'lianluliebiao',
      meta: {
        title: '链路列表',
        navPosition: 'side',
      },
    },
    {
      path: 'chuangjianjiangzuo', // 主应用中的路由 正常写就可以
      name: 'chuangjianjiangzuo',
      meta: {
        title: '创建讲座',
        navPosition: 'side',
      },
    },
  ],
},

// 针对于不用在主应用里菜单显示出来的路由,都进行模糊匹配
// 【vue3】
// 因为vue3中,vue-router4.x[取消了通配符的写法](https://next.router.vuejs.org/zh/guide/migration/index.html#%E5%88%A0%E9%99%A4%E4%BA%86-%EF%BC%88%E6%98%9F%E6%A0%87%E6%88%96%E9%80%9A%E9%85%8D%E7%AC%A6%EF%BC%89%E8%B7%AF%E7%94%B1),所以在匹配的`path`的写法上会有一些区别:
{
  path: '/trace_micro/:pathMatch(.*)*', // 【重要】这里我们采用router4.x中提供的写法去匹配子应用的路由
  hidden: true,
  component: SideLayout,
  name: 'projectOperate',
  meta: {
    title: '创建链路',
    navPosition: 'side',
  },
}

其余的使用方法(start挂载节点等)与官方提供的一致。

6、常见问题

官方有提供常见问题文档,遇到问题时,可以先去这里查看一下是否有相应的匹配解决方案。

这里也列举几个,我在接入项目时候遇到的其它的坑:

1、与主应用共用同一节点,如#app。在返回主应用时,主应用不显示。

这里因为我们在初始化微前端时,把#app中的内容替换成了子应用的数据,在返回时,并没有重新挂载,目前可以通过reload简单处理。

2、与主应用共用同一节点,如#app。dom看起来挂载成功了,但是样式都没有。

这个和第一个又是不同的情况,主要是因为我们把子应用的dom结构直接挂载到了主应用上,微前端这一套都没有生效。在mount生命周期中,我们可以拿到props,这里面挂载了一个container属性,就是挂载微前端的dom节点,可以通过props.container.querySelector('#app')去挂载子应用的vue

3、子应用跳转主应用,$router.push不生效

是因为我们主、子应用使用的router并不是一个实例,注册的路由也不一样,所以子应用并不能匹配到主应用的路由,可以使用history.push或者location.href进行跳转。

4、在打开主应用打开子应用时,会报js加载不到,或者<等语法不对的问题

一般由于主应用引入子应用的路径不对,检查路径是否能引到子应用资源。

5、主应用引入babel-polyfill时,有时候会babel-polyfill引入两次的问题。

这个先检查一下是否子应用也单独引入babel-polyfill了,如果引入了就都统一放到一处去引用。

现在大部分新的应用都是用vue-cli进行生成,并不会单独引入babel-polyfill,那如果报这个错了,就检查一下是不是,子应用的路径有问题,引入的资源不对。报了引两次的问题,可能也是由于子应用没有引到的问题。和babel-polyfill本身引入没有关系。

6、关于样式隔离

上面也有给出样式隔离的方案,但方案本身还是有一些问题的。比如我们提到的popup挂在document.body上的问题,需要处理。可能还会有一些其它的问题要处理。

最简单有效的方法还是在主、子应用中,尽量使用style scoped进行组件级别的样式隔离。对于使用同一组件库,不同配置,可以通过修改组件、样式前缀解决。

以上就是微前端(qiankun)在vue2、3中的实战开发,希望对各位有所帮助。