项目背景
主应用 vue2 + element;
子应用 vue3 + element-plus。
问题描述
子应用目前的路由路口放在了主应用内,但是之后的业务中会作为一个项目独立出来;需要保证主应用和子应用的样式不能互相影响;
而主应用的老代码由于历史原因,有很多不规范的全局样式,因此对子应用中使用了 strictStyleIsolation: true
开启严格样式隔离;
同时,在子应用的 main.ts
入口文件中,导入了 element-plus
然而项目运行之后发现 element-plus 组件的部分样式居然失效了?!
原因分析
首先,qiankun 严格样式隔离的原理是通过创建一个 shadow dom,并把子应用包裹在 shadow dom 里面:
同时,子应用的样式也都会被挂在 shadow dom 的根节点下:
而 element-plus 的全局变量是通过 :root
选择器来作用到根节点的,但是在 shadow dom 中,是无法通过 :root
来选中根节点的,也就导致了这部分的样式失效。
其实 shadow dom 设计的初衷就是为了代码隔离,避免元素之间互相影响;从这个角度理解 shadow dom 中的样式表通过 :root
无法选中根节点,似乎也合情合理。
but,合理归合理,问题还是得解决的~
解决思路
实际上,在 shadow dom 中也是存在根节点的,这个根节点名为 shadow host ,我们可以用 :host
来代替 :root
选中 shadow dom 对应的根节点:
shadow dom
那我们只要将样式文件中的 :root
替换成 :host
,不就可以使样式重新生效了吗!
这里给大家提供几个解决方案:
-
在项目本地维护一份 element-plus 相关样式文件,手动将样式文件中的
:root
替换成:host
。但这样做的问题就是:如果更新了 element-plus,要记得去手动同步一下这个样式文件。
-
介入应用打包构建流程,通过修改编译时信息实现对样式文件的替换。
乍一听十分高大上,其实说白了就是写一个 loader,在 webpack 打包文件的时候把
:root
替换成:host
;
我在项目中最终采用的是第 2 个方案,下面我们来看看如何实现:
编写 loader
大家千万别被编写 loader 吓到,这个 loader 的逻辑十分简单: 我们直接在项目中新建一个 js 文件,名为 my-style-loader.js
,代码如下:
js
// my-style-loader.js
const myStyleLoader = function (source) {
return source.replace(/:root/g, ':host')
}
module.exports = myStyleLoader
这段代码只做了两件事情:
- 定义了一个函数,拿到上一个 loader 解析的资源文件,把
:root
替换成:host
,然后再返回替换后的资源文件。 - 然后导出这个函数。
so easy~
找准 loader 作用时机
代码完成了,接下来就是考虑怎么使用这个 loader 了。
我们知道最终页面上被包裹在 <style>
标签中的样式内容,实际上是各个 loader 链式作用后的结果,那么我们编写的这个 loader 应该放在哪个步骤呢?我们来一起分析一下:
首先 element-plus 样式的入口是个.scss
文件,内容如下:
element-plus/theme-chalk/src/index.scss
而一个 .scss
文件在打包的时候,一般会经历以下步骤:
- 首先会通过
sass-loader
,解析文件中的语法,将.scss
文件转换成.css
文件; - 接下来,
css-loader
会接收到sass-loader
转换后的资源文件,并进一步解析文件中的 url 路径、@import 语法等等; - 最后通过
style-loader
,将解析好的样式文件包裹在<style>
标签内,并挂载到 dom 上;
严格来讲,
style-loader
的作用时机是早于sass-loader
和css-loader
,这和 loader 的机制有关系,大家可以先按照这个顺序来理解,关键是了解每个 loader 做了什么,从这一点来考虑我们编写的 loader 的插入顺序
这么一看,我们编写的 loader 只要作用在 sass-loader
之后,style-loader
之前就可以了。
修改 webpack 配置
最后一步,就是修改 webpack 配置了。
由于 webpack 默认是从 node_modules 中寻找 loader 的,所以我们要把我们编写的 loader 路径,添加到 webpack loader 解析路径的规则中:
js
module.exports = {
//...
resolveLoader: {
modules: [
'node_modules',
'./src/loaders' // 在这里添加我们编写的 loader 所在路径
],
},
};
然后我们需要在 webpack 配置中新增一条规则:
js
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 来导入这个文件:
js
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 挂载到页面上。
这么一来就大功告成啦!