一種比css_scoped和css_module更優雅的避免css命名衝突小妙招

2022-09-01 12:04:36

css_scoped 與 css_module

我們知道,簡單的class名稱容易造成css命名重複,比如你定義一個class:

<style>
.main { float: left; }
</style>

如果別人剛好也定義了一個className:.main,你的float:left就會影響到它。

所以Vue中發明了css_scoped,其原理就是在class名稱後加上一個data屬性選擇器:

<style scoped>
.main { float: left; } 
</style>

//跳脫後變成
<style>
.main[data-v-49729759] { float: left }
</style>

css_scoped是Vue的專用方案,如果你使用React等其它UI框架,那麼你可以使用更通用的css_module,其原理是為樣式名加hash字串字尾,從而保證class名全域性唯一:

<style module>
.main { float: left; } 
</style>

//跳脫後變成
<style>
.main_3FI3s6uz { float: left; } 
</style>

相比於css_scopedcss_module方案更通用,不改變其本身的權重,而且渲染效能要比前者好很多,所以更推薦大家使用css_module

不足之處

然而不管是css_scoped還是css_module,都繞不開2大缺點:

  1. 由於加上了隨機字元,所以如果想在父元件中覆蓋子元件中的樣式變得麻煩,雖然css_scoped可以使用穿透,但這樣容易引發別的問題。
  2. 加上隨機字元讓class名稱變得不優雅,也影響編譯速度。

css名稱空間

我們來回憶一下,在css_scopedcss_module出現之前,人們是如何避免css命名衝突的?

對,就是人為的定義一些css名稱空間

那個時候,對每個Component元件都會在其根節點上定義一個不重複的ID或者class作為其名稱空間,然後其內部的其它class都會以此名稱空間作為前置限定,比如:

<div class="table-list">
    <div class="hd"></div>
    <div class="bd"></div>
    <div class="ft"></div>
</div>

<style>
.table-list {
    > .hd {
        color: red
    }
    > .bd {
        color: blue
    }
} 
</style>

這樣一來,只要保證根節點的class不重複,其子節點的class就不會重複。

而對於一些全域性樣式,人們習慣加上一個g-作為名稱空間,比如:

<style>
.g-hd {
    color: red
} 
</style>

這種依靠人為約定的css名稱空間,雖然比較原始,但有其優點:

  • 簡單有效,按模組-元件名稱的命名約定,基本上很容易保證其不重複。
  • 樣式名更具語意,從任何一個dom出發,向上一定能找到其元件根節點class名,基本上就能猜到其元件所在的業務模組、元件位置等。
  • 父元件很容易利用權重覆蓋子元件的任何樣式。

css_namespace + css_module

如果我們把css_modulecss_名稱空間結合起來,元件的名稱空間由css_module自動生成,那豈不是一種更優雅的解決css衝突的方案麼?

css_module中有2個特別的作用域限定符:

  • :global 該限定符下的class名稱將保持原樣,不會被css moudle轉換,比如:
    :global { 
        .test1 { color: blue; } 
        .test2 { color: red; } 
    }
    //編譯後
    .test1 { color: blue; }
    .test2 { color: red; } 
    
  • :local 該限定符下的class名稱,將會被css moudle轉換,比如:
      :local { 
          .test1 { color: blue; } 
          .test2 { color: red; } 
      }
      //編譯後
      .test1_3zyde4l1y { color: blue; }
      .test2_2DHwuiHWM { color: red; } 
    

如果我們使用css_namespace + css_module

<div :class="styles.root">
    <div class="hd"></div>
    <div class="bd"></div>
    <div class="ft"></div>
</div>

<style module>
:global {
  :local(.root) {
    > .hd { 
     color: red;
     .title {
         font-size: 18px;
     }
    }
    > .bd { color: blue; }
  }
}
</style>

//css編譯後
<style>
.root_3zyde4l1y > .hd{ color: red; }
.root_3zyde4l1y > .hd .title{ font-size: 18px; }
.root_3zyde4l1y > .bd{ color: blue; }
</style>

這樣的意思是:

  • 每個元件原則上僅根節點使用css_module自動生成不重複的class名稱,其餘內部元素保持原始命名,不做任何轉換。(當然某些情況下,也可以使用多個轉換)
  • 為了保證孫子輩樣式不影響別人,可以適當加入dom層級限定,比如> .hd這樣就只會影響子級的.hd

去除css_moudle隨機字元

<style>
.root_3zyde4l1y > .hd{ color: red; }
.root_3zyde4l1y > .hd .title{ font-size: 18px; }
.root_3zyde4l1y > .bd{ color: blue; }
</style>

根節點上的class命名帶個hash小尾巴,仍然很不優雅。其實hash字元只是為了保證這個名稱全域性唯一而已,你也可以使用另外的方法來保證。如果你為工程設計一個有意義的目錄結構,那麼完全可以使用目錄路徑來替代hash字串,比如你的工程目錄如下:

src
├── components
│    ├── moduleA
│    │     ├── componentX
│    │     ├── componentY
│    ├── moduleB
│    │     ├── componentZ

那麼:components-moduleA-componentX這個目錄路徑一定是全域性唯一的,所以你可以使用這個路徑來替代hash字元,css_module提供了自定義轉換className的方法:

type getLocalIdent = (
        context: LoaderContext,
        localIdentName: string,
        localName: string
) : string;

你可以通過該方法來將目錄路徑對映為class名稱,並替換掉一些固定的目錄,比如工程目錄如下:

src
├── assets
│     ├── css
│           ├── global.module.scss //全域性樣式
│                  ├── :local(.loading) {} //全域性樣式只需要加個g-字首,編譯成.g-loading
├── components
│     ├── NavBar
│           ├── index.module.scss
│                  ├── :local(.root) {} //根據目錄路徑可編譯成即可.comp-NavBar
│
├── modules
│     ├── user
│           ├── components
│                 ├── LoginForm
│                         ├── index.module.scss
│                               ├── :local(.root) {} //根據目錄路徑可編譯成.user-LoginForm,
│

注意的是src/modules/user/components/LoginForm/index.module.scss,根據目錄路徑可以生成:modules-user-components-LoginForm,但因為user是一個module,其名稱是唯一的,且內部結構遵循約定,所以可以簡化為:user-LoginForm

根據class名稱推測檔案位置

  • .g-loading - 帶g-字首,說明它是一個全域性class,對應的檔案一定是src/assets/css/global.module.scss
  • .comp-NavBar - 帶comp-字首,說明它是一個公共元件,對應的元件一定是src/components/NavBar
  • .user-LoginForm - 根據約定,對應的元件一定是src/modules/user/components/LoginForm

範例及原始碼

如果你也使用類似的工程目錄,那麼可以直接使用我封裝好了的路徑對映函數getCssScopedName

const {getCssScopedName} = require('@elux/cli-utils');
const srcPath = path.resolve(__dirname, '../src');

// webpack css-loader
{
    loader: 'css-loader',
    options: {
      importLoaders: 2,
      modules: {
        getLocalIdent: (context, localIdentName, localName) => {
          return getCssScopedName(srcPath, localName, context.resourcePath);
        },
        localIdentContext: srcPath,
      },
    },
  };

當然你也可自己實現個性化的getLocalIdent,無非就是一些正則匹配與替換罷了...

採用css_namespace + css_module的實際案例:

如圖所示,通過class名稱基本上就能推測出元件位置...