应用背景:
之前在公司里有一个需求,我们有很多外包公司做的各种项目,包含react和vue项目,想搭建一个统一的母系统,完成统一的用户登录操作,然后统一做菜单管理,不同的一级菜单对应不同的子系统,然后点击菜单跳到对应的系统页面。
乍听起来就是现在的微前端应用,使用母应用统一做用户身份管理和菜单管理,并统一系统UI,给用户看起来就是在一个系统里操作。
当时还没有qiankun这样成熟的微前端方案,采用的是母系统内通过iframe嵌入子系统页面来实现的,母系统对接登录和菜单接口,然后点击不同的菜单,更换对应的iframe页面为对应的子系统页面,来加载刷新,然后系统之间通过共享cookie里的token来实现用户身份统一。
从体验上来看,其实还不错,对用户来说是无感知的,是感觉在同一个系统里操作。
但是iframe的方案还是有很多瓶颈,具体有哪些障碍,直接引用qiankun里的原文。
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
当时做的时候确实有不少坑,主要是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就可以了。