微前端应用实践:qiankun项目搭建

198 阅读7分钟

应用背景:

之前在公司里有一个需求,我们有很多外包公司做的各种项目,包含react和vue项目,想搭建一个统一的母系统,完成统一的用户登录操作,然后统一做菜单管理,不同的一级菜单对应不同的子系统,然后点击菜单跳到对应的系统页面。

乍听起来就是现在的微前端应用,使用母应用统一做用户身份管理和菜单管理,并统一系统UI,给用户看起来就是在一个系统里操作。

当时还没有qiankun这样成熟的微前端方案,采用的是母系统内通过iframe嵌入子系统页面来实现的,母系统对接登录和菜单接口,然后点击不同的菜单,更换对应的iframe页面为对应的子系统页面,来加载刷新,然后系统之间通过共享cookie里的token来实现用户身份统一。

从体验上来看,其实还不错,对用户来说是无感知的,是感觉在同一个系统里操作。

但是iframe的方案还是有很多瓶颈,具体有哪些障碍,直接引用qiankun里的原文。

  1. url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
  2. UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
  3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
  4. 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

当时做的时候确实有不少坑,主要是url不同步和数据隔离,也用了很多方式解决,但是这毕竟不是本文重点,就不展开了。

下面是qiankun框架的体验之旅:

首先需要创建一个母应用,这里我们通过vite框架搭建一个vue3项目。

一、母应用配置

npm create vite@latest my-vue-project --template vue
npm install
cd my-vue-project
npm run dev

创建完之后,安装完依赖就可以启动项目了,如果启动报错,大概率是node版本太低,需要升级到node 18以上,用nvm可以随意切换node版本。

项目启动后,安装qiankun的依赖

npm install qiankun -S

首先配置main.js页面

import { createApp } from 'vue'import { createRouter, createWebHistory } from 'vue-router'import { registerMicroApps, start } from 'qiankun'import App from './App.vue'const app = createApp(App)// 路由配置const router = createRouter({  history: createWebHistory(),  routes: [    { path: '/', component: () => import('./views/MyHome.vue') },    { path: '/child', component: () => import('./views/ChildContainer.vue') },    { path: '/child2', component: () => import('./views/ChildContainer2.vue') },    // { path: '/child/page2', component: () => import('./views/ChildContainer.vue') }  ]})// 注册子应用registerMicroApps([  {    name: 'sub-app',    entry: '//localhost:5173',    container: '#subapp-container',    activeRule: ['/sub-app'],    props: {       baseRoute: '/child' // 传递路由基准路径    }  },  {    name: 'sub-app2',    entry: '//localhost:5174',    container: '#subapp-container',    activeRule: ['/sub-app2'],    props: {       baseRoute: '/child2' // 传递路由基准路径    }  }])// 启动qiankunstart({  sandbox: { strictStyleIsolation: true }  // prefetch: 'all' // 预加载所有子应用})app.use(router).mount('#main-app')

qiankun的核心在于registerMicroApps注册子应用和start开启qiankun

registerMicroApps是一个数组,里面对应着不同的子应用,属性含义如下:

name:对应着子应用的名称

entry:子应用的入口页面,即子应用开发服务器地址

container:需要在母应用的页面里有这对于id的容器,用于挂载子应用页面

activeRule:匹配规则,这里与name同步就可以。

props/baseRoute: 这里对应着路由url前缀,即http://localhost:3000/child#/就会匹配对子应用sub-app的页面。

start里面的sandbox代表着沙盒隔离,如果设置为true,就代表母应用和子应用相互的css样式不会互相影响,否则可能样式会相互冲突难以维护,qiankun的沙箱可能会干扰Dom查询,所以尽量采用严格样式隔离strictStyleIsolation: true

