[譯] Vue: scoped 樣式與 CSS Module 對比

西樓聽雨發表於2018-09-09

原文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 獲取到我們定義的一些變數(如色彩值、樣式斷點),這樣我們就無需手動保持其在多個檔案中同步。

相關文章