⚡微前端下,子应用使用UI库组件,修改样式失败的原因及解决方案

728 阅读3分钟

最近手上微前端的项目正在快速迭代,需求量大到头秃,其中有个问题发生的频率越发地高,值得总结下问题及解决方案

问题

以微前端框架qiankun、UI库element-plus为例,当我们在子组件中使用抽屉/对话框/多选框组件时,往往会有定制样式的需求(毕竟UI大佬是看不上UI库的默认样式的)

可是当我们去修改样式的,无论怎么使用穿透语句,修改全都无法生效,以致被逼到去除组件style的scoped,埋下全局样式污染的隐患

原因

因为当我们使用抽屉/对话框/多选框等组件时,UI库往往会默认将其插入到body下,这时候,这个组件将会脱离子应用的节点,而qiankun自身的沙箱隔离机制,会导致子应用对样式的修改,是无法穿透子应用自身的

image.png

以上图为例,假设当我们在【child1】中使用了弹窗组件,这时候弹窗实际上是插入在body下,也就是<div class="popper">节点,可以看出,popper已经不在<div id="child1">内部了,因此按照qiankun的沙箱隔离机制,我们在child1中编写的样式,是无法影响到popper节点的

解决方案

  1. 不要插入body(UI库组件一般会提供这个功能),但是可能会出现被页面内其他元素遮盖的情况(即使修改z-index也是无效的,因为z-index只对兄弟元素之间生效,而实际页面中往往dom结构是嵌套了多层的,尤其用了UI库的组件),例如在el-table中使用了el-select,如果你设置el-select不插入body的话,可能就会出现el-select被table遮挡的情况
  2. 修改全局的appendChild方法,开启白名单,检查到特定组件时,将其插入子应用的body(或者说子应用的根元素),注意,这个要看UI库,例如element-plus是通过appendChild方法来实现将弹窗插入body,但其他UI库就需要看源码才能知道是什么方式
  3. 手动实现一个组件,用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)
}

注意,以上文件均在子应用里实现, 不需要对主应用进行修改