最近手上微前端的项目正在快速迭代,需求量大到头秃,其中有个问题发生的频率越发地高,值得总结下问题及解决方案
问题
以微前端框架qiankun、UI库element-plus为例,当我们在子组件中使用抽屉/对话框/多选框组件时,往往会有定制样式的需求(毕竟UI大佬是看不上UI库的默认样式的)
可是当我们去修改样式的,无论怎么使用穿透语句,修改全都无法生效,以致被逼到去除组件style的scoped,埋下全局样式污染的隐患
原因
因为当我们使用抽屉/对话框/多选框等组件时,UI库往往会默认将其插入到body下,这时候,这个组件将会脱离子应用的节点,而qiankun自身的沙箱隔离机制,会导致子应用对样式的修改,是无法穿透子应用自身的
以上图为例,假设当我们在【child1】中使用了弹窗组件,这时候弹窗实际上是插入在body下,也就是<div class="popper">
节点,可以看出,popper
已经不在<div id="child1">
内部了,因此按照qiankun的沙箱隔离机制,我们在child1
中编写的样式,是无法影响到popper
节点的
解决方案
- 不要插入body(UI库组件一般会提供这个功能),但是可能会出现被页面内其他元素遮盖的情况(即使修改z-index也是无效的,因为z-index只对兄弟元素之间生效,而实际页面中往往dom结构是嵌套了多层的,尤其用了UI库的组件),例如在el-table中使用了el-select,如果你设置el-select不插入body的话,可能就会出现el-select被table遮挡的情况
- 修改全局的appendChild方法,开启白名单,检查到特定组件时,将其插入子应用的body(或者说子应用的根元素),注意,这个要看UI库,例如element-plus是通过appendChild方法来实现将弹窗插入body,但其他UI库就需要看源码才能知道是什么方式
- 手动实现一个组件,用fixed或absolute,插入子应用的根元素,实时计算坐标来实现(其实就是复刻一个UI库组件)
修改全局的appendChild方法
2的方法省事(不至于手写组件工作量大),同时不用担心1导致的层级问题,但是缺点就是白名单的设计方式要合理,不然可能会有误伤,这里也给出一个参考(建议最好结合自己的项目情况进行编写,不要直接copy)
appendChild.ts
/** 仅实现一次,主要是发现现在的element-plus的弹窗,哪怕有多个,也只会插入一次 */
let isAppended = false;
/**
* 由于qiankun的shadow隔离,
* 导致弹窗等组件在插入body后,自己编写的样式就会失效
* 因此重写appendChild方法,使有需要的组件仅插入到微应用根节点
*/
export function redirectPopup(container: any, originFn: any) {
const whiteList: string[] = [
'appendChild-to-micro-root'
]
document.body.appendChild = (dom: any) => {
let isInWhiteList = false;
/**
* 这里要强调一下,不同的UI库生成弹窗的设计不同
* element-plus是在body插入el-popper-container-xxxx节点,然后将所有多选框的节
* 点添加到el-popper-container-xxxx节点下,这个过程中,
* 实际只有插入el-popper-container-xxxx是发生了appendChild的,
* 因此我在这里没有用白名单,而是直接用el-popper-container-作为判断
*/
if (dom.id.includes('el-popper-container-')) isInWhiteList = true;
if (isInWhiteList && !isAppended){
isAppended = true
return container.querySelector('#child').appendChild(dom)
} else {
return originFn(dom)
}
}
}
main.ts
import { redirectPopup } from './utils/appendChild';
const originFn = document.body.appendChild.bind(document.body)
function render(props: any = {}) {
app = createApp(App) as any;
const { container, routerBase, mainRoutes } = props;
redirectPopup(container, originFn)
}
注意,以上文件均在子应用里实现, 不需要对主应用进行修改