Vue中的作用域CSS和CSS模块的区别


Posted in Javascript onOctober 09, 2018

现代Web开发中的CSS离完美还差得远,这并不奇怪。现在,项目通常是相当的复杂的,而CSS样式又是全局性的,所以到最后总是极容易地发生样式冲突: 样式相互覆盖 隐式地级联到我们未考虑到的元素

为了减轻CSS存在的主要痛点,我们在项目中普遍采用 BEM 的方法来。不过这只能解决CSS问题中的一小部分。

对我们来说是幸运的,社区已经开发出了可以帮助我们更彻底地解决问题的解决方案。你可能已经听说过 CSS Modules 、 Styled Componetns 、 Glamorous 或 JSS 。这些只是我们今天可以添加到项目中的一些最流行的工具。如果你对这个话题感兴趣,你可以查看这篇文章: @Indrek Lasn 详细介绍了 CSS in JS的全部思想 。

使用Vue-cli构建的Vue应用程序提供了两个很棒的内置解决方案: 作用域CSS 和 CSS Modules 。它们都有一些优点和缺点,所以让我们仔细看看哪种解决方案更适合你。

作用域CSS

在Vue中引入了CSS作用域 scoped 这个概念, scoped 的设计思想就是让当前组件的样式不会影响到其他地方的样式,编译出来的选择器将会带上 data-v-hash 的方式来应用到对应的组件中,这样一来,CSS也不需要添加额外的选择器。也将解决CSS中选择器作用域和选择器权重的问题。

在Vue中,为了让作用域样式工作,只需要在 <style> 标签添加 scoped 属性:

<!-- Button.vue -->
<template>
 <button class="btn">
 <slot></slot>
 </button>
</template>
<style scoped>
 .btn {
 color: red;
 }
</style>

通过使用PostCSS并将上面的示例转换为以下内容,它仅将我们的样式应用于相同的组件中的元素:

Vue中的作用域CSS和CSS模块的区别

就像你看到的一样,整个过程不需要做什么就可以达到很好的效果: 作用域样式 (CSS中一直以来令人头痛的问题之一)。

现在假设你需要调整 Button 组件的宽度,你可以像平常使用一样,在调用这个组件的地方添加一个额外的 class 来设置其样式:

<!-- App.vue -->
<template>
 <div id="app">
 <Button class="btn-lg">click</Button>
 </div>
</template>
<script>
 import Button from "./components/Button";
 export default {
 name: "App",
 components: {
  Button
 }
 };
</script>
<style scoped>
 .btn-lg {
 padding: 10px 30px;
 }
</style>

转换后就像下面这样:

Vue中的作用域CSS和CSS模块的区别

这次还是一样,不需要做什么就可以很好的控制样式。

不过请注意:这个特性存在一个缺陷,即如果你子组件的元素上有一个类已经在这个父组件中定义过了,那么这个父组件的样式就也会应用到子组件上。只不过其权重没有子组件同类名的重。比如下面这个示例:

<!-- Button.vue -->
<template>
 <button class="btn btn-lg">
 <slot></slot>
 </button>
</template>
<style scoped>
.btn {
 color: red;
}
.btn-lg {
 padding: 10px 20px;
 border: 2px solid red;
}
</style>
<!-- App.vue -->
<template>
 <div id="app">
 <Button class="btn-lg">click</Button>
 </div>
</template>
<script>
 import Button from "./components/Button";
 export default {
 name: "App",
 components: {
  Button
 }
 };
</script>
<style scoped>
.btn-lg {
 padding: 30px;
 border: 5px solid green;
}
</style>

编译出来的效果如下:

Vue中的作用域CSS和CSS模块的区别

还有一些情况是我们需要对子组件的深层次结构设置样式。虽然这种做法并不受推荐,而且应该尽量去避免。比如下面这个示例, Button 组件下有一个 <span> 标签,而在调用 Button 组件的父组件 App 中设置 span 样式:

<!-- Button.vue -->
<template>
 <button class="btn">
 <span>
  <slot></slot>
 </span>
 </button>
</template>
<style scoped>
.btn {
 color: red;
}
</style>
<!-- App.vue -->
<template>
 <div id="app">
 <Button class="btn-lg">click</Button>
 </div>
</template>
<script>
 import Button from "./components/Button";
 export default {
 name: "App",
 components: {
  Button
 }
 };
</script>
<style scoped>
.btn span {
 color: green;
 font-weight: bold;
 border: 1px solid green;
 padding: 10px;
}
</style>

编译出来的结果如下:

Vue中的作用域CSS和CSS模块的区别

从上面的结果可以看出来,在父组件 App.vue 中的样式:

.btn span {
 color: green;
 font-weight: bold;
 border: 1px solid green;
 padding: 10px;
}

