scoped实现样式隔离与穿透
scoped在vue单文件组件中用来标记style标签,让该标签内的css选择器只能选中组件内的元素。就是说让组件内的样式与其它组件隔离,不会影响父组件、子组件的样式。
scoped的实现原理
vue模板编译器在编译有scoped的stye标签时,会生成对应的postCSS插件,该插件会给每个scoped标记的style标签模块,生成唯一一个对应的data-v-xxxhash
字符串,同时:
-
该字符串会作为属性,添加到该组件内,每一个元素上。
如果存在子组件,则只会在子组件顶级根元素添加该属性(透传),无法给子组件内部元素添加。
-
scoped标记的style标签内,每个选择器都会联合该属性选择器(所有选择器都会默认被重写)。
如果是嵌套选择器,会在最内层的选择器联合上属性选择器。
css
.ipt {}
.ipt .el-input__wrapper {}
.ipt .el-input__wrapper span {}
/*结果,每个scoped对应的唯一字符串,会联合到最内层的选择器*/
.ipt[data-v-88888888] {}
.ipt .el-input__wrapper[data-v-88888888] {}
.ipt .el-input__wrapper span[data-v-88888888] {}
样式隔离效果:
有scoped的style标签内的选择器,只能选中本组件元素,无法选中子组件的元素。即,在父组件使用子组件的选择器修改样式,是无法选中子组件内的元素的。
- 生成联合属性选择器时,默认只给最内层选择器联合。限定了每个(嵌套)选择器选中的元素,必须是带有唯一属性标记的元素。而子组件内的元素是不会带上该属性标记的。
scoped中的样式穿透原理
样式穿透意思是:让父组件在有scoped的标签里,使用子元素的选择器来修改子元素的样式,即父组件定义的样式穿透到子组件(默认无法穿透)。
vue3中只能使用:deep()
来实现样式穿透,而>>> /deep/
已经被废弃。
原理:
-
生成联合选择器时,不再默认给最内层的选择器联合,而是手动指定某个选择器的上一级选择器进行联合。
vue编译分为三部分:script、template、style,scoped处理就在style编译里处理。
css
/* 指定给.el-input__wrapper前面的ipt联合属性选择器 */
.union .ipt :deep(.el-input__wrapper) input {}
/* 结果: */
.union .ipt[data-v-0904fc8e] .el-input__wrapper input {}
CSS Modules实现样式隔离
css Modules的作用和vue中的scoped作用一样,都是用来实现样式的隔离。在webpack的项目中是css-loader提供的能力。在vite中已经适配了vue3单文件组件。
实现原理
将导入的css模块里的自定义css类名,替换为一个无语义,但唯一的字符串类名,并提供一个对象充当map映射来访问转换后的类名。
我们先来介绍wepack中的实现,来理解它的执行过程。
webpack中的CSS Modules
在webpack中通过配置css-loader,即可实现CSS Modules。可以应用在react或原生等场景中。但它的应用场景更多是让两个css模块(即使有相同的类名)能够互相隔离。
原理:
- 转换:导入一个css模块时,将所有的css类的类名都替换成一个唯一的字符串。说白了就是,将自定义css类名都替换成无语义,但唯一的类名。
- 访问转换映射:导入该css模块时,可以拿到这个模块里自定义类名和对应被替换后的类名的映射(一个对象),访问它身上的未转换前的css类名,就可以拿到转换后的类名。然后将转换后的类添加到对应的元素上的classList即可。
- 插入到html文档中的css是转换类名后的css。
效果:
-
当导入两个css文件,即使有相同的类名,也不会相互影响。但是需要根据拿到的映射对象,给元素加上被替换后的唯一类名。
-
为了避免所有css文件都会进行转换类名操作(有些全局css模块不需要转换),可以给不需要转换的css文件起一个特殊的文件名,并通过test正则,为它们匹配一套不会转换的css-loader配置。
实现:
-
配置css-loader,处理css文件(模块)时,进行类名替换
jsmodule.exports = { module: { rules: [ { test:/\.css$/, exclude: /node_modules/, // 排除依赖里的代码 use: [ 'style-loader', // 替换类名后,把css插入html头部 { loader: 'css-loader', options: { modules: true, // 开启css模块 } } ] } ] } }
-
导入css文件(模块)时,用一个变量接收导入结果,这个结果就包含原始类名和被替换后类名的映射。其中key是原始类名,value是被替换后的。
js// a.css .test .div { color: green; } // b.css .test .div { color: yellow; } // index.js import style1 from 'a.css' import style2 from 'b.css' // 把页面上第一个有test类的元素加上a.css里test类对应的样式 document.queryselector('.test').classList.add(style1.test)
vue3中的CSS Modules
vue3组件中应用CSS Modules是通过在style添加module属性,让该style中的类名都替换成唯一类名,并且类名映射对象只能在组件内部访问。最终的效果就是和scoped一样,实现组件样式与其它组件隔离。
步骤:
-
在单位组件的style标签添加module属性,取值为自定义的映射名(说白了就是映射对象的名字)
html<style module="cssmodule"> .red { color: red; } .box .text { font-size: large; } </style>
-
template中可直接拿到该映射对象。
- style标签里的每一个css类,都会全部变成映射对象里的属性
html<template> <div :class="cssmodule.box"> <span :class="[cssmodule.text, cssmodule.red]"> hellow </span> </div> </template>
-
setup标签中通过
useCssModule
hooks也可以拿到css映射对象html<script setup lang="ts"> import { useCssModule } from 'vue' // 具名情况下, 返回 <style module="classes"> 内的 class 映射 let cssModules = useCssModule('cssmodule'); // 匿名情况下, <style module> 则不需要传参 // let cssModules = useCssModule(); console.log(cssModules); </script>
完整示例:
在vue中,使用scoped会更简单,所以CSS Modules一般用在render函数里。
html
<script setup lang="ts">
import { useCssModule } from 'vue'
// 具名情况下, 返回 <style module="cssmodule"> 内的 class 映射对象
let cssModules = useCssModule('cssmodule');
// 匿名情况下, 如<style module> ,则useCssModule调用时不需要传参
// let cssModules = useCssModule();
console.log(cssModules);
</script>
<template>
<!-- 通过style标签的module属性值获取类名 -->
<!-- 如果<style module>,则通过$style访问: <div :class="$style.box"> -->
<div :class="cssmodule.box">
<span :class="[cssmodule.text, cssmodule.red]">
hellow
</span>
</div>
</template>
<!--module属性可以只占位(匿名)如:<style module> -->
<style module="cssmodule">
.red {
color: red;
}
.box .text {
font-size: large;
}
</style>