我們來回憶一下,在`css_scoped`和`css_module`出現之前,人們是如何避免css命名衝突的?對,就是人為的定義一些`css命名空間`。那個時候,對每個Component組件都會在其根節點上定義一個不重覆的ID或者class作為其**命名空間**,然後其內部的其它class都會以此命... ...
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_scoped
,css_module
方案更通用,不改變其本身的權重,而且渲染性能要比前者好很多,所以更推薦大家使用css_module
。
不足之處
然而不管是css_scoped
還是css_module
,都繞不開2大缺點:
- 由於加上了隨機字元,所以如果想在父組件中覆蓋子組件中的樣式變得麻煩,雖然
css_scoped
可以使用穿透,但這樣容易引發別的問題。 - 加上隨機字元讓class名稱變得不優雅,也影響編譯速度。
css命名空間
我們來回憶一下,在css_scoped
和css_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_module
和css_命名空間
結合起來,組件的命名空間由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
的實際案例:
-
或者使用任意一個elux工程模版:
npm create elux@latest 或 yarn create elux
如圖所示,通過class名稱基本上就能推測出組件位置...