使用微前端(qiankun),解决样式污染问题

前言

使用微前端拆分巨石项目,主应用使用VUE2.0+Element,子应用同样使用VUE2.0+Element。由于主应用和子应用使用了相同的框架,多数元素的class类名相同,子应用与主应用样式隔离不彻底,会导致页面样式错乱。 完全使用qiankun自带的沙箱隔离会遇到一些坑。

方案一:使用插件转换子应用class的前缀名称,尽量不和主应用重名(不推荐)
change-prefix-loader

1、安装依赖change-prefix-loader,使用change-prefix-loader替换js中的class前缀

npm i change-prefix-loader -D

2、配置规则

<!-- vue.config.js -->

module.exports = {
    chainWebpack: config => {
        config.module
            .rule('change-prefix')
            .test(/\.js$/)
            .include.add(path.resolve(__dirname, './node_modules/element-ui/lib'))
            .end()
            .use('change-prefix')
            .loader('change-prefix-loader')
            .options({
                prefix: 'el-',
                replace: 'gp-'
            })
            .end()
    },
}

postcss-change-css-prefix

1、使用postcss-change-css-prefix替换css样式前缀

npm i postcss-change-css-prefix -D

2、配置规则,在根目录创建postcss.config.js

<!-- postcss.config.js -->
const addCssPrefix = require('postcss-change-css-prefix')

module.exports = {
  plugins: [
    addCssPrefix({
      prefix: 'el-',
      replace: 'gp-',
    }),
  ],
}

3、element-ui的组件前缀和样式前缀都被替换成gp了,在运行代码或者编译后查看子应用的元素,class类名由"el-"变成了"gp-"

4、虽然这样也能隔离样式,但是插件转换了class的命名,但是实际上转换过后的子应用改动比较大。样式也会有些错乱。这个需要根据自己实际项目来判别。

方案二:使用qiankun自带的沙箱隔离(不推荐)

使用qiankun自带的沙箱隔离,在主应用中start函数中,通过配置sandbox做到样式隔离。但这样直接使用沙箱隔离会遇到很多坑。

start({
   sandbox: {
     // 开启严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 [shadow dom]节点,从而确保微应用的样式不会对全局造成影响。
     strictStyleIsolation: true,
     // 设置实验性的样式隔离特性,即在子应用下面的样式都会包一个特殊的选择器规则来限定其影响范围
     experimentalStyleIsolation: true
   }
})
1、strictStyleIsolation严格样式隔离模式:

当启用strictStyleIsolation严格样式隔离模式时,qiankun将采用shadowDom的方式进行样式隔离,即为子应用的根节点创建一个shadow root。最终整个应用的所有DOM将形成一棵shadow tree。我们知道,shadowDom的特点是,它内部所有节点的样式对树外面的节点无效,因此自然就实现了样式隔离。

坑:当启用strictStyleIsolation时,因为某些UI框架(例如element、ant-design等)可能会生成一些弹出框直接挂载到主应用的document.body下,此时由于脱离了shadow tree。会导致页面报错。

experimentalStyleIsolation样式隔离:

experimentalStyleIsolation设置实验性的样式隔离特性,即在子应用下面的样式都会包一个特殊的选择器规则来限定其影响范围。这种方案的策略是为子应用的根节点添加一个特定的随机属性,这样页面也不会报错,但子应用弹出框的样式不生效,

<div
  data-qiankun-asiw732sde
  id="__qiankun_microapp_wrapper__"
  data-qiankun="approval"
>
// 然后为所有样式前面都加上这样的约束:
.app-main {
  font-size: 14 px ;
}
// ->
div[data-qiankun="approval"] .app-main {  
  font-size: 14 px ;
}

坑:动态类的弹窗样式丢失。

方案三:使用qiankun自带的experimentalStyleIsolation实验性的样式隔离的同时,在子应用的main.js中重写document.body.appendChild的方法

在主应用中start函数中,通过配置sandbox做到样式隔离,使用qiankun自带的experimentalStyleIsolation实验性的样式隔离

start({
      sandbox: {
        // 开启严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 [shadow dom]节点,从而确保微应用的样式不会对全局造成影响。
        strictStyleIsolation: false,
        // 设置实验性的样式隔离特性,即在子应用下面的样式都会包一个特殊的选择器规则来限定其影响范围
        experimentalStyleIsolation: true
      }
    })

子应用中使用的是element组件库,子应用中的部分弹出框、下拉框等是动态渲染的,因为样式隔离,子应用中element的样式对动态渲染的弹窗不生效,弹窗脱离了子应用,直接加载到主应用的body上了。弹出框每次渲染的时候相当于是使用了主应用的document.body.appendChild这个方法,于是在子应用的main.js中重写document.body.appendChild的方法。

let instance = null
// 初始的document.body.appendChild事件
const originFn = document.body.appendChild.bind(document.body)

function render(props = {}) {
  const { container } = props
  // 每次渲染的时候调用redirectPopup事件
  redirectPopup(props)
  instance = new Vue({
    router,
    store,
    render: (h) => h(App)
  }).$mount(container ? container.querySelector('#app') : '#app')
}

if (!window.__POWERED_BY_QIANKUN__) {
  render(document)
}

export async function mount(props) {
  const currentApp = getStorage('currentApp')
  if (currentApp) {
    store.commit('app/SET_CURRENTAPP', currentApp)
  }
  props.onGlobalStateChange((state) => {
    // 监听状态变更
    if (state.currentApp) {
      store.commit('app/SET_CURRENTAPP', state.currentApp)
    }
  })
  render(props)
}

function redirectPopup(container) {
  // 子应用中需要挂载到子应用的弹窗className。样式class白名单,用子应用的样式。
  const whiteList = ['el-select-dropdown', 'el-popper', 'el-popover', 'el-dialog__wrapper']

  // 保存原有document.body.appendChild方法
  const originFn = document.body.appendChild.bind(document.body)

  // 重写appendChild方法
  document.body.appendChild = (dom) => {
    // 根据标记,来区分是否用新的挂载方式
    let count = 0
    whiteList.forEach((x) => {
      if (dom.className.includes(x)) count++
    })
    if (count > 0 && container.container) {
      // 有弹出框的时候,挂载的元素挂载到子应用上,而不是主应用的body上
      container.container.querySelector('#app').appendChild(dom)
    } else {
      originFn(dom)
    }
  }
}

/**
 * 之所以要标记使用和还原document.body.appendChild方法,
 * 是因为主应用和子应用中有很多组件都用到了这个方法,比如select,日历组件。
 * 不还原这个方法的话,这些组件的样式会受到破坏。
 */
export async function unmount() {
  instance.$destroy()
  instance.$el.innerHTML = ''
  instance = null
  instance?.unmount?.()
  instance = null
  history?.destroy?.()
  document.body.appendChild = originFn
}

这样使用样式隔离就不会报错,且动态弹窗也不会丢失样式。

欢迎关注我的个人公众号:javascript艺术
  • 24
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值