從搭建工程講到CSS Modules

網易考拉前端團隊發表於2019-03-01

背景

這周的主要的工作就是搭建新工程的架子,專案基於vue-cli構建。基本的功能在腳手架裡都已經具備,但是還是需要針對具體的業務場景來做一些定製。

mock

以現在前後端分離的開發模式來講,一個正常的開發流程大概是這樣:
從搭建工程講到CSS Modules

這樣前端可以不用等後端介面完全寫完才可以開發,前後端並行開發,提高團隊效率。

而腳手架提供的功能只有一個proxy,不能滿足我們的需求。所以需要我們自己寫一個mock-middleware。大概是這樣的一個功能,就不貼mock-middleware的程式碼了:

module.exports = () => {
    const argv = Array.prototype.slice.call(process.argv, 2);
    const proxyAddress = argv[0];
    if (!argv.length || proxyAddress === `mock`) {
        return mockMiddleware;
    } else {
        return proxyMiddleware(proxyAddress);
    }
}複製程式碼

最終的效果就是執行 npm run dev mock ,會將api請求(注意:這裡只會代理api請求,而不代理靜態資源)轉發到mock資料。在前後端開發完成之後,可以執行npm run dev {proxy_ip}將api請求代理到某臺開發伺服器進行聯調。

當然這篇文章的重點不是這個,而是想總結下在SPA應用裡如何規範的寫CSS。

CSS模組化

故事的起點從元件庫的選擇說起,專案選擇了開源的element 作為元件庫。而這套元件庫並不符合考拉前端組的視覺規範,而目前也沒有計劃fork一條分支來維護,於是就採取了比較簡單粗暴的做法,樣式覆蓋。先來看一段element中的樣式:

@component-namespace el {
  @b progress {
    position: relative;
    line-height: 1;

    @e text {
      font-size:14px;
      display: inline-block;
      vertical-align: middle;
      margin-left: 10px;
      line-height: 1;
    }
    @m circle {
      display: inline-block;
    }
  }
}複製程式碼

可能有同學在剛看到這段樣式的時候會有點蒙,但是仔細看@b,@e,@m,不就是bem規範麼。
從搭建工程講到CSS Modules

BEM規範

關於BEM規範,網上有很多文章介紹。知乎上也有一篇文章來討論其優劣,這裡不去討論BEM規範的好處和壞處,我覺得只要在一個工程里約定好一種規範並嚴格執行,這樣總是不會錯的。

一方面為了和元件庫的CSS規範保持統一,另一方面我個人覺得BEM的優點還是大於缺點,因此在專案裡也準備按照這個規範來寫。

那麼問題來了,瀏覽器是不認識上面這些@b,@e,@m的語法的,這個時候就需要postcss來幫忙了。

postcss

postcss和gulp,webpack等工具一樣,他本身並不是一種前處理器或者後處理器,而是通過各種外掛來完成轉換(比如很流行的Autoprefixer就是它的一個外掛)。

postcss-salad可以認為是一個postcss外掛的集合,支援最新的css語法,一些sass巢狀的語法以及bem轉化。這時候先來一份配置檔案postcss.config.js

module.exports = {
  plugins: [
    require(`postcss-salad`)({
      browsers: [`ie > 9`, `last 2 versions`],
      features: {
        bem: {
          shortcuts: {
            component: `b`,
            descendent: `e`,
            modifier: `m`
          },
          separators: {
            descendent: `__`,
            modifier: `--`
          }
        }
      }
    })
  ]
}複製程式碼

bem之間的連線符可以自定義,這邊是為了和element的元件保持一致。這時候我們可以先通過postcss-cli這個工具來看一下效果。我們就用上述那段css來用postcss+postcss-salad進行轉換,得到結果如下:

.el-progress {
    position: relative;
    line-height: 1
}

.el-progress__text {
    font-size: 14px;
    display: inline-block;
    vertical-align: middle;
    margin-left: 10px;
    line-height: 1
}

.el-progress--circle {
    display: inline-block
}複製程式碼

已經達到了我們預想的效果,那麼接下來要想在webpack中使用postcss肯定會需要一個loader,也就是postcss-loader

