微前端这个内容在之前就做过分享,但是对于完整的项目实战没有写过,在公司刚好有后台,解决一下之前遗留的登录态和权限的问题。
主应用
1、安装依赖
bash
pnpm i qiankun
2、main.ts
采用 registerMicroApps 来注册子应用
ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import 'src/styles/index.scss'
import router from 'src/router/index'
import { registerMicroApps } from 'qiankun'
const app = createApp(App)
app.use(router)
app.mount('#app')
registerMicroApps([
{
name: 'h5App',
entry: '//dev.yvyiai.com:8090/yvyiai-digital-human-h5', // 确保路径不以斜杠结尾
container: '#sub-container',
activeRule: '/yvyiai-digital-human-web/h5App'
}
])
3、编写路由
这里需要添加子应用的路径匹配(不然子应用带路由的话,主应用会404)
ts
const routes = [
{
path: '/h5App',
component: () => import('src/views/Layout/index.vue'),
children: [
{
path: '/:pathMatch(.*)*',
name: 'h5App',
component: () => import('src/views/subApp/index.vue'),
meta: {
title: 'h5App'
}
}
]
}
]
4、subApp/index.vue
需要添加一个子应用的渲染容器节点,需要和 registerMicroApps 注册的子应用的 container 节点一致
html
<template>
<div id="sub-container"></div>
</template>
<script setup lang="ts">
import { start } from 'qiankun'
import { onMounted } from 'vue'
onMounted(() => {
// 启动 qiankun,添加错误处理
start({
sandbox: {
experimentalStyleIsolation: true
},
prefetch: false, // 禁用预加载避免冲突
// 添加全局错误处理
globalContext: window
})
})
</script>
子应用
1、安装依赖
bash
pnpm i vite-plugin-qiankun -D
2、配置vite.config.ts
-
如果子应用是webpack的话,可以看qiankun官网,vite需要使用插件。
-
子应用同时需要支持跨域
-
配置打包为umd
ts
import qiankun from 'vite-plugin-qiankun'
export default defineConfig({
// 参数1:子应用名
plugin: [qiankun('h5App', { useDevMode: true })],
server: {
// 开发环境的host
host: 'dev.yvyiai.com',
cors: true,
// 添加跨域头部
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods':
'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers':
'X-Requested-With, content-type, Authorization'
}
},
build: {
lib: {
entry: './src/main.ts', // 入口文件
name: 'h5App', // 子应用名称
fileName: 'h5App', // 打包后的文件名
formats: ['umd'] // 打包为 UMD 格式
}
}
})
3、改造main.ts
webpack的应用改造方法有点不同,比vite简单
ts
import { createApp, type App as AppInstance } from "vue";
import router from './router'
import App from './App.vue'
import {
renderWithQiankun,
qiankunWindow
} from 'vite-plugin-qiankun/dist/helper'
let app: AppInstance | null = null
function render(props: any = {}) {
const { container } = props
app = createApp(App)
app.use(router)
app.mount(container ? container.querySelector("#app") : "#app");
}
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
render()
}
renderWithQiankun({
mount(props) {
render(props)
},
bootstrap() {},
unmount(_props) {
if (app) {
app.unmount()
if (app._container) {
app._container.innerHTML = ''
}
app = null
}
},
update() {}
})
4、路由改造
这个项目的主子应用都配置了base route的,子应用在qiankun环境下,需要把base route换成主应用的base+route path
-
主应用base route:yvyiai-digital-human-web
-
主应用对应子应用的出口路由:/h5App
ts
// 子应用router/index.ts
const ROUTER_BASE = 'yvyiai-digital-human-h5'
const router = createRouter({
history: createWebHistory(!qiankunWindow.__POWERED_BY_QIANKUN__ ? ROUTER_BASE : 'yvyiai-digital-human-web/h5App'),
routes
})
对应如下:
-
子应用独立运行路径: dev.yvyiai.com:8090/yvyiai-digi...
-
在主应用下运行路径: dev.yvyiai.com:8080/yvyiai-digi...
子应用在主应用运行的路径就要以主应用的base为准了,不然会找不到资源的。
开发模式下接口问题
在开发模式下,我们的子应用通常会做proxy代理,但是这就导致我主应用是没有子应用的代理配置的,例如:
-
子应用单独运行代理请求url: dev.yvyiai.com:8090/liveApi/api...
-
子应用在主应用下运行的请求url: http://http://dev.yvyiai.com:8080/liveApi/api1
这里的 /liveApi 就是子应用在vite配置中做的代理,但是在主应用下看到是没有这个代理的,所以请求会报错。
解决办法:
-
在后端对接口处理跨域,不采用代理的方式
-
在主应用的vite中也配置同样的代理
但是方法2这种方法会存在一个缺点就是:我的子应用很多,代理也很多,我当前的子应用下面就有快10个代理接口,如果子应用也多,就会导致主应用的配置不好维护。
上面的解决方法只是基础原理
最终方法:
可以采用 CMS 进行后端配置,在运行前去加载代理的应用数据(也就是我的子应用需要提前去CMS系统上注册)。
这种方法对主应用去注册子应用也同样有用,可以通过远程数据配置来动态注册应用,这样基座代码不用每次注册都进行修改。
新建 remote-proxy-loader.js ,用于去加载不同子应用的proxy,去主应用的 vite.config.ts 去使用即可。
js
const fs = require('fs');
const path = require('path');
const axios = require('axios');
// 本地缓存文件路径
const PROXY_CACHE_PATH = path.resolve(__dirname, './proxy-cache.json');
/**
* 从远程获取代理配置
*/
async function fetchRemoteProxyConfig() {
try {
const response = await axios.get('https://your-config-server.com/proxy-config');
return response.data; // 假设返回格式: { subApp1: { ... }, subApp2: { ... } }
} catch (error) {
console.error('获取远程代理配置失败,使用本地缓存', error);
// 尝试读取本地缓存
if (fs.existsSync(PROXY_CACHE_PATH)) {
return JSON.parse(fs.readFileSync(PROXY_CACHE_PATH, 'utf-8'));
}
return {}; // 无配置时返回空对象
}
}
/**
* 保存配置到本地缓存
*/
function saveProxyCache(config) {
fs.writeFileSync(PROXY_CACHE_PATH, JSON.stringify(config, null, 2));
}
/**
* 获取当前应用的代理配置
*/
async function getCurrentAppProxy(appName) {
const allConfig = await fetchRemoteProxyConfig();
// 保存到本地缓存
saveProxyCache(allConfig);
// 返回当前应用的配置
return allConfig[appName] || {};
}
module.exports = { getCurrentAppProxy };
登录态的问题
项目中少不了的就是接口或页面的权限问题,这对项目的安全性也是非常重要的。
1、接口权限
公司的项目采用的是cookie作为登录态校验的,核心思路是利用 cookie 的跨域特性和 qiankun 的通信机制来做控制。
1.1 原理
cookie 具有域(domain)属性,若主应用和子应用配置在同一主域名下(如主应用 app.example.com ,子应用 sub1.example.com ),可通过设置 domain=.example.com 实现 cookie 共享
1.2 实现方案
同域的情况下直接共享:
- 登录接口设置 cookie 时指定主域名
bash
// 登录接口响应头设置(后端)
Set-Cookie: token=xxx; domain=.example.com; path=/; HttpOnly; Secure
- 主应用登录后,子应用自动获取同域名下的 cookie
不同域的情况下的登录态同步:
当主应用与子应用不在同一域时:
- 主应用登录后,通过 qiankun 的全局通信机制通知子应用
ts
// 主应用登录成功后
import { initGlobalState } from 'qiankun';
const globalState = initGlobalState({
token: 'xxx', // 登录后获取的 token
isLogin: true
});
// 主应用监听子应用消息
globalState.onGlobalStateChange((state, prev) => {
console.log('主应用监听到状态变化', state, prev);
});
- 子应用监听主应用的登录状态
js
// 子应用中
export function mount(props) {
// 监听主应用传递的登录状态
props.onGlobalStateChange((state, prev) => {
if (state.isLogin) {
// 子应用存储 token 到本地或内存
localStorage.setItem('token', state.token);
}
}, true);
}
2、页面权限
页面权限可以在登录的时候,通过动态路由生成权限路由(还是靠CMS去拿)。但只限于主应用的路由,子应用的路由的话,只能通过主子应用通信+接口(接口返回403状态,可以直接返回login页面)解决权限
效果展示
由于没有做样式的特殊处理,导致子应用的样式污染到了主应用。
即使qiankun设置了样式隔离,vite还是会有影响,所以需要手动处理样式(webpack可以做到完美隔离)
