Vue中的作用域CSS和CSS模块的区别
现代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并将上面的示例转换为以下内容,它仅将我们的样式应用于相同的组件中的元素:
就像你看到的一样,整个过程不需要做什么就可以达到很好的效果: 作用域样式 (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中存在的痛点。最后希望这篇文章对大家有所帮助。
上一篇: 古代大炮,打的炮弹都是实心铁球吗?
推荐阅读
-
Vue中对比scoped css和css module的区别
-
CSS3中:nth-child和:nth-of-type的区别深入理解
-
CSS3中background-clip和background-origin的区别示例介绍
-
网页布局设计css中单位px和em,rem的区别
-
详解在vue-cli3.0中自定css、js和图片的打包路径
-
css中filter属性和backdrop-filter的应用与区别详解
-
css3媒体查询中device-width和width的区别详解
-
CSS中flex和inline-flex的区别详解
-
css中z-index: 0和z-index: auto的区别
-
Vue中的作用域CSS和CSS模块的区别