上面这段样式并没有编译出来,运用到子组件 Button.vue 中的 span 中。

在 scoped 样式中,这种情况可以使用 >>> 连接符或者 /deep/ 来解决:

<!-- App.vue -->
<style scoped>
 .btn >>> span {
 color: green;
 font-weight: bold;
 border: 1px solid green;
 padding: 10px;
 }
</style>

此时虽然依旧是在 App.vue 中 scoped 控制 Button.vue 组件中 span ,但上面不同的是,这次样式生效。编译出来的结果如下:

Vue中的作用域CSS和CSS模块的区别

另外使用作用域样式还存在一个问题。那就是对 v-html 中内在的标签样式不生效。比如下面这个示例:

<!-- Button.vue -->
<template>
 <button class="btn">
 <slot></slot>
 </button>
</template>
<style scoped>
.btn {
 color: red;
}
</style>
<!-- App.vue -->
<template>
 <div id="app">
 <Button class="btn-lg" v-html="vhtml"></Button>
 </div>
</template>
<script>
 import Button from "./components/Button";
 export default {
 name: "App",
 data () {
  return {
  vhtml: 'Click <strong>7</strong>'
  }
 },
 components: {
  Button
 }
 };
</script>
<style scoped>
strong {
 color: green;
 border: 1px solid green;
 padding: 10px;
}
</style>

编译出来的结果如下:

Vue中的作用域CSS和CSS模块的区别

从上图可以看出来, v-html 中的 strong 标签样式并未生效。和前面在父组件的 scoped 中设置子组件内部标签未生效一样。当然,其解决方案也是同样的, 使用 >>> 连接符或 /deep/ 可以让 v-html 中的标签样式生效。比如上面的示例,可以将代码修改为:

<!-- App.vue -->
<style scoped>
 .btn /deep/ strong {
 color: green;
 border: 1px solid green;
 padding: 10px;
 }
</style>

这个时候 v-html 中的 strong 样式生效了,如下图所示:

Vue中的作用域CSS和CSS模块的区别

话又说回来,虽然 >>> 或 /deep/ 可以帮助我们穿透已封装好的组件中的样式,但这也失去了组件封装的效果。再次回到以前CSS中令人头痛的问题: CSS作用域 。

简单的小结一下,在Vue中 scoped 属性的渲染规则:

给DOM节点添加一个不重复的 data 属性(比如 data-v-7ba5bd90 )来表示他的唯一性
在每个CSS选择器末尾(编译后生成的CSS)加一个当前组件的 data 属性选择器(如 [data-v-7ba5bd90] )来私有化样式。选择器末尾的 data 属性和其对应的DOM中的 data 属性相匹配
如果组件内部包含有其他组件,只会给其他组件的最外层标签加上当前组件的 data 属性
上面我们看到的是Vue机制内作用域CSS的使用。在Vue中,除了作用域CSS之外,还有另外一种机制,那就是 CSS Modules ,即 模块化CSS 。

CSS Modules

CSS Modules的流行起源于React社区,它获得了社区的迅速的采用。Vue更甚之,其强大,简便的特性在加上Vue-cli对其开箱即用的支持,将其发展到另一个高度。

在Vue中使用CSS Modules和作用域CSS同样的简单。和作用域CSS类似,在 <style> 标签中添加 module 属性。比如像下面这样:

<style module>
 .btn {
 color: red;
 }
</style>

然后在 <template> 里这样写:

<template>
 <button :class="$style.btn">{{msg}}</button>
</template>

这个时候编译出来的效果如下:

Vue中的作用域CSS和CSS模块的区别

正如上图所示, :class="$style.btn" 会被 vue-template-compiler 编译成为 .Button_btn_3ykLd 这个类名,并且样式的选择器也自动发生了相应的变化。

但在这里有一点需要注意,我们平时有可能在类名中会使用分隔线,比如:

<style module>
 .btn-lg {
  border: 1px solid red;
  padding: 10px 30px;
 }
</style>

如果通过 $style 调用该类名时要是写成 $style.btn-lg ,这样写是一个不合法的JavaScript变量名。此时在编译的时候,会报一个错话信息:

Vue中的作用域CSS和CSS模块的区别

按钮的样式也不会生效。如果要生效,我们需要通过下面这样的方式来写:

<template>
 <button :class="$style['btn-lg']">{{msg}}</button>
</template>

编译出来的结果如下:

Vue中的作用域CSS和CSS模块的区别

除了$style.btn-lg这种方式会报错之外,写在驼峰($style.btnLg)的也会报错。

上面说的 module 属性会经由Vue-loader编译后,在我们的 component 产生一个叫 $style 的隐藏的 computed 属性。也就是说,我们甚至可以在Vue生命周期的 created 钩子中取得由CSS Modules生成的 class 类名:

