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编写的第一人称射击游戏
Feb 25 Javascript
js 获取服务器控件值的代码
Mar 05 Javascript
javascript中创建对象的几种方法总结
Nov 01 Javascript
Javascript基础教程之switch语句
Jan 18 Javascript
Jquery 实现table样式的设定
Jan 28 Javascript
jQuery循环遍历子节点并获取值的方法
Apr 14 Javascript
使用React实现轮播效果组件示例代码
Sep 05 Javascript
ES6新特性之字符串的扩展实例分析
Apr 01 Javascript
捕获未处理的Promise错误方法
Oct 13 Javascript
说说Vue.js中的functional函数化组件的使用
Feb 12 Javascript
vue项目使用.env文件配置全局环境变量的方法
Oct 24 Javascript
详解如何使用React Hooks请求数据并渲染
Oct 18 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实现加减法验证码代码
2014/02/14 PHP
PHP图片自动裁切应付不同尺寸的显示
2014/10/16 PHP
Ubuntu12下编译安装PHP5.3开发环境
2015/03/27 PHP
php根据日期显示所在星座的方法
2015/07/13 PHP
基于php判断客户端类型
2016/10/14 PHP
php5.5使用PHPMailer-5.2发送邮件的完整步骤
2018/10/14 PHP
简单的代码实现jquery定时器
2014/01/03 Javascript
jQuery结合CSS制作动态的下拉菜单
2015/10/27 Javascript
浅析Node.js实现HTTP文件下载
2016/08/05 Javascript
vue.js+boostrap项目实践(案例详解)
2016/09/21 Javascript
新闻上下滚动jquery 超简洁(必看篇)
2017/01/21 Javascript
webstrom Debug 调试vue项目的方法步骤
2018/07/17 Javascript
微信小程序实现留言功能
2018/10/31 Javascript
在React中写一个Animation组件为组件进入和离开加上动画/过度效果
2019/06/24 Javascript
python实现迭代法求方程组的根过程解析
2019/11/25 Javascript
js cavans实现静态滚动弹幕
2020/05/21 Javascript
JavaScript实现鼠标经过表格某行时此行变色
2020/11/20 Javascript
[01:54]TI珍贵瞬间系列(三):翻盘
2020/08/28 DOTA
Python中使用partial改变方法默认参数实例
2015/04/28 Python
Pycharm以root权限运行脚本的方法
2019/01/19 Python
Python 中Django验证码功能的实现代码
2019/06/20 Python
简单了解Django应用app及分布式路由
2019/07/24 Python
python求绝对值的三种方法小结
2019/12/04 Python
Django如何使用asyncio协程和ThreadPoolExecutor多线程
2020/10/12 Python
英国领先的大码时装品牌之一:Elvi
2018/08/26 全球购物
通往英国高街的商店橱窗:Down Your High Street
2020/07/19 全球购物
类如何去实现接口
2013/12/19 面试题
护理学毕业生自荐信
2013/10/02 职场文书
《鸟的天堂》教学反思
2014/02/27 职场文书
班风口号
2014/06/18 职场文书
2014年学生会部门工作总结
2014/11/07 职场文书
2014年法院个人工作总结
2014/12/17 职场文书
2015秋季幼儿园开学寄语
2015/03/25 职场文书
创业计划书之密室逃脱
2019/11/08 职场文书
利用python调用摄像头的实例分析
2021/06/07 Python
教你nginx跳转配置的四种方式
2022/07/07 Servers