Vue项目中CSS Modules和Scoped CSS的介绍与区别
背景
在前端工程化飞速发展的时候,作为非编程语言的CSS在融入模块化的浪潮时产生了很多问题:
- 无法做到样式模块化
组件化开发是前端模块化的核心,但是原生CSS的思想是样式的层叠,对于组件来说并不友好,会造成组件样式被覆盖等问题。
于是我们希望样式是存在作用域的,即在组件的作用域内,组件样式只对该组件生效。
- 命名混乱
在大型项目中,多人合作经常容易产生命名混乱的问题,直接后果就是代码风格不统一、样式冲突等。
- 高重复
组件开发也意味着有很多样式代码是重复的,在项目中显得十分冗余。
于是我们希望存在一种机制可以导入和导出CSS,做到样式的复用。
解决CSS模块化的方案有很多种,在Vue项目中,Vue Loader支持的两种分别是Scoped CSS和CSS Modules。
Scoped CSS是Vue Loader默认支持的,因此在Vue项目中可以直接使用;
CSS Modules被Vue Loader视为模拟Scoped CSS的替代方案,也提供了集成来支持,但是需要写入一些配置。
// vue.config.js // 使用vue-cli的话,可以直接在该文件中配置,不用暴露webpack的配置 ? module.exports = { css: { // 开启CSS Modules requireModuleExtension: true; loaderOptions: { // 向loader传递配置选项 css: { modules: { // 自定义类命名规则 // 在我们的项目中,对类的命名是根据环境不同而改变的,开发环境中会展示该类的文件路径方便调试,其他环境中会 使用hash值封装 localIdentName: process.env.NODE_ENV === 'development' ? '[path][name]__[local]' : '[hash:base62:8]', } } } } }
需要知道的是,这两个解决方案都不是CSS的官方规范,只是我们通过一些构建工具,比如Webpack或者脚手架来加载对应的Loader,从而对CSS代码进行一些转换,来做到实现模块化的效果。
CSS Modules
原理
CSS Modules实现CSS模块化的原理就是根据我们在config文件中定义的类名命名规则给类生成一个独一无二的命名,从而实现作用域的隔离。
- 转化前
<style module> .example { color: red; } </style> ? <template> <div class="example">hi</div> </template>
- 转化后
<!-- 转化规则是自己定义的 --> <!-- 生产环境中,直接用hash值封装 --> <style module> ._1TdbN_VT { color: red; } </style> ? <template> <div class="._1TdbN_VT">hi</div> </template> ? <!-- 开发环境中,会显示来源 --> <style module> .src-views-login-Index__example { color: red; } </style> ? <template> <div class=".src-views-login-Index__example">hi</div> </template>
规则
- Vue项目中的使用
在style标签中需要声明module特性:
<style module> ... </style>
声明module特性后,Vue Loader会生成一个$style计算属性,向组件注入CSS Modules对象,我们可以通过使用该计算属性在template和JS中访问到CSS Modules对象。
<template> <!-- 直接通过$style来访问red类,同理$style作为计算属性也支持对象和数组语法 --> <p :class="$style.red"> This should be red </p> </template>
- Composition:样式的组合
.classA { color: green; background: red; } ? .classB { composes: classA; color: red; }
使用composes规则可以把其他类和该类的样式组合起来
要注意composes声明的规则必须在自身规则之前;
可以一次性组合多个class,类名之间用空格隔开,但是这些class都必须是local scoped的
- Dependencies:通过导入实现依赖
如果需要组合其他module里的样式,可以通过导入的方式:
.classC { composes: classA from './style.css' }
- 要注意当组合了不同CSS文件中的不同类时,CSS Modules是默认这些类之间不存在先后顺序的,因此导入时要保证这些类之间没有规则冲突
- 组合的类之间不能存在循环依赖
- composes实现的原理就是通过loader把import的CSS文件处理成ICSS格式的中间件,导出一个保存了所有local类名到global类名映射的对象,实际上这也是在JS文件中直接import CSS文件的原理
:global选择器
:global()允许括号中声明的选择器命中全局,即其类名不会经过规则封装,因此不受作用域的限制。
实际项目中,当我们希望修改所使用组件库的默认样式时,在使用CSS Modules方案的情况下,就可以通过:global()来修改其默认样式,但是要注意最好外面有一层类封装,否则可能影响全局样式。
Vue3新特性
- 自定义module命名
<style module="login"> ... </style>
支持给module命名,可以用module名替代$style,比如<div :class="login.red"></div>。
- 组合式API中的CSS Modules
在setup()中可以使用useCssModuleAPI来获取module对象。
- 在CSS中使用v-bind
支持在style中把某条rule的值和data中的一个数据绑定,比如:
.red { color: v-bind(color); }
Scoped CSS
原理
Vue Loader默认使用CSS后处理器PostCSS来实现Scoped CSS,原理就是给声明了scoped的样式中选择器命中的元素添加一个自定义属性,再通过属性选择器实现作用域隔离样式的效果。
- 转化前
<style module> .example { color: red; } </style> ? <template> <div class="example">hi</div> </template>
- 转化后
<!-- 用自定义属性把类名封装起来了 --> <style> .example[data-v-f3f3eg9] { color: red; } </style> ? <template> <div class="example" data-v-f3f3eg9>hi</div> </template>
规则
- 一个Vue文件中可以同时存在global和scoped的样式,即允许声明两个style标签。
<style> /* global styles */ </style> ? <style scoped> /* local styles */ </style>
使用Scoped CSS以后,因为样式具有了作用域,所以父组件的样式是不会影响到子组件的,即父组件和子组件的样式都具有自己的作用域。
但是对于子组件的根元素来说,其样式还是可以受父组件控制的,使得父组件可以控制布局。
- 注意通过v-html创建的DOM内容是不受Scoped CSS控制的,如果希望修改其中的样式,可以通过深度作用选择器。
- 因为Scoped CSS是通过属性选择器实现的,所以最好不要和标签选择器混用,会产生性能问题。
深度作用选择器
- 深度作用选择器使得父组件的样式可以渗透到子组件,其原理是使用后代选择器。
/* 转化前 */ <style scoped> .a :deep(.b) { /* ... */ } </style> ? /* 转化后 */ .a[data-v-f3f3eg9] .b { /* ... */ }
需要注意深度作用选择器和声明为global样式的区别,深度作用选择器只是为了能让父组件控制子组件样式,而global样式是全局起效的。
实际项目中,当我们希望修改所使用组件库的默认样式时,在使用Scoped CSS方案的情况下,就可以通过深度作用选择器来修改其默认样式。
几种深度左右选择器的写法:
- /deep/:已废弃
- >>>:在不使用Sass预处理器时可以使用
- ::v-deep:使用Sass预处理器时使用
Vue3新特性
深度作用选择器
废弃/deep和>>>,使用:deep(.child-class)来替代::v-deep
:slotted()选择器
支持使用:slotted(selector)来控制slot中的样式
:global()选择器
当只有某些规则需要全局起效时,允许不重复声明一个全局作用域的style标签,而是使用:global(selector)来声明为全局样式。
二者的比较
CSS Modules | Scoped CSS |
---|---|
需要在vue.config.js中额外配置 | Vue Loader默认支持,无需额外配置 |
通过根据配置的类命名规则,为元素生成独一无二的类名来实现作用域隔离 | 通过给元素自定义hash属性,再使用属性选择器选中元素来实现作用域隔离 |
在style标签中声明module | 在style标签中声明scoped |
支持导入其他module的样式,支持样式组合 | / |
通过:global()来解除作用域的隔离,使样式在全局生效 | 1. 可以定义全局样式,使样式不受作用域约束;2. 可以通过深度作用选择器命中子组件,从而控制子组件的样式 |