<script>
export default {
 created () {
  console.log(this.$style['btn-lg'])
 }
}
</script>

在浏览器的 console 中可以看到 modules 编译出来对应的类名:

Vue中的作用域CSS和CSS模块的区别

利用这样的特性,在 <template> 也可以这样写:

<!-- App.vue -->
<template>
 <div id="app">
  <Button msg="Default Button" />
  <Button :class="{[$style['btn-lg']]: isLg}" msg="Larger Button" />
  <Button :class="{[$style['btn-sm']]: isSm}" msg="Smaller Button" />
 </div>
</template>
<script>
 import Button from './components/Button'
 export default {
  name: 'app',
  components: {
   Button
  },
  data () {
   return {
    isLg: true,
    isSm: false
   }
  }
 }
</script>
<style module>
.btn-lg {
 padding: 15px 30px;
}
.btn-sm {
 padding: 5px;
}
</style>

这个时候编译出来的结果如下:

Vue中的作用域CSS和CSS模块的区别

如上图所示,当 data 中的 isLg 属性值为 true 时, Larger Button 按钮的 padding 变了,按钮也同时变大了。除此之外,我们还可以通过 props 将 class 传到子组件中。比如像下面这样使用:

<!-- Button.vue -->
<template>
 <button :class="[$style.btn, primaryClass]">{{msg}}</button>
</template>
<script>
 export default {
  name: 'Button',
  props: {
   msg: String,
   primaryClass: ''
  }
 }
</script>
<style module>
 .btn {
  border: 1px solid #ccc;
  border-radius: 3px;
  padding: 5px 15px;
  background: #fefefe;
  margin: 5px;
 }
</style>
<!-- App.vue -->
<template>
 <div id="app">
  <Button msg="Default Button" />
  <Button :class="{[$style['btn-lg']]: isLg}" msg="Larger Button" />
  <Button :class="{[$style['btn-sm']]: isSm}" msg="Smaller Button" />
  <Button msg="Primary Button" :primaryClass="$style['btn-primary']" />
 </div>
</template>
<script>
 import Button from './components/Button'
 export default {
  name: 'app',
  components: {
   Button
  },
  data () {
   return {
    isLg: true,
    isSm: false
   }
  }
 }
</script>
<style module>
 .btn-lg {
  padding: 15px 30px;
 }
 .btn-sm {
  padding: 5px;
 }
 .btn-primary {
  background: rgb(54, 152, 244);
  border-color: rgb(32, 108, 221);
  color: #fff;
 }
</style>

编译出来的效果如下图所示:

Vue中的作用域CSS和CSS模块的区别

如果我们想要在JavaScript里面将独立的CSS文件作为CSS模块来加载的话,需要在 .css 文件名前添加 .module 前缀,比如:

Vue中的作用域CSS和CSS模块的区别

<script>
 import barStyle from './src/style/bar.module.css'
</script>

如果你是在项目中引入的是处理器文件也是如此,比如 .scss 文件:

<script>
 import fooSassStyle from './src/scss/foo.module.scss'
</script>

如果你觉得这样比较麻烦,可以在 vue.config.js 文件中 css.modules 设为 true :

// vue.config.js
module.exports = {
 css: {
  modules: true
 }
}

注意,上面的示例创建的项目是使用Vue-cli 3创建的。如果是使用Webpack的话,需要根据Webpack的相关机制进行配制。

从上面的示例中我们可以看出。使用 module 和 scoped 不一样的地方就是在于所有创建的类可以通过 $style 对象获取。因此类要应用到元素上,就需要通过 :class 来绑定 $style 这个对象。它的好处是,当我们在HTML中查看这个元素时,我们可以立刻知道它所属的是哪个组件。如果你够细心的话,可以看到编译出来的类名,都会以组件名为前缀,比如:

除了这个好处之外,还有另一个好处,即: 一切都变成显式的了,我们拥有了彻底的控制权 。

总结

不管是CSS Modules还是作用域CSS,这两种方案都非常简单,易用。在某种程度上解决的是同样的痛点(CSS的痛)。那么你应该选择哪种呢?

scoped 样式的使用不需要额外的知识,给人舒适的感觉。它所存在的局限,也正它的使用简单的原因。它可以用于支持小型到中型的Web应用程序。在更大的Web应用程序或更复杂的场景中,对于CSS的运用,我们更希望它是显式的,更具有控制权。比如说,你的样式可以在多组件中重用时,那么 scoped 的局限性就更为明显了。反之,CSS Modules的出现,正好解决了这些问题,不过也要付出一定的代价,那就是需要通过 $style 来引用。虽然在 <template> 中大量使用 $style ,让人看起来很蛋疼,但它会让你的样式更加安全和灵活,更易于控制。CSS Modules还有一个好处就是可以使用JavaScript获取到我们定义的一些变量,这样我们就不需要手动保持其在多个文件中同步。

