Functional CSS: 從試著改進可重用CSS做起

泊學發表於2017-10-22

從試著改進可重用CSS做起

泊學4K視訊

回想起每次更新泊學網站,最讓我頭疼的,就是改寫CSS。在不同的階段,對CSS不斷深入的理解,對網站內容的調整,對UI的重用需求,都影響著CSS的編寫方式,因此,稍不留神,你的程式碼理解就會充斥著各種風格和各種作用的CSS,讓你什麼時候想起這些,都覺得心情不那麼愉快。

因此,就和大家分享一些心得,如何理解CSS,以及如何更有效的編寫CSS。

從基於語義的CSS說起

首先,我們從一個最簡單的例子開始,回想一下你的第一個CSS例子,一定和下面這樣是類似的,所謂CSS,表達的就是頁面DOM的樣式:

  <p class="text-center">
        Hello world!
    </p>

然後,在text-center裡,我們指定文字居中對齊的樣式:

.text-center {
    text-align: center;
}

semantic-css-is-bad-1@2x.png

很簡單對不對?隨著樣式越寫越多,我們很快就會開始關注到一些編寫CSS的建議。例如:應該把HTML和CSS的職責分開,HTML中不應該包含任何和具體樣式(例如居中對齊)有關的資訊,這些具體的樣式都應該放到CSS中處理。

於是,我們就開始嘗試著用樣式要表達的語意來替換掉它表達的具體樣式:

<p class="greeting">
    Hello world!
</p>

<style>
.greeting {
    text-align: center;
}
</style>

這樣看起來就好多了。無論.greeting指定的具體樣式是什麼,都不影響它在HTML中表示歡迎資訊樣式的含義。這樣,從理論上說,我們就可以用一套HTML模板,實現各種不同風格的UI了。

於是,我們就開始基於這種語義的方式,來編寫各種介面了。例如,我們新增一個表示視訊作者的資訊卡,它的HTML模板是這樣的:

<div class="container">
  <div class="creator-info">
    <img src="http://7xncmx.com1.z0.glb.clouddn.com/dora11.png" alt="">
    <div>
      <h2>Mars</h2>
      <p>
        The creator of boxue.io. Bla bla bla...
      </p>
    </div>
  </div>
</div>

同樣,在這個模板裡,creator-info是一個按語義命名的樣式,接下來,是這個樣式的實現:

.creator-info {
  background-color: white;
  border: 1px solid rgba(0,0,0,0.1);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  overflow: hidden;
  > img {
    display: block;
    width: 100%;
    height: auto;
  }
  > div {
    padding: 1rem;
    > h2 {
      font-size: 1.25rem;
      color: rgba(0,0,0,0.8);
    }
    > p {
      font-size: 1rem;
      color: rgba(0,0,0,0.75);
      line-height: 1.5;
    }
  }
}

它看上去的結果是這樣的:

semantic-css-is-bad-2@2x.png

這裡,我們的重點不是這些樣式的具體內容,而是這個CSS的結構,如果我們把所有具體的樣式都去掉,你就會發現,這個樣式嚴重依賴於HTML中DOM的層次結構:

.creator-info
  > img
  > div
    > h2
    > p

因此,儘管在HTML中,我們依靠基於語義的樣式剝離了CSS,但這種方式卻很容易在CSS中暴露過多和HTML相關的細節。因此,這樣的做法,實際上並沒有完全實現剝離CSS和HTML職責的目的,我們需要更好的做法。

把樣式從DOM結構中剝離出來

為了避免樣式依賴DOM結構的問題,我們的思路是:讓樣式的命名方式兼具格式和語義的功能。然後,在DOM裡,對不同位置的元素,使用對應的樣式。這裡,我們借鑑了BEM命名方法,對我們要使用的樣式名稱,統一使用這樣的命名格式:主體-依賴主體的內容__內容的屬性

<div class="container">
  <div class="creator-info">
    <img class="creator-info__image"
         src="http://7xncmx.com1.z0.glb.clouddn.com/dora11.png" alt="">
    <div class="creator-info__content">
      <h2 class="creator-info__name">Mars</h2>
      <p class="creator-info__description">
        The creator of boxue.io. Bla bla bla...
      </p>
    </div>
  </div>
</div>

這次,我們給DOM中,每一個需要樣式的元素繫結了有特定命名規則的樣式。這樣,在樣式表裡,所有的樣式就可以是扁平結構的了:

.creator-info {
    background-color: white;
    border: 1px solid rgba(0,0,0,0.1);
    border-radius: 4px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    overflow: hidden;
}

.creator-info__image {
    display: block;
    width: 100%;
    height: auto;
}

.creator-info__content {
    padding: 1rem;
}

.creator-info__name {
    font-size: 1.25rem;
    color: rgba(0,0,0,0.8);
}

.creator-info__description {
    font-size: 1rem;
    color: rgba(0,0,0,0.75);
    line-height: 1.5;
}

還記得當時自己把泊學網站樣式修改成這樣之後,著實興奮了一陣子,因為這樣的方式似乎徹底解決了HTML模板和CSS之間相互依賴的問題。

