[译] Vue: scoped 样式与 CSS Module 对比

10,003 阅读9分钟

原文Vue.js - Scoped Styles vs CSS Modules
作者Michał Sajnóg 发表时间:Aug 28, 2018
译者:西楼听雨 发表时间: 2018/9/10
(转载请注明出处)

译者按:本文讲解的主要是 Scoped 样式和 CSS Module 的对比,对这两个概念已经熟悉的同学,同样也建议看一下,因为文中还提到了一些如 CSS Modules 的 “:export”概念,及 Scoped 样式存在一些缺陷,如果你对这些细节也已经熟知,那么请尽快离开这个页面,以免浪费您时间。

展开原文 It should not come as a surprise that CSS in modern web development is far from perfect. Nowadays, projects are usually quite complex and, given the global nature of styles, it’s extremely easy to end up with conflicting styles that override each other or that implicitly cascade down to elements we didn’t consider before.

The most commonly used solution that we use to reduce the main pain points is introducing the BEM (Block Element Modifier) methodology. However, it addresses only a small part of the bigger problem.

在现代化的 Web 开发中,CSS 还远未完美,这一点应该没有什么意外。现今的项目通常都相当复杂,而 css 样式天生又是全局性的,所以到最后总是极容易地就发生样式冲突——要么是样式相互覆盖,要么就是隐式地级联到了下面那些我们未考虑到的元素。

在减轻 CSS 存在的主要痛点方面,我们普遍采用的解决方案是引入 BEM (Block Element Modifier) 方法学。不过这只能解决我们这个大问题的很小一部分。