最后还是那句话, 任何解决CSS的方案,没有最好的,只有最合适的! 我们应该根据自己的项目、场景和团队进行选择。当然,不管选择哪种方案,都是为了帮助我们更好的控制样式,解决原生CSS中存在的痛点。最后希望这篇文章对大家有所帮助。

Javascript 相关文章推荐
Javascript调试工具(下载)
Jan 09 Javascript
代码生成器 document.write()
Apr 15 Javascript
jquery 输入框数字限制插件
Nov 10 Javascript
node.js应用后台守护进程管理器Forever安装和使用实例
Jun 01 Javascript
Internet Explorer 11 浏览器介绍:别叫我IE
Sep 28 Javascript
jQuery实现表单提交时判断的方法
Dec 13 Javascript
JavaScript数据存储 Cookie篇
Jul 02 Javascript
利用VS Code开发你的第一个AngularJS 2应用程序
Dec 15 Javascript
小程序自定义单页面、全局导航栏的实现代码
Mar 15 Javascript
vue实现带过渡效果的下拉菜单功能
Feb 19 Javascript
vue radio单选框,获取当前项(每一项)的value值操作
Sep 10 Javascript
JS中如何优雅的使用async await详解
Oct 05 Javascript
利用JS动态生成隔行换色HTML表格的两种方法
Oct 09 #Javascript
对angularJs中自定义指令replace的属性详解
Oct 09 #Javascript
基于vue2.0的活动倒计时组件countdown(附源码下载)
Oct 09 #Javascript
Vue中的$set的使用实例代码
Oct 08 #Javascript
js根据json数据中的某一个属性来给数据分组的方法
Oct 08 #Javascript
基于Angular中ng-controller父子级嵌套的相关属性详解
Oct 08 #Javascript
Bootstrap fileinput 上传新文件移除时触发服务器同步删除的配置
Oct 08 #Javascript
You might like
php设计模式 Builder(建造者模式)
2011/06/26 PHP
CodeIgniter基本配置详细介绍
2013/11/12 PHP
Laravel中扩展Memcached缓存驱动实现使用阿里云OCS缓存
2015/02/10 PHP
php集成套件服务器xampp安装使用教程(适合第一次玩PHP的新手)
2015/06/03 PHP
PHP中array_keys和array_unique函数源码的分析
2016/02/26 PHP
老生常谈PHP位运算的用途
2017/03/12 PHP
关于PHP虚拟主机概念及如何选择稳定的PHP虚拟主机
2018/11/20 PHP
实例:尽可能写友好的Javascript代码
2006/10/09 Javascript
node.js使用require()函数加载模块
2014/11/26 Javascript
javascript实现tab切换的两个实例
2015/11/05 Javascript
easyui window refresh 刷新两次的解决方法(推荐)
2016/05/18 Javascript
原生的强大DOM选择器querySelector介绍
2016/12/21 Javascript
nodejs 实现钉钉ISV接入的加密解密方法
2017/01/16 NodeJs
JavaScript学习总结之正则的元字符和一些简单的应用
2017/06/30 Javascript
微信小程序一周时间表功能实现
2019/10/17 Javascript
python机器学习理论与实战(一)K近邻法
2021/01/28 Python
python实现播放音频和录音功能示例代码
2018/12/30 Python
Python单元和文档测试实例详解
2019/04/11 Python
python控制nao机器人身体动作实例详解
2019/04/29 Python
OpenCV模板匹配matchTemplate的实现
2019/10/18 Python
基于python实现文件加密功能
2020/01/06 Python
Python使用Pygame绘制时钟
2020/11/29 Python
美国蔬菜和植物种子公司:Burpee
2017/02/01 全球购物
德国的各种媒体在线商店:Thalia.de(书籍、电子书、玩具等)
2020/10/08 全球购物
简述Linux文件系统通过i节点把文件的逻辑结构和物理结构转换的工作过程
2012/04/17 面试题
机械制造毕业生求职信
2014/03/03 职场文书
创先争优承诺书范文
2014/03/31 职场文书
教师对学生的寄语
2014/04/03 职场文书
励志演讲稿范文
2014/04/29 职场文书
2015年学习部工作总结范文
2015/03/31 职场文书
幼儿园开学温馨提示
2015/07/15 职场文书
解决Jupyter-notebook不弹出默认浏览器的问题
2021/03/30 Python
Nginx配置https原理及实现过程详解
2021/03/31 Servers
MySQL系列之十二 备份与恢复
2021/07/02 MySQL
Spring-cloud Config Server的3种配置方式
2021/09/25 Java/Android
MongoDB数据库之添删改查
2022/04/26 MongoDB