處理重複的介面佈局

但是沒過多久,我就發現了新的問題。當我編寫首頁上每個視訊系列的UI元件時,結構上,它和之前的作者資訊卡幾乎是一樣的。於是我幾乎不假思索的寫出了這樣的HTML模板:

<div class="container">
  <div class="series-info">
    <img class="series-info__image"
         src="https://dn-boxueio.qbox.me/YourFirstMLProject@2x-911fdc56906f05fc0757e5577084c840.jpg"
         alt="">
    <div class="series-info__content">
      <h2 class="series-info__name">Machine Learning from Scratch</h2>
      <p class="series-info__description">
        Let`s create a real-world machine learning demo from scratch.
      </p>
    </div>
  </div>
</div>

它同樣包含了一個封面圖,一個標題和一個簡介。只不過,我們把樣式名稱中的主體從creator換成了series。但是,當我要給這些新的樣式設定值的時候,就有點兒糾結了。該如何設定這些series-***的樣式呢?你可能想到了兩種選擇。

第一種,最直接的方法,就是把series-***按照creator-***複製一遍。這肯定可以工作,但是估計沒多少人會認同這種做法,因為它違反了Don`t Repeat Yourself的原則;

第二種,如果你使用了SCSS,就可以實現從某個樣式繼承這樣的用法:

.series-info {
    @extend .creator-info;
}

.series-info__image {
    @extend .creator-info__image;
}

.series-info__content {
    @extend .creator-info__content;
}

.series-info__name {
    @extend .creator-info__name;
}

.series-info__description {
    @extend .creator-info__description;
}

但這樣做也有它自己的問題,@extend應該只在彼此有關聯的樣式之間使用,而不僅僅是為了避免重複編寫相同的樣式。並且,如果稍後我們還要視訊資訊卡呢?真的需要這些使用了相同樣式的selector麼?顯然,目前的這種解決方案仍舊不夠理想。

去除掉過於細緻的語義

實際上,造成樣式難以重用的原因,是因為selector表達的語義過於細緻了。語義越細緻,重用就越困難。因此,我們只要把這種繫結類似介面佈局UI的selector,起個名字替代掉類似creatorseries這樣的名字就好了:

.media-card {
  background-color: white;
  border: 1px solid rgba(0,0,0,0.1);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  overflow: hidden;
}

.media-card__image {
    display: block;
    width: 100%;
    height: auto;
}

.media-card__content {
    padding: 1rem;
}

.media-card__name {
      font-size: 1.25rem;
      color: rgba(0,0,0,0.8);
}

.media-card__description {
      font-size: 1rem;
      color: rgba(0,0,0,0.75);
      line-height: 1.5;
}

這樣,無論是作者資訊還是視訊系列資訊,就都可以用同一套樣式來表示了:

<div class="container">
  <div class="media-card">
    <img class="media-card__image"
         src="https://dn-boxueio.qbox.me/YourFirstMLProject@2x-911fdc56906f05fc0757e5577084c840.jpg" alt="">
    <div class="media-card__content">
      <h2 class="media-card__name">Machine Learning from Scratch</h2>
      <p class="media-card__description">
        Let`s create a real-world machine learning demo from scratch.
      </p>
    </div>
  </div>
</div>

甚至,只要UI佈局和media-card描述的體系相同,這套樣式就可以直接重用。

但是情到此結束了麼?顯然沒有,現在,你可能又會想了:假設我們需要修改作者資訊卡的樣式,但仍儲存視訊系列資訊卡的樣式該怎麼辦呢?

如果像之前一樣,它們的樣式是獨立的,只修改對應的樣式就好了。現在,它們共享樣式了,我不僅要建立新的樣式,還要連同對應的HTML一起修改,這樣做真的好麼?

實際上根本沒有絕對的職責分離

為了回答這個問題,我們得回到這一節開始提出的目的:分離HTML和CSS的職責。面對這個話題,我們直覺上就會認為,只有徹底剝離了才算完成達成目標。但實際的情況則是,它們兩者根本無法做到完全分離。我們只能根據自己專案的實際情況,選擇一種適合自己的方式。

對於哪些具備詳細語義(.creator-info.series-info)的樣式而言,此時,HTML是獨立的,它完全不關心這些DOM會長成什麼樣子。它只暴露了一個介面,允許我們定製其中的樣式。因此,這種選擇下的CSS不是獨立的,它依賴於樣式繫結的HTML,需要以HTML為參考,定義樣式的內容。

對於那些具備中立語義(.media-card)的樣式而言,此時,CSS是獨立的,它完全不關心自己會被用在什麼元素上。此時,HTML就不是獨立的了,它需要知道樣式表提供了哪些內容,並基於這些內容,來編排DOM。

實際上,這兩種方法,沒有絕對的誰優誰劣的問題。只是你要想清楚,哪種方式更適合自己的專案。

What`s next?

看到這裡,如果你和我之前有過類似的困惑,現在,你應該躍躍欲試地要調整下自己的CSS了。先彆著急,在下一節裡,我們將繼續討論,如何通過合理的命名,最大化實現樣式的可重用目標。

相關文章