然後將剛才的postcss.config.js作為postcss-loader的配置檔案匯入。

{
  loader: `postcss-loader`,
  options: {
    config: {
      path: `path/to/postcss.config.js`
    }
  }
}複製程式碼

至此,就可以在webpack專案中,用這種巢狀的bem語法來進行css的書寫了。

CSS Modules

故事還沒有結束,大家都知道在寫單頁面應用的時候,一個常見的需求是希望元件間的css作用域是互相隔離的。這時候第一反應想到就是Scoped CSS,vue-loader也是支援Scoped CSS的(實際上對CSS進行轉換的還是postcss)。那麼Scoped CSS是如何來處理這個問題的呢:

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

=>

<style>
.example[_v-f3f3eg9] {
  color: red;
}
</style>複製程式碼

可以看到Scoped CSS會在class後面加上一段hash,從而來實現CSS作用域的隔離,但是在實踐過程中,會發現幾個問題:

  1. Scoped CSS將同時作用在父元件和子元件上,也就是無法做到父子元件樣式的隔離。
  2. 由於Scoped CSS改變了DOM中的class,因此就無法在元件內去覆蓋element元件的全域性樣式了。

這時候就需要CSS Modules出場了,CSS Modules是目前CSS模組化方案中被接受度較高的一種方案,網上也有很多的文章去介紹它。同樣的,我們需要先看下CSS Modules能做什麼事情呢?

剛才講到,postcss有很多的外掛,那麼肯定也會有CSS Modules的外掛了。我們同樣通過之前的postcss-cli來進行測試,首先配置上這個外掛:

require(`postcss-modules`)({
  generateScopedName: `[local]--[hash:base64:5]`
})複製程式碼

這裡的[local]代表類名,[hash:base64:5]按照給定規則生成的hash值,還可以用的變數有[name]代表標籤名,[path]代表路徑等
CSS Modules 生成的class可以自定義規則,因此也可以用自定義的規則,而不用bem規範,可以看專案的具體情況來定

還是同樣那段css程式碼,看下轉換的結果:

.el-progress--3tuDF {
    position: relative;
    line-height: 1
}

.el-progress__text--1W8n3 {
    font-size: 14px;
    display: inline-block;
    vertical-align: middle;
    margin-left: 10px;
    line-height: 1
}

.el-progress--circle--3OD0E {
    display: inline-block
}複製程式碼

跟我們預設的樣式規則一致,這樣就可以解決上述的第一個問題,做到每個元件內的樣式是唯一的。

但是我們發現CSS Modules同樣會改變class,那麼也同樣無法在元件內覆蓋element元件的全域性樣式,這時候可以用CSS Modules的全域性樣式寫法,:global{.class}

:global(.el-progress) {
  position: relative;
  line-height: 1;
}

=>

.el-progress {
  position: relative;
  line-height: 1;
}複製程式碼

這樣就不會在class上加上hash字尾,從而可以達到覆蓋全域性樣式的目的。

CSS Modules 在Vue+webpack專案中的實踐

首先在Vue-loader的配置檔案中配置生成class規則:

cssModules: {
    localIdentName: `[local]--[hash:base64:5]`,
    camelCase: true
}複製程式碼

然後在元件內通過在style上新增module開啟CSS Modules。

<style module>
</style>複製程式碼

css-loader 會將一個 $style物件注入到當前元件。所以在實際中使用大概是這個樣子:

<header :class="$style[`titan-header`]">
</header>複製程式碼

有時候我們會有這樣的用法:

<div :class="{ `active`: selectedIndex == index} ">
</div>複製程式碼

這時候樣式就會作為物件的屬性名,而我們知道用了CSS Modules,就必須用$style.active來替換`active`,還好我們有ES6!
ES6有一個特性是用雙括號支援用計算屬性作為屬性名,也就是這樣:


<div :class="{ [$style.active]: selectedIndex == index} ">
</div>複製程式碼

總結

bem+CSS Modules在新專案的實踐已經有一週的時間,並沒有發現什麼問題,才寫下這篇文章來總結。以上只是我在本次工程搭建過程中的一些總結,並不保證觀點完全正確,供大家參考。

相關文章