配置路由:
routes: [    { path: '/', component: () => import('./views/MyHome.vue') },    { path: '/child', component: () => import('./views/ChildContainer.vue') },    { path: '/child2', component: () => import('./views/ChildContainer2.vue') },    // { path: '/child/page2', component: () => import('./views/ChildContainer.vue') }  ]

上述配置路由,配置了三条页面路由,分别对应着母应用页面,子应用1的容器和子应用2的容器,http://localhost:3000/child#/,则会访问ChildContainer.vue文件,内容如下:

// ChildContainer.vue
<template>  <!-- 子应用挂载容器 -->   <div>sdasdasd</div>  <div id="subapp-container"></div></template><script setup>import { onUnmounted } from 'vue'import { loadMicroApp } from 'qiankun'const microApp = loadMicroApp({  name: 'sub-app',  entry: '//localhost:5173',  container: '#subapp-container'})onUnmounted(() => {  microApp.unmount()})</script>

ChildContainer.vue主要做了几件事情:

1、添加一个id为subapp-container的容器,用于子应用的页面挂载,id名与注册子应用时container的值对应

2、通过loadMicroApp方法手动加载子容器,内容与main.js中注册子应用的值对应。

3、添加unmount方法,保证子页面消除时,一并将子应用页面消除。

由此,母应用的设置就做完了(是的,母应用的配置比子应用还要简单)

二、子应用配置

npm create vite@latest my-vue-project --template vue
npm install
cd my-vue-project
npm install qiankun -S
npm run dev

同理,创建一个vue3项目,启动。

配置main.js文件

//main.js
import { createApp } from 'vue'import './style.css'import routers from '@/router';import App from './App.vue'import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'let app = nullfunction render(props = {}) {  app = createApp(App)  app.use(routers)  app.mount(props.container || '#app1')}if (qiankunWindow.__POWERED_BY_QIANKUN__) {  renderWithQiankun({    bootstrap() {},    mount(props) {         console.log('props12', props)        render(props)     },    unmount() { app.unmount() }  })} else {  render()}// 独立运行if (!window.__POWERED_BY_QIANKUN__) {  render()}

这里核心就是子应用需要暴露出3个生命周期,分别是bootstrap(),mount(),unmount()

然后判断一下当前是否是qiankun环境,即是否在母应用的窗口里,如果独立运行,正常运行render方法,创建app挂载到app1的容器上,但如果是qiankun环境下,需要通过props获取母应用传来的容器,props.contaner,然后判断如果有container,则挂载母应用的container容器,如果没有则挂载子应用的app1容器。

这里有一个坑,就是vite项目无法直接通过window.__POWERED_BY_QIANKUN__判断当前是否是qiankun环境,会提示window is not defined,需要通过vite插件vite-plugin-qiankun/dist/helper来获取qiankunWindow里的值。

并且插件内有renderWithQiankun方法,可以直接暴露三个钩子函数,如果不是vite项目,可以直接按照官网文档,直接在main.js最下面声明三个钩子函数:

export async function bootstrap() {  
  console.log('[vue] vue app bootstraped'); 
} 
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;
  router = null;
}

对比一下,在qiankun环境内,只有两个区别,一是多声明了三个生命周期函数,二是render方法多传入了container,让页面挂载在该容器下。

配置vite.config.js

//vite.config.js
import { defineConfig } from 'vite'import vue from '@vitejs/plugin-vue'import path from 'path'import qiankun from 'vite-plugin-qiankun'function _resolve(dir) {  return path.resolve(__dirname, dir);}// https://vite.dev/config/export default defineConfig({  plugins: [    vue(),    qiankun('sub-app', { // 子应用名称,需与主应用注册的name一致      useDevMode: true    })  ],  base: '/',  server: {    port: 5173,    origin: 'http://localhost:5173', // 确保资源路径正确    headers: {      'Access-Control-Allow-Origin': '*'    }  },  resolve: {      alias: {        '@': _resolve('src'),        '@assets': _resolve('src/assets'),        '@components': _resolve('src/components'),        '@util': _resolve('src/components/util'),        '@router': _resolve('src/router'),        '@store': _resolve('src/store'),        '@mock': _resolve('/mock'),      }  },  build: {    lib: {      entry: 'src/main.js',      name: 'subApp',      formats: ['umd']    }  }})

核心点:

子应用需要设置CORS来支持跨域,然后创建qiankun的应用名,用于母应用的注册子应用。

路由配置页:

//router.js
import { createRouter, createWebHashHistory } from "vue-router";const routes = [  {    path: '/',    name: 'Home',    meta: {      title: 'home'    },    component: () => import(/* webpackChunkName: "login" */ '@/view/Home/Home.vue')  },  {    path: '/page2',    name: 'Page2',    meta: {      title: 'Page2'    },    component: () => import(/* webpackChunkName: "login" */ '@/view/Home/Page2.vue')  },  {    path: '/page3',    name: 'Page3',    meta: {      title: 'Page2'    },    component: () => import(/* webpackChunkName: "login" */ '@/view/Home/Page3.vue')  },  // {  //   path: '/403',  //   name: '403',  //   meta: {  //     title: '没有权限'  //   },  //   component: () => import(/* webpackChunkName: "403" */ '../views/403.vue')  // }]const router = createRouter({  history: createWebHashHistory(),  routes,});export default router;

这里注意一点就是官网推荐路由用history模式,应该为了url能保持洁净。

App.vue

//App.vue
<template>    <router-view /></template><script setup></script>

其他项目可以按照同样的配置搭建,注意子应用名和服务器端口区分开就可以。

这时,运行所有项目,这时在子应用http://localhost:5174/#/project2/project2Page打开的页面,在母应用http://localhost:3000/child2/#/project2/project2Page也可以访问了,至此整个项目流程算是搭建成功了。

后续在主应用里编写菜单和头部组件,剩下的显示页面的区域就是子应用的container,这时候切换菜单的时候正常跳转对应页面url就可以了。