vue组件中的样式隔离方式与原理解析

scoped实现样式隔离与穿透

scoped在vue单文件组件中用来标记style标签,让该标签内的css选择器只能选中组件内的元素。就是说让组件内的样式与其它组件隔离,不会影响父组件、子组件的样式。

scoped的实现原理

vue模板编译器在编译有scoped的stye标签时,会生成对应的postCSS插件,该插件会给每个scoped标记的style标签模块,生成唯一一个对应的data-v-xxxhash字符串,同时:

  • 该字符串会作为属性,添加到该组件内,每一个元素上。

    如果存在子组件,则只会在子组件顶级根元素添加该属性(透传),无法给子组件内部元素添加。

  • scoped标记的style标签内,每个选择器都会联合该属性选择器(所有选择器都会默认被重写)。

    如果是嵌套选择器,会在最内层的选择器联合上属性选择器。

.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编译里处理。

/* 指定给.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配置。

实现:

  1. 配置css-loader,处理css文件(模块)时,进行类名替换

    module.exports = {
        module: {
            rules: [
                {
                    test:/\.css$/,
                    exclude: /node_modules/, // 排除依赖里的代码
                    use: [
                        'style-loader', // 替换类名后,把css插入html头部
                        {
                            loader: 'css-loader',
                            options: {
                                modules: true, // 开启css模块
                            }
                        }
                    ]
                }
            ]
        }
    }
    
  2. 导入css文件(模块)时,用一个变量接收导入结果,这个结果就包含原始类名和被替换后类名的映射。其中key是原始类名,value是被替换后的。

    // 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一样,实现组件样式与其它组件隔离。

步骤:

  1. 在单位组件的style标签添加module属性,取值为自定义的映射名(说白了就是映射对象的名字)

    <style module="cssmodule">
    .red {
      color: red;
    }
    .box .text {
      font-size: large;
    }
    style>
    
  2. template中可直接拿到该映射对象。

    • style标签里的每一个css类,都会全部变成映射对象里的属性
    <template>
      <div :class="cssmodule.box">
        <span :class="[cssmodule.text, cssmodule.red]">
          hellow
        span>
      div>
    template>
    
  3. setup标签中通过useCssModulehooks也可以拿到css映射对象

    <script setup lang="ts">
    import { useCssModule } from 'vue'
    
    // 具名情况下, 返回