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 相关文章推荐
PJ Blog修改-禁止复制的代码和方法
Oct 25 Javascript
教你如何使用node.js制作代理服务器
Nov 26 Javascript
JavaScript静态类型检查工具FLOW简介
Jan 06 Javascript
js判断一个字符串是否包含一个子串的方法
Jan 26 Javascript
在JavaScript的AngularJS库中进行单元测试的方法
Jun 23 Javascript
基于bootstrap3和jquery的分页插件
Jul 31 Javascript
AngularJS 整理一些优化的小技巧
Aug 18 Javascript
基于Vue 2.0的模块化前端 UI 组件库小结
Dec 21 Javascript
在小程序中使用canvas的方法示例
Sep 17 Javascript
javascript实现考勤日历功能
Nov 29 Javascript
JavaScript事件冒泡机制原理实例解析
Jan 14 Javascript
解决vue项目中某一页面不想引用公共组件app.vue的问题
Aug 14 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
使用 MySQL Date/Time 类型
2008/03/26 PHP
php生成二维码的几种方式整理及使用实例
2013/06/03 PHP
解析关于wamp启动是80端口被占用的问题
2013/06/21 PHP
PHP中4个加速、缓存扩展的区别和选用建议
2014/03/12 PHP
PHP实现绘制二叉树图形显示功能详解【包括二叉搜索树、平衡树及红黑树】
2017/11/16 PHP
Javascript 复制数组实现代码
2009/11/26 Javascript
JavaScript的document对象和window对象详解
2010/12/30 Javascript
Javascript基础教程之数据类型 (布尔型 Boolean)
2015/01/18 Javascript
javascript解析xml实现省市县三级联动的方法
2015/07/25 Javascript
10个很棒的jQuery代码片段
2015/09/24 Javascript
JavaScript 正则表达式中global模式的特性
2016/02/25 Javascript
js实现上传图片预览方法
2016/10/25 Javascript
JS重载实现方法分析
2016/12/16 Javascript
浅谈vue的踩坑路
2017/08/31 Javascript
浅谈JavaScript find 方法不支持IE的问题
2017/09/28 Javascript
vue结合element-ui使用示例
2019/01/24 Javascript
微信小程序生成二维码的示例代码
2019/03/29 Javascript
vue项目中引入Sass实例方法
2019/08/27 Javascript
jQuery实现日历效果
2020/09/11 jQuery
OpenLayer3自定义测量控件MeasureTool
2020/09/28 Javascript
[05:45]Ti4观战指南(下)
2014/07/07 DOTA
[34:27]DOTA2上海特级锦标赛B组败者赛 VG VS Spirit第一局
2016/02/26 DOTA
selenium WebDriverWait类等待机制的实现
2020/03/18 Python
django haystack实现全文检索的示例代码
2020/06/24 Python
详解用Python调用百度地图正/逆地理编码API
2020/07/02 Python
安装pyinstaller遇到的各种问题(小结)
2020/11/20 Python
Python 爬虫批量爬取网页图片保存到本地的实现代码
2020/12/24 Python
用html5实现语音搜索框的方法
2014/03/18 HTML / CSS
英国最大的宝石首饰超市:QP Jewellers
2018/09/23 全球购物
大学本科毕业生求职信范文
2013/12/18 职场文书
老干部工作汇报材料
2014/10/28 职场文书
2014年信息中心工作总结
2014/12/17 职场文书
幼儿园班级工作总结2015
2015/05/25 职场文书
学习社交礼仪心得体会
2016/01/22 职场文书
安装配置mysql及Navicat prenium的详细流程
2021/06/10 MySQL
Pytest中skip和skipif的具体使用方法
2021/06/30 Python