展开原文 Fortunately for us, the community already developed solutions that can help us deal with the problem more thoroughly. You might’ve already heard about [**CSS Modules**](https://github.com/css-modules/css-modules), **Styled Components**, **Glamorous** or[ **JSS**](http://cssinjs.org/) - these are just a few of the most popular tools that we can add to our projects today. If you're interested in the topic, you can check [this post](https://hackernoon.com/all-you-need-to-know-about-css-in-js-984a72d48ebc) - [Indrek Lasn](https://twitter.com/lasnindrek) explains the whole CSS-in-JS idea there very thoroughly.

Every new Vue.js application created by vue-cli comes with two great built-in solutions: Scoped CSS and CSS Modules. Both of them have some pros and cons, so let’s take a closer look and see which solution might be a better fit for your case.

我们非常幸运,社区已经开发出了一些解决方案,他们可以帮我们处理这些问题。说不定你已经听说过了 CSS ModulesStyled ComponentsGlamorousJSS——这些只是众多流行的工具中的少数几个。如果你对这个话题感兴趣,你可以查看这篇帖文——作者 Indrek Lasn 对 CSS-in-JS 的思想做了非常详尽的讲解。

每个通过 vue-cli 创建的 Vue.js 应用都内置了两个很好的解决方案:Scoped CSSCSS Modules (模块式 CSS)。两种方案各有优缺点,所以下面我们就仔细看下哪种方案在你的案例中更适用。

Scoped 样式

In order to get scoped styles working, we just have to add a scoped attribute to the **<style>** tag:

我们只需要在 <style> 标签上添加一个 scoped 属性即可启用 scoped 样式:

<template>
  <button class=”button” />
</template>

<style scoped>
  .button {
    color: red;
  }
</style>

It will apply our styles only to elements in the same component by using PostCSS and transforming the above example to the following:

这样就会使得我们的样式只被应用到这个组件中的元素上。这是借助 PostCSS 实现的,它会将上面的代码转换成下面这样:

<style>
.button[data-v-f61kqi1] {
  color: red;
}
</style>

<button class=”button” data-v-f61kqi1></button>

As you can see, it requires no effort at all to have nicely scoped styles, and it also handles scoping tags’ styles in the same way.

就像你看到的这样,整个过程不需要做什么就可以达到很好的 scoped 样式效果。

Now, if you need to - let’s say - change the width of a component in a specific view, you can apply an extra class to it and style it as you normally would with all the benefits of scoped styles:

现在假设你需要调整一个视图中的某个组件的宽度,那么你可以像你平时那样做的一样:在这个组件上添加一个额外的 class 来设置其样式。

<template>
  <BasePanel class=”pricing-panel”>
    content
  </BasePanel>
</template>

<style scoped>
  .pricing-panel {
    width: 300px;
    margin-bottom: 30px;
  }
</style>

经转换后:

<style>
  .base-panel[data-v-d17eko1] {
    ...
  }
  .pricing-panel[data-v-b52c41] {
    width: 300px;
    margin-bottom: 30px;
  }
</style>

<div class=”base-panel pricing-paneldata-v-d17eko1 data-v-b52c41>
  content
</div>
展开原文 Once again - with no extra effort you’ve got full control over the layout.

However, be aware that this feature was introduced with one drawback - if your child component's root element has a class that also exists in the parent component, the parent component's styles will leak to the child. You can check out this CodeSandbox to get a better understanding of the problem.

Although it is not recommended and should be avoided - there are cases where we need to style something deeply inside our child component. For the sake of simplicity, let’s assume that our parent component should be responsible for the styling header of the BasePanel component. In scoped styles, the >>> combinator (also known as /deep/) comes in at this point.

这次还是一样,不需要做什么你就获得了对布局的彻底控制。

不过请注意:这个特性存在一个缺陷,即如果你子组件的根元素上有一个类已经在这个父组件中定义过了,那么这个父组件的样式就会泄露到子组件中。如果想更好地理解这个问题,可以查看这个 CodeSandbox 例子。

还有一些情况是我们需要对我们的子组件的深层结构设置样式——虽然这种做法并不受推荐且应该避免。为了简便起见,我们假设我们的父组件现在要对 BasePanel 的标题设置样式,在 scoped 样式中,这种情况可以使用 >>> 连接符(或者 /deep/ )实现。

<style scoped>
  .pricing-panel >>> .title {
    font-size: 24px;
  }
</style>

经转换后:

.pricing-panel[data-v-b52c41] .title {
  font-size: 24px;
}

Plain and simple, huh? But be aware that we just lost the encapsulation. Any .title class that will be used inside this component (even implicitly by a grandchild) will be affected by these styles.

非常简单,是吧?可是别忘记,我们却因此失去了组件的封装效果。这个组件内的所有的 .title 类的样式都会被这些样式所浸染——即便是孙节点。

模块式 CSS

CSS Modules gained their popularity due to the React community that quickly adopted this technology. Vue.js takes it to another level by combining its power with simplicity of use and out-of-the-box support by using vue-cli.

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

Now let’s look at how we can use it:

现在让我们来看下怎么使用它:

<style module>
  .button {
    color: red
  }
</style>

What makes it so special and different from scoped styles is that all the created classes are accessible via the $style object inside the component. So in order to apply this class we have to use class binding:

这次我们使用的不是 scoped 属性,而是 module。这等于告诉 vue-template-compiler 和 vue-cli 的 webpack 配置要对这一部分采用哪些相应的 loader,进而生成像下面这样的 CSS:

.ComponentName__button__2Kxy {
  color: red;
}

What makes it so special and different from scoped styles is that all the created classes are accessible via the $style object inside the component. So in order to apply this class we have to use class binding:

它的特殊之处以及和 scoped 样式不一样的地方就在于所有创建的类可以通过这个组件的 $style 对象获取。因此,要将这个类进行应用,我们需要像下面这样进行 class 绑定:

<template>
  <button :class="$style.button" />
</template>

<style module>
  .button {
    color: red
  }
</style>

这段代码将生成下面的 HTML 及相关的样式:

<style>
  .ComponentName__button__2Kxy {
    color: red;
  }
</style>

<button class=”ComponentName__button__2Kxy”></button> 
展开原文

The first benefit is that by looking at this element in our HTML we immediately know which component it belongs to. Secondly, everything becomes very explicit and we have full control - no magic whatsoever. However, we have to be careful while styling HTML tags, as they land in the final CSS as-is, as opposed to scoped styles, where even plain tags are scoped by the unique data attribute.

Similar to the second example from scoped styles, let’s see how we can style the component in a certain context:

它的第一点好处就是,当我们在 HMTL 中查看这个元素时我们可以立刻知道它所属的是哪个组件;第二点好处是,一切都变成显式的了,我们拥有了彻底的控制权——不会再有什么奇怪的现象了。和 scoped 样式那种把普通的标签也加上那些 data 属性的做法不一样,这些普通标签在转换后还是最初的样子。

比较 scoped 样式中的第二个例子,我们来看下我们可以怎么对那个组件设置样式:

<template>
  <BasePanel :class="$style['pricing-panel']">
    content
  </BasePanel>
</template>

<style module>
  .pricing-panel {
    width: 300px;
    margin-bottom: 30px;
  }
</style>

其转换后:

<style>
  .BasePanel__d17eko1 {
    /* some styles */
  }
  .ComponentName__pricing-panel__a81Kj {
    width: 300px;
    margin-bottom: 30px;
  }
</style>

<div class="BasePanel__d17eko1 ComponentName__pricing-panel__a81Kj">
  content
</div>

It simply gets the job done, without any surprises! Moreover, because all classes are available through the $style object we can now pass them however deep we want using props, making it super easy to use a class in any place of the child component:

毫无意外,跟我们期望的结果一样。此外,因为所有的 CSS 类可以通过 $style 对象获取到,所以我们可以通过 props 将这些类传递到任何我们希望的深度中,这样,在子组件中的任意位置使用这些类就会变得极其容易:

<template>
  <BasePanel
    title="Lorem ipsum"
    :titleClass="$style.title"
  >
    Content
  </BasePanel>
</template>

CSS Modules have great interoperability with JS, and they do not limit you to classes. Using :export keyword, we can also export additional things to our $style object.

模块式 CSS 与 JS 有着很好的互操作性 (interoperability),这一点不只局限于 CSS 类。我们还可以使用 :export 关键字将其他的东西导出到 $style 对象上。

Imagine you have a chart to develop - you can keep your colour variables in CSS, and additionally export them for use in your component:

例如,想象一下你有一个图表需要开发 —— 你可以在 CSS 中定义你的色彩变量的同时将其导出,以供你的组件使用:

<template>
  <div>{{ $style.primaryColor }}</div> <!-- #B4DC47 -->
</template>

<style module lang="scss">
  $primary-color: #B4DC47;
  
  :export {
    primaryColor: $primary-color
  }
</style>

I only scratched the surface here - the CSS Modules concept is much broader and I encourage you to check out the full specification to know more.

对于模块式 CSS的概念,我这里还只是讲到了它的皮毛,它实际要宽泛的多,建议你查看下它完整的规范以了解更多。

总结

展开原文

Both solutions are very simple, easy to use and, to an extent, solve the same issue. Which one should you choose then?

Scoped styles require literally no extra knowledge to use and feel comfortable with. Their limitations also make them simple to use, and they're capable of supporting small to mid-sized applications.

However, when it comes to more complex scenarios and bigger apps, we probably want to be more explicit and have more control over what’s going on in our CSS. Even though using the $style object multiple times in a template might not look so sexy, it’s a small price to pay for the safety and flexibility it allows. We also get easy access to our variables (like colours or breakpoints) in JS, without having to keep separate files in sync.

Which one do you use? And why? Feel free to share any additional scenarios you encountered along the way!

其实两种方案都非常简单、易用,在某种程度上解决的是同样的问题。 那么你该选择哪种呢?

scoped 样式的使用不需要额外的知识,给人舒适的感觉。它所存在的局限,也正是它的使用简单的原因。它可以用于支持小型到中型的应用。

在更大的应用或更复杂的场景中,这个时候,对于 CSS 的运用,我们就会希望它更加显式,拥有更多的控制权。虽然在模板中大量使用 $style 看起来并不那么“性感”,但却更加安全和灵活,为此我们只需付出微小的代价。还有一个好处就是我们可以用 JS 获取到我们定义的一些变量(如色彩值、样式断点),这样我们就无需手动保持其在多个文件中同步。