一些坑
vue 3 作为子应用,路由跳转使不同子应用来回切换,vue3子应用多次挂载问题
https://github.com/umijs/qiankun/issues/2254
javascript
// 路由跳转使不同子应用来回切换,vue3子应用多次挂载问题
// 解决方法
import { isEmpty,assign } from 'radash'
router.beforeEach((to, from, next) => {
if (_.isEmpty(history.state.current)) {
_.assign(history.state, { current: from.fullPath });
}
next();
});
子应用部分元素挂载在 body 上
js
//一些ui库会将元素添加到body中,会导致这部分元素样式丢失
//解决方案:
// 将子应用appendBody的元素,挂载到子应用根元素身上
const proxy = (container) => {
//**检查是否已代理
if (document.body.appendChild.__isProxy__) return;
//创建可撤销的proxy对象,返回{proxy,revoke},proxy是代理对象,revoke是撤销方法
const revocable = Proxy.revocable(document.body.appendChild, {
apply (target, thisArg, [node]) {
if (container) {
container.appendChild(node);
} else {
target.call(thisArg, node);
}
}
});
if (revocable.proxy) {
document.body.appendChild = revocable.proxy;
}
document.body.appendChild.__isProxy__ = true;
};
子应用开启 experimentalStyleIsolation 后, 局部样式未添加前缀, 会导致权重问题
js
//experimentalStyleIsolation用于样式隔离,会给全局style加上div[data-qiankun=`${appName}`] 前缀
//但是<style scoped></script>中的样式不会添加
// 解决方案: 写一个vite插件,在每个scoped的样式上加上相同的前缀即可
// ./plugins/addScopedCssPrefixPostBuildPlugin.js
// 修改html中前缀的函数
function changeHtmlClassPrefix (htmlString, oldPrefix, newPrefix) {
const regex = new RegExp(
`(class|style)\\s*:\\s*((["']((${oldPrefix}\\b)-).*["'])|((_normalizeClass|_normalizeStyle)\\(.*(${oldPrefix}\\b)-.*\\)))`,
'g'
)
return htmlString.replace(regex, (match, p1, offset, string) => {
return match.replace(oldPrefix, newPrefix)
})
}
// 修改css中前缀的函数
function changeSelectorPrefix (cssString, oldPrefix, newPrefix) {
const regex = new RegExp(
`(\\.${oldPrefix}\\b|\#${oldPrefix}\\b|\--${oldPrefix}\\b)`,
'g'
)
return cssString.replace(regex, (match, p1, offset, string) => {
return match.replace(oldPrefix, newPrefix)
})
}
export default function addScopedCssPrefixPostBuildPlugin({ prefixScoped, oldPrefix, newPrefix }) {
return {
name: 'add-prefixScoped-or-changePrefix-css',
transform(code, id) {
if (!oldPrefix || !newPrefix) return
if (id.includes('node_modules')) return
const cssLangs = ['css', 'scss', 'less', 'stylus', 'styl']
let newCode = code
if (id.endsWith('.vue')) {
newCode = changeHtmlClassPrefix(newCode, oldPrefix, newPrefix)
}
// else if (id.includes('.vue') && id.includes('scoped')) {
else if (cssLangs.some((lang) => id.endsWith(`.${lang}`))) {
if (oldPrefix && newPrefix) {
newCode = changeSelectorPrefix(newCode, oldPrefix, newPrefix)
}
// 给代码包裹一层指定前缀的选择器
if (prefixScoped) {
newCode = `${newCode}${prefixScoped}{${newCode}}`
}
return newCode
}
return newCode
}
}
}
// vite-config.js
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import qiankun from "vite-plugin-qiankun";
import addScopedCssPrefixPostBuildPlugin from './plugins/addScopedCssPrefixPostBuildPlugin.js';
// https://vitejs.dev/config/
export default defineConfig((mode) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }
return {
plugins: [
vue(),
addScopedCssPrefixPostBuildPlugin({
prefixScoped: "div[data-qiankun='residentdoctor']",
oldPrefix: 'el', //初始样式前缀
newPrefix: 'residentdoctor' // 传入你想要添加的前缀,这个前缀需要与子应用注册时的name相同
}),
...
],
...
}
})
使用 css 变量,部分样式丢失
js
// 当使用css缩写,并且使用了css var,在该样式之后存在它的子属性,则会导致样式丢失
// 仅在使用 styleNode.textContent=styleNode.sheet.cssRules[0].cssText 时存在
// 例如:
html{
--test:red;
}
.a{
background: var(--test, blue);
background-color: red;
}
//解析为:
.a{
background-image: ;
background-position-x: ;
background-position-y: ;
background-size: ;
background-repeat: ;
background-attachment: ;
background-origin: ;
background-clip: ;
background-color: red;
}
<head>
<style>
html{
--test: red
}
</style>
</head>
<body>
<div class="ddd1">
22222
</div>
<script>
const textNode = document.createTextNode(`
.ddd1 {
background: var(--test, blue);
background-color: red;
}
`);
const styleNode = document.createElement('style');
styleNode.appendChild(textNode);
document.body.append(styleNode);
const rule = styleNode.sheet.cssRules[0];
styleNode.textContent = rule.cssText;
</script>
乾坤源码部分: ![[Pasted image 20241114160854.png]]
解决方案:
js
//experimentalStyleIsolation用于样式隔离,会给全局style加上div[data-qiankun=`${appName}`] 前缀
// element-plus中,部分样式覆盖导致qiankun的style解析失败导致样式丢失
// 例如border:var(--el-border-color); border-left:0;
//例如:
.residentdoctor-button{
border-color: var(--residentdoctor-button-border-color);
border-left: 0;
}
//解析为:
div[data-qiankun="residentdoctor"] .residentdoctor-button {
border-top-style: ;
border-top-width: ;
border-right-style: ;
border-right-width: ;
border-bottom-style: ;
border-bottom-width: ;
border-left-style: ;
border-left-width: ;
border-image-source: ;
border-image-slice: ;
border-image-width: ;
border-image-outset: ;
border-image-repeat: ;
border-left:0;
}
js
//fixQiankun.scss
//将这个文件引入放在element样式引入之后,
.el-button {
border: 1px solid var(--border-color-1);
&--text{
border-color: transparent;
}
}
.el-radio-button__inner {
border: 1px solid var(--border-color-1);
}
scoped 样式冲突
vue 的 scoped 样式其实也有问题,
它是通过. vue 文件基于项目根目录的相对路径 path+文件名进行计算 hash 值的,
当主子应用中同时存在一个 path 和文件名相同时的. vue 文件,
它的 data-v-XXXXX 算出来就是一样的,此时样式还是会冲突
乾坤配置
主应用
主应用注册并启动
通常主应用会做登录和菜单功能, 点击菜单的同时通过路由动态切换子应用
js
import { loadMicroApp, start, registerMicroApps } from 'qiankun'
//任何一个项目都可以作为主应用,在主应用中,用对应元素作为容器容纳子应用
//qiankun通过配置项中activeRule加载对应子路由的entry
//手动注册可激活任意子应用,未满足匹配规则的也会在激活状态 调用loadMicroApp,接收一个对象
//自动注册只能激活当前匹配子应用,未满足匹配规则且不是手动注册的会被销毁 调用registerMicroApps,接收对象组成的数组
registerMicroApps([
{
name: 'react-app',
entry: '//http://192.168.211.180', // 子应用的入口地址
container: '#appContainer', // 子应用挂在的容器元素
activeRule: '/react-app', // 子应用的激活规则
props: {}, // 传递给子应用的数据,子应用通过mount生命周期接收
},
...
])
registerMicroApps([app])
start({
singular: true, // 是否启用微应用的独立运行时
prefetch: true, // 开启预加载关掉
//sandbox: true, // 是否启用沙箱隔离
sandbox: {
// 开启严格样式隔离,其实就是给每个子项目的根元素attachShadow,并将子项目挂载在这个影子节点上
// 导致的问题:一些第三方库的样式会丢失,因为他们可能期望挂载的body上,还有些样式依赖body什么的
strictStyleIsolation: true,
// scoped隔离,会给每个子应用的样式添加div[data-qiankun=`${appName}`] 前缀
// 导致的问题: 样式权重会受到影响,需要提前规避
experimentalStyleIsolation: true,
},
})
子应用
声明子应用QIANKUN_APP
js
//.env
// 该名称必须与子应用的entry名称相同
VITE_QIANKUN_APP_NAME = 'residentdoctor'
子应用注入生命周期
vue2
js
//main.js
import Vue form 'vue';
import { getRouter } from './router';
import App from './App.vue';
import store from './store';
let app;
/**
* @param container 主应用下发的props中的container,也就是子应用的根节点
* 将子应用appendBody的元素,挂载到子应用根元素身上
* 用于解决乾坤子应用开启如下样式隔离方案后,添加到body的元素样式失效
* sandbox: {
* 以下配置项只能开启一个,另一个要为false
* strictStyleIsolation: true, //严格模式,其实就是给子应用根节点改造为shadowRoot,也就是套上shadowDom
* experimentalStyleIsolation: true, //样式隔离,就是给子应用的全局样式加上div[data-qiankun=`${appName}`],会导致scoped中的样式权重变低
* },
*/
const proxy = (container) => {
if (document.body.appendChild.__isProxy__) return;
const revocable = Proxy.revocable(document.body.appendChild, {
apply (target, thisArg, [node]) {
if (container) {
container.appendChild(node);
} else {
target.call(thisArg, node);
}
}
});
if (revocable.proxy) {
document.body.appendChild = revocable.proxy;
}
document.body.appendChild.__isProxy__ = true;
};
function render(props) {
const { container } = props;
proxy(container)
const router = getRouter(props);
app = new Vue({
router,
store,
render: h => h(App)
})
if(container){
const root = container.querySelector('#app');
app.mount(root);
}else{
app.mount('#app')
}
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}else{
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {}
/** 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法 */
export async function mount(props) {
render(props);
}
/** 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效 */
export async function update(props) {}
/** 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例*/
export async function unmount() {
app.$destroy()
}
}
vue3
js
// main.js
// 引入vue,样式等
...
import App from './App.vue'
import getRouter from './router'
let app;
/**
* @param container 主应用下发的props中的container,也就是子应用的根节点
* 将子应用appendBody的元素,挂载到子应用根元素身上
* 用于解决乾坤子应用开启如下样式隔离方案后,添加到body的元素样式失效
* sandbox: {
* 以下配置项只能开启一个,另一个要为false
* strictStyleIsolation: true, //严格模式,其实就是给子应用根节点改造为shadowRoot,也就是套上shadowDom
* experimentalStyleIsolation: true, //样式隔离,就是给子应用的全局样式加上div[data-qiankun=`${appName}`],会导致scoped中的样式权重变低
* },
*/
const proxy = (container) => {
if (document.body.appendChild.__isProxy__) return;
const revocable = Proxy.revocable(document.body.appendChild, {
apply (target, thisArg, [node]) {
if (container) {
container.appendChild(node);
} else {
target.call(thisArg, node);
}
}
});
if (revocable.proxy) {
document.body.appendChild = revocable.proxy;
}
document.body.appendChild.__isProxy__ = true;
};
function render(props) {
const { container } = props;
proxy(container)
app = createApp(App);
// 注册指令
directives(app)
// 注册组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.provide('getUtils', utils)
app.use(pinia)
// 测试主题变更
// const themeStore = useThemeStore()
// themeStore.setTheme('red'); const router = getRouter(props);
const router = getRouter(props);
app.use(router)
app.use(ElementPlus)
if(container){
const root = container.querySelector('#app');
app.mount(root);
}else{
app.mount('#app')
}
}
// 独立运行时
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
render({});
}else{
renderWithQiankun({
/** 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法 */
mount (props) {
render(props);
},
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
bootstrap () {},
/** 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效 */
update(props){},
/** 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例*/
unmount (props) {
app.unmount();
app = null;
},
});
}
子应用设置服务基础路径
基础路径需要与 activeRule 相同, 用于主应用匹配子应用并激活, 和 nginx 代理
vue2
javascript
// vue.config.js或webpack.config.js中
// 基础路径需要与 activeRule 相同, 用于主应用匹配子应用并激活, 和 nginx 代理
module.exports = {
//publicPath将服务本来是 http://172.18.120.209:3333/的变为 http://172.18.120.209:3333/residentdoctor/
publicPath: `/${process.env.VITE_QIANKUN_APP_NAME}`,
...,
}
vite
javascript
// vite.cofing.js
// 基础路径需要与 activeRule 相同, 用于主应用匹配子应用并激活, 和 nginx 代理
import qiankun from "vite-plugin-qiankun";
export default ({ mode }: ConfigEnv): UserConfig => {
const VITE_QIANKUN_APP_NAME=process.env.VITE_QIANKUN_APP_NAME
return {
//publicPath将服务本来是 http://172.18.120.209:3333/的变为 http://172.18.120.209:3333/residentdoctor/
base: `/${VITE_QIANKUN_APP_NAME}$`,
plugins:[
qiankun(VITE_QIANKUN_APP_NAME, {
useDevMode: true, // 开发环境必须配置
}),
]
}
}
子应用设置路由
vue2
js
import VueRouter from 'vue-router';
const routes=[...]
export const getRouter = function(props) {
let base = '';
if (window.__POWERED_BY_QIANKUN__) {
const { activeRule='/' } = props.data;
...
base = activeRule;
} else {
base = process.env.BASE_URL;
}
const router = new VueRouter({
base,
mode:'history',
routes
});
vue3
js
import { createRouter, createWebHistory } from 'vue-router'
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
import { isEmpty,assign } from 'radash'
const routes= [...]
function getRouter(props) {
let base;
const routes=_.cloneDeep(Routes);
if (qiankunWindow.__POWERED_BY_QIANKUN__) {
const { activeRule='/' } = props.data;
...
base = activeRule;
}
else {
base = `/${import.meta.env.VITE_QIANKUN_APP_NAME}`
}
const router = createRouter({
history: createWebHistory(base),
routes
})
router.beforeEach((to, from, next) => {
_.assign(history.state, { current: from.fullPath })
}
next()
})
return router
}
export default getRouter
应用通信
乾坤提供的 globalState 通信方式
js
//父应用通过在注册子应用时,添加props配置项,将actions传递给子应用,子应用则可脱离qiankun包
import {initGlobalState , MicroAppStateActions } from 'qiankun';
//初始化globalState
const actions:MicroAppStateActions = initGlobalState(state);
//监听globalState变化
actions.onGlobalStateChange((state,preState)=>{});
//更新globalState
actions.setGlobalState(newState);
//卸载globalState
actions.offGlobalStateChange();
事件流方式
js
//因为qiankun父子应用在同一个页面上,可以通过事件流进行通信
//实例创建完毕后message不可变,如需要变化,需要重新创建事件实例
new CustomEvent('eventdemo', { detail: message })
window.dispatchEvent('eventdemo');
mounted() {
window.addEventListener('eventdemo',res=>{}, false);
},
beforeDestroy() {
window.removeEventListener('eventdemo', res=>{},false);
}
如何在本地连上远程的主应用 (壳子)
javascript
远程主项目在http://192.168.18.228:9999/middle/
本地子项目在http://172.18.120.209:3333/TSIDS/ 本地服务器地址(本地IP+端口,在devsever配置)
// 子项目通过nginx代理转发请求,使请求能正常请求远程服务器,例如我现在代理83端口,
// location ^~/middle/ {proxy_pass http://192.168.18.228:9999/middle/; #开发时运行访问地址}
'/middle路由对应主项目,代理middle到主项目所在的远程服务器地址http://192.168.18.228:9999/middle/,'
此时,我们访问localhost:83/middle,nginx帮我们代理转发到http://192.168.18.228:9999/middle/,获取到主应用,浏览器会将主应用下载到当前域名,也就是localhost:83下的middle目录(因为主项目的vue.config.js配置了publicPath: '/middle',)
//registerMicroApps([
// {
// name: 'app1',
// entry: '/TSIDS/',
// container: '#container',
// activeRule: '/middle/TSIDS/',
// },
//])
'主应用设置微应用时注意entry不能和activeRule一样,否则刷新则变成微应用'
'微应用的 webpack 打包和devserver的 publicPath和部署时的目录(相对于主应用所在目录的相对路径) 都需要跟entry一致,这里是 /TSIDS/'
'publicPath将服务本来是 http://172.18.120.209:3333/的变为 http://172.18.120.209:3333/TSIDS/'
'子项目的activeRule会作为子项目的路由基础路径,activeRule为/middle/TSIDS/'
'由于主应用配置子应用时,entry仅指定了相对路径/TSIDS/'
'最终访问localhost:83/middle/TSIDS/会先访问http://192.168.18.228:9999/middle/获取主应用'
'主应用基于当前域名通过相对路径/TSIDS/访问子应用,即localhost:83/TSIDS/,'
'而/TSIDS/又被代理到http://172.18.120.209:3333/TSIDS/,'
'至此,完成了子应用本地连上远程的主应用'