微前端踩坑:qiankun 子应用中使用 element-plus 样式失效问题

项目背景

主应用 vue2 + element;
子应用 vue3 + element-plus。

问题描述

子应用目前的路由路口放在了主应用内,但是之后的业务中会作为一个项目独立出来;需要保证主应用和子应用的样式不能互相影响;
而主应用的老代码由于历史原因,有很多不规范的全局样式,因此对子应用中使用了 strictStyleIsolation: true 开启严格样式隔离;
同时,在子应用的 main.ts 入口文件中,导入了 element-plus

微前端踩坑:qiankun 子应用中使用 element-plus 样式失效问题
然而项目运行之后发现 element-plus 组件的部分样式居然失效了?!

微前端踩坑:qiankun 子应用中使用 element-plus 样式失效问题

原因分析

首先,qiankun 严格样式隔离的原理是通过创建一个 shadow dom,并把子应用包裹在 shadow dom 里面:

微前端踩坑:qiankun 子应用中使用 element-plus 样式失效问题

同时,子应用的样式也都会被挂在 shadow dom 的根节点下:

微前端踩坑:qiankun 子应用中使用 element-plus 样式失效问题

而 element-plus 的全局变量是通过 :root 选择器来作用到根节点的,但是在 shadow dom 中,是无法通过 :root 来选中根节点的,也就导致了这部分的样式失效。

微前端踩坑:qiankun 子应用中使用 element-plus 样式失效问题

其实 shadow dom 设计的初衷就是为了代码隔离,避免元素之间互相影响;从这个角度理解 shadow dom 中的样式表通过 :root 无法选中根节点,似乎也合情合理。

but,合理归合理,问题还是得解决的~

解决思路

实际上,在 shadow dom 中也是存在根节点的,这个根节点名为 shadow host,我们可以用 :host 来代替 :root 选中 shadow dom 对应的根节点:

微前端踩坑:qiankun 子应用中使用 element-plus 样式失效问题

shadow dom

那我们只要将样式文件中的 :root 替换成 :host,不就可以使样式重新生效了吗!

这里给大家提供几个解决方案:

  1. 在项目本地维护一份 element-plus 相关样式文件,手动将样式文件中的 :root 替换成 :host
    但这样做的问题就是:如果更新了 element-plus,要记得去手动同步一下这个样式文件。

  2. 介入应用打包构建流程,通过修改编译时信息实现对样式文件的替换。
    乍一听十分高大上,其实说白了就是写一个 loader,在 webpack 打包文件的时候把 :root 替换成 :host

我在项目中最终采用的是第 2 个方案,下面我们来看看如何实现:

编写 loader

大家千万别被编写 loader 吓到,这个 loader 的逻辑十分简单:
我们直接在项目中新建一个 js 文件,名为 my-style-loader.js,代码如下:

// my-style-loader.js
const myStyleLoader = function (source) {
  return source.replace(/:root/g, ':host')
}
module.exports = myStyleLoader

这段代码只做了两件事情:

  1. 定义了一个函数,拿到上一个 loader 解析的资源文件,把 :root 替换成 :host,然后再返回替换后的资源文件。
  2. 然后导出这个函数。

so easy~

找准 loader 作用时机

代码完成了,接下来就是考虑怎么使用这个 loader 了。

我们知道最终页面上被包裹在 <style> 标签中的样式内容,实际上是各个 loader 链式作用后的结果,那么我们编写的这个 loader 应该放在哪个步骤呢?我们来一起分析一下:

首先 element-plus 样式的入口是个.scss 文件,内容如下:

微前端踩坑:qiankun 子应用中使用 element-plus 样式失效问题

element-plus/theme-chalk/src/index.scss

而一个 .scss 文件在打包的时候,一般会经历以下步骤:

  1. 首先会通过 sass-loader解析文件中的语法,将 .scss 文件转换成 .css 文件
  2. 接下来,css-loader 会接收到 sass-loader 转换后的资源文件,并进一步解析文件中的 url 路径、@import 语法等等;
  3. 最后通过 style-loader将解析好的样式文件包裹在 <style> 标签内,并挂载到 dom 上

严格来讲,style-loader 的作用时机是早于 sass-loadercss-loader,这和 loader 的机制有关系,大家可以先按照这个顺序来理解,关键是了解每个 loader 做了什么,从这一点来考虑我们编写的 loader 的插入顺序

这么一看,我们编写的 loader 只要作用在 sass-loader 之后,style-loader 之前就可以了。

修改 webpack 配置

最后一步,就是修改 webpack 配置了。

由于 webpack 默认是从 node_modules 中寻找 loader 的,所以我们要把我们编写的 loader 路径,添加到 webpack loader 解析路径的规则中:

module.exports = {
  //...
  resolveLoader: {
    modules: [
    'node_modules',
    './src/loaders' // 在这里添加我们编写的 loader 所在路径
   ],
  },
};

然后我们需要在 webpack 配置中新增一条规则:

module: {
  rules: [
    {
      test: /\.scss$/,
      include: [
        path.resolve(__dirname, 'node_modules/element-plus/theme-chalk') // 这条规则只匹配element-plus 下的样式文件
      ],
      use: [
        'style-loader',
        'css-loader',
        'my-style-loader',
        'sass-loader'
      ]
    }
  ]
}

这里还有一种更为便捷的方式——通过内联 loader 来导入这个文件:

import '!!style-loader!css-loader!my-style-loader!sass-loader!element-plus/theme-chalk/src/index.scss'

其中 !! 表示禁用其它所有的 loader 配置,只启用内联 loader;每个 loader 之间又通过 ! 来分隔。

因此上面代码翻译过来就是:

针对这个 .scss 文件,先用 sass-loader 处理 sass 语法,再用我们自己编写的 my-style-loader 替换指定选择器,然后交给 css-loader 解析,最后再通过 style-loader 挂载到页面上。

这么一来就大功告成啦!

原文链接:https://juejin.cn/post/7262317630308384824 作者:hprep

(0)
上一篇 2023年8月2日 上午10:00
下一篇 2023年8月2日 上午10:10

相关推荐

发表回复

登录后才能评论