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

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

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

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

使用Vue-cli构建的Vue应用程序提供了两个很棒的内置解决方案: 作用域CSSCSS 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并将上面的示例转换为以下内容,它仅将我们的样式应用于相同的组件中的元素:

就像你看到的一样,整个过程不需要做什么就可以达到很好的效果: 作用域样式 (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>

转换后就像下面这样:

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

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

<!-- 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>

编译出来的效果如下:

还有一些情况是我们需要对子组件的深层次结构设置样式。虽然这种做法并不受推荐,而且应该尽量去避免。比如下面这个示例, 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>

编译出来的结果如下:

从上面的结果可以看出来,在父组件 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 ,但上面不同的是,这次样式生效。编译出来的结果如下:

另外使用作用域样式还存在一个问题。那就是对 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>

编译出来的结果如下:

从上图可以看出来, 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 样式生效了,如下图所示:

话又说回来,虽然 >>> 或 /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>

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

正如上图所示, :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变量名。此时在编译的时候,会报一个错话信息:

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

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

编译出来的结果如下:

除了$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 编译出来对应的类名:

利用这样的特性,在 <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>

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

如上图所示,当 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>

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

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

<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中存在的痛点。最后希望这篇文章对大家有所帮助。

(0)

相关推荐

  • 如何理解Vue的作用域插槽的实现原理

    本文介绍了如何理解Vue的作用域插槽的实现原理,分享给大家,也给自己留个笔记 举个例子,比如我写了一个可以实现条纹相间的列表组件,发布后,使用者可以自定义每一行的内容或样式(普通的slot就可以完成这个工作).而作用域插槽的关键之处就在于,父组件能接收来自子组件的slot传递过来的参数,具体看案例和注释. <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"

  • 详解Vue.js分发之作用域槽

    首先,需要知道的是,每个组件有每个组件自己的作用域.每个组件都是Vue()的实例,有自己的作用域. 比如现在有个组件这样的: Vue.component('father-component1',{ template:'<div class="father"><p>Hi,我是父组件</p>{{fatherMessage}}</div>', data:function(){ return { fatherMessage:'这是父组件发出的消息

  • Vue作用域插槽slot-scope实例代码

    vue中的插槽有三种:单个插槽.具名插槽.作用域插槽,这个在官网上能看到 (https://cn.vuejs.org/v2/guide/components.html#单个插槽) 作用域插槽简单来说就是父组件只管显示样式,数据由子组件来提供.比如上面的代码,el-table-column内的template是传递给其内部solt的显示内容,但是scope的值是由el-table-column内部solt绑定的数据返回的.这个数据具体是什么由el-table-column中指定,类似: <slot

  • Vuejs第九篇之组件作用域及props数据传递实例详解

    本篇资料来于官方文档: http://cn.vuejs.org/guide/components.html#Props 本教程是小编结合官方文档整理的一套更加细致,代码更多更全的教程,特别适合新手阅读. props数据传递 ①组件实例的作用域: 是孤立的,简单的来说,组件和组件之间,即使有同名属性,值也不共享. <div id="app"> <add></add> <del></del> </div> <sc

  • Vue中watch、computed、updated三者的区别及用法

    1.watch 理解: 监听器,监听某个数据的变化从而来执行一些操作,当data里面的数据发生变化的时候来执行一下开销较大或异步的操作 1.监听值类型(简单类型)数据 //在一个vue实例中 new Vue({ el:"#myApp", data:{ num1:1, num2:2 }, methods:{}, watch:{ //这里两个属性,第一个值是变化后最新的值,第二个是变化前 num1(after,before){ this.num2 = after +1 } immediat

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

    现代Web开发中的CSS离完美还差得远,这并不奇怪.现在,项目通常是相当的复杂的,而CSS样式又是全局性的,所以到最后总是极容易地发生样式冲突: 样式相互覆盖 或 隐式地级联到我们未考虑到的元素 . 为了减轻CSS存在的主要痛点,我们在项目中普遍采用 BEM 的方法来.不过这只能解决CSS问题中的一小部分. 对我们来说是幸运的,社区已经开发出了可以帮助我们更彻底地解决问题的解决方案.你可能已经听说过 CSS Modules. Styled Componetns. Glamorous或 JSS.这

  • 在vue中如何引入外部的css文件

    目录 vue中引入外部css文件 1. 全局引入 2. 局部引入相对路径 3. 局部引入绝对路径 css-loader导致vue中样式失效的坑 问题描述 环境 原因 方案 vue中引入外部css文件 在项目的src文件下,新建一个style文件夹,存放css文件. 1. 全局引入 将外部的css文件放到style文件下,引入外部文件只需在main.js文件中 import '@/style/reset.css' 我引入的是清除默认样式的css文件 2. 局部引入相对路径 <style scope

  • Vue中的methods、watch、computed的区别

    看到这个标题就知道这篇文章接下来要讲的内容,我们在使用vue的时候methods.watch.computed这三个特性一定经常使用,因为它们是非常的有用,但是没有彻底的理解它们的区别和各自的使用场景,也很难用好它们,希望接下来的介绍为你答疑解惑. computed 我们先来看计算属性:computed,光看名字也知道是用来干什么的,计算属性当然是用来计算的,但是是怎么计算的呢?计算属性有两个显著的特点: 计算属性计算时所依赖的属性一定是响应式依赖,否则计算属性不会执行 计算属性是基于依赖进行缓

  • Vue中的v-model,v-bind,v-on的区别解析

    目录 v-model v-bind(缩写为 : ) v-on(缩写为@) 总结 v-model v-model是进行动态双向绑定的(只能用在input, textarea, select上),以input为例,进行绑定后,vue对象中data的相应值会与input的输入同步变动. // HTML <input type="text" v-model="message"> new Vue({ data { message: ' ', //最开始messag

  • vue中v-model指令与.sync修饰符的区别详解

    目录 v-model .sync 细微之处的区别 总结功能作用场景: v-model <!--父组件--> <template> <!--v-model 指令是语法糖--> <Child v-model="model"></Child> <!-- 把 v-model 指令展开后相当于下面的代码 --> <!-- v-model绑定的默认事件是input,默认prop是value属性 --> <Ch

  • 在vue中使用css modules替代scroped的方法

    前面的话 css modules是一种流行的模块化和组合CSS的系统. vue-loader提供了与css modules的集成,作为scope CSS的替代方案.本文将详细介绍css modules 引入 最开始使用Vue的时候,提倡并大量使用的是scoped这种技术 <style scoped> @media (min-width: 250px) { .list-container:hover { background: orange; } } </style> 这个可选 sc

  • Vue 中使用 CSS Modules优雅方法

    CSS Modules:局部作用域 & 模块化 CSS Modules 为每一个局部类赋予全局唯一的类名,这样组件样式间就不会相互影响了.如: /* button.css */ .button { font-size: 16px; } .mini { font-size: 12px; } 它会被转换为类似这样: /* button.css */ .button__button--d8fj3 { font-size: 16px; } .button__mini--f90jc { font-size

  • 基于vue中css预加载使用sass的配置方式详解

    1.安装sass的依赖包 npm install --save-dev sass-loader //sass-loader依赖于node-sass npm install --save-dev node-sass 2.在build文件夹下的webpack.base.conf.js的rules里面添加配置,如下红色部分 { test: /\.sass$/, loaders: ['style', 'css', 'sass'] } <span style="color:#454545;"

  • vue中element-ui组件默认css样式修改的四种方式

    目录 前言 1.使用全局统一覆盖 2.在.vue文件中修改 3.修改组件的style样式 4. 参考element-ui官方文档的api 疑问 总结 前言 修改element-ui组件的默认样式一直是一个老生长谈的话题,在做完公司的一整个项目后,我总结了以下4种修改element-ui默认样式的方法. 1.使用全局统一覆盖 针对一些通用的.样式固定的组件,可以全局处理,其方法是新建一个css或者scss文件,覆盖element原有的class 你可以在src/styles目录下新建一个eleme

随机推荐