層疊樣式表
我們知道,css的全名叫做層疊樣式表,這個(gè)“層疊”到底是什么意思呢?
有一種解釋是,如果你先寫(xiě)了一條樣式規(guī)則(選手1):
.title {
color: silver;
}
然后又在后邊寫(xiě)了一條類(lèi)似的(選手2):
因?yàn)槊窒嗤x手2就會(huì)和選手1打起來(lái)(讓你丫冒充我?。?。結(jié)果是選手2獲勝,class名為title的元素,最終的color值為gold。
css里就像這樣,隨時(shí)可能一言不和就發(fā)生戰(zhàn)爭(zhēng),結(jié)果輸?shù)舻囊环骄蜁?huì)被勝利的一方所覆蓋?!皩盈B”一詞可以說(shuō)形象地描述了這個(gè)過(guò)程。
那么,為什么會(huì)有這樣的層疊(zhàn zhēng )呢?
css的作用域問(wèn)題
在javascript里,可以做到這樣的搭配:
var title = "silver";
(function(){
var title = "gold";
console.log(title); // gold
}());
console.log(title); // silver
利用javascript的函數(shù)作用域,兩位同樣名為title的選手可以友好相處。
但回到css里的樣式規(guī)則,情況就完全不是這么回事了。
css不是程序語(yǔ)言,但如果說(shuō)要給它加一個(gè)作用域的概念的話(huà),那就是:只有全局作用域。
無(wú)論分拆為多少個(gè)css文件,無(wú)論用怎樣的方式引入,所有的樣式規(guī)則都位于同一作用域,只要選擇符近似,就有發(fā)生覆蓋的可能。
減少相互影響的策略
為減少相互影響,避免預(yù)料之外的樣式覆蓋,我們一直以來(lái)想過(guò)很多辦法。
比如你接手一個(gè)別人留下來(lái)的舊項(xiàng)目,接下來(lái)要新增一個(gè)標(biāo)題元素的時(shí)候,你會(huì)有意識(shí)地不去使用.title這樣模糊的class名,因?yàn)樗菀字孛?。最終,你用的名稱(chēng)可能是:
.module-sp-title {
color: deepskyblue;
}
即使你決定要用.title這個(gè)名字,你也會(huì)加上包含選擇符作為限定:
.module-1 .title {
font-size: 18px;
}
/* ... */
.module-2 .title {
font-size: 14px;
}
其中.module-1和.module-2的名字應(yīng)該是唯一的,這樣的代碼在組件化(模塊化)的開(kāi)發(fā)風(fēng)格里很常見(jiàn)。
此外,一些有名的css理論,如SMACSS,會(huì)建議你為所有布局樣式使用l-或layout-的前綴,以示區(qū)分。
類(lèi)似的做法還有很多,但歸結(jié)起來(lái),都是在嘗試提供一種合理的命名約定。而合理的命名約定,的確是組織css代碼的有效策略。
現(xiàn)在,我們有了新的可用策略,CSS Modules就是其中之一。
技術(shù)流的模塊化
CSS Modules是一種技術(shù)流的組織css代碼的策略,它將為css提供默認(rèn)的局部作用域。
CSS Modules是如何做到的呢?來(lái)看一個(gè)CSS Modules的簡(jiǎn)單例子吧。
有這樣的一個(gè)html元素:
h2 id="example_title" class="title">a title for CSS Modules/h2>
按照普通css的寫(xiě)法,我們可以這樣為它添加樣式:
.title {
background-color: snow;
}
現(xiàn)在我們改用CSS Modules。首先,css保持不變。然后,修改html的寫(xiě)法。不再這樣直接寫(xiě)html,而是改為在javascript文件里動(dòng)態(tài)添加,這樣做(css文件名為main.css):
var styles = require("./main.css");
var el = document.getElementById("example_title");
el.outerHTML = 'h2 class="' + styles.title + '">a title for CSS Modules/h2>';
咦,require了一個(gè)css文件?對(duì)的,所以要用到webpack。編譯后,html和css會(huì)變成這樣:
看到這樣不太美觀(guān)的class名你大概就明白了,CSS Modules無(wú)法改變css全局作用域的本性,它是依靠動(dòng)態(tài)生成class名這一手段,來(lái)實(shí)現(xiàn)局部作用域的。顯然,這樣的class名就可以是唯一的,不管原本的css代碼寫(xiě)得有多隨便,都可以這樣轉(zhuǎn)換得到不沖突的css代碼。
模擬的局部作用域也沒(méi)有關(guān)系,它是可靠的。
這個(gè)CSS Modules的例子說(shuō)完了,但你一定跟我最初看到的時(shí)候一樣有很多問(wèn)題。
CSS Modules的應(yīng)用細(xì)節(jié)
如何啟用CSS Modules
“webpack編譯css我也用過(guò),怎么我用的時(shí)候不長(zhǎng)這樣?”
一般來(lái)說(shuō),require一個(gè)css文件的寫(xiě)法是:
但在前面的例子中,用了var styles = require("./main.css");的寫(xiě)法。這就好像是在說(shuō),我要這個(gè)css文件里的樣式是局部的,然后我根據(jù)需要自行取用。
在項(xiàng)目里應(yīng)用CSS Modules有很多方法,目前比較常用的是使用webpack的css-loader。在webpack配置文件里寫(xiě)css-loader?modules就可以開(kāi)啟CSS Modules,例如前面的例子所用的:
module: {
loaders: [{
test: /\.css$/,
loader: 'style!css?modules'
}]
}
才發(fā)現(xiàn)一直用著的css-loader原來(lái)有這功能?其實(shí),CSS Modules確實(shí)是一個(gè)后來(lái)才并入css-loader的新功能。
自定義生成的class名
“
名字都這樣了,還怎么調(diào)試?”
為css-loader增加localIdentName參數(shù),是可以指定生成的名字。localIdentName的默認(rèn)值是[hash:base64],一般開(kāi)發(fā)環(huán)境建議用類(lèi)似這樣的配置:
{
test: /\.css$/,
loader: 'style!css?moduleslocalIdentName=[name]__[local]___[hash:base64:5]'
}
同樣應(yīng)用到前面的例子里,這時(shí)候就會(huì)變成這樣的結(jié)果:
這樣是不是要有意義多了?
如果是線(xiàn)上環(huán)境,可以考慮用更短的名字進(jìn)一步減小css文件大小。
CSS Modules下的html
(看了前面例子里的el.outerHTML = ...后)
“什么,outerHTML?class名還要拼接?你家html才這么寫(xiě)呢!”
很遺憾,CSS Modules官方的例子,也是這個(gè)意思:要使用CSS Modules,必須想辦法把變量風(fēng)格的class名注入到html中。也就是說(shuō),html模板系統(tǒng)是必需的,也正是如此,相比普通css的情況,CSS Modules的html寫(xiě)起來(lái)要更為費(fèi)勁。
如果你搜一下CSS Modules的demo,可以發(fā)現(xiàn)大部分都是基于React的。顯然,虛擬DOM風(fēng)格的React,搭配CSS Modules會(huì)很容易(ES6):
import styles from './ScopedSelectors.css';
import React, { Component } from 'react';
export default class ScopedSelectors extends Component {
render() {
return (
div className={ styles.root }>
p className={ styles.text }>Scoped Selectors/p>
/div>
);
}
};
如果不使用React,還是那句話(huà),只要有辦法把變量風(fēng)格的class名注入到html中,就可以用CSS Modules。原始的字符串拼接的寫(xiě)法顯然很糟糕,但我們可以借助各種模板引擎和編譯工具做一些改進(jìn)。下面請(qǐng)看一個(gè)用Jade的參考示例。
想象一下你有一個(gè)用普通css的頁(yè)面,但你想在一小塊區(qū)域使用CSS Modules。這一塊區(qū)域在一個(gè)容器元素里:
div id="module_sp_container">/div>
后用jade來(lái)寫(xiě)html(關(guān)聯(lián)的css文件為module_sp.css):
- styles = require("./module_sp.css");
h2(class=styles.title) a title for CSS Modules
接下來(lái),仍然是在javascript里添加這段jade生成的html:
var el = document.getElementById("module_sp_container");
var template = require("./main.jade");
el.innerHTML = template();
最后,記得在css-loader啟用CSS Modules的同時(shí),增加jade-loader:
{
test: /\.jade$/,
loader: 'jade'
}
編譯運(yùn)行,就可以得到想要的結(jié)果。除Jade以外,還有些其他CSS Modules的html應(yīng)用方案,推薦參考github上的這篇issue。
目前CSS Modules還在發(fā)展中,而且也在考慮改進(jìn)CSS Modules下的html寫(xiě)作體驗(yàn)。CSS Modules團(tuán)隊(duì)成員有提到一個(gè)叫CSS Modules Injector的未來(lái)規(guī)劃項(xiàng)目,目的是讓開(kāi)發(fā)者不用javascript也可以使用CSS Modules(這就很接近原生html + css的組合了)。
CSS Modules下的樣式復(fù)用
“樣式都是唯一的了,怎么復(fù)用?”
我們已經(jīng)說(shuō)了挺多普通css單個(gè)全局作用域的壞處。但對(duì)應(yīng)的,這也有一個(gè)很大的好處,就是便于實(shí)現(xiàn)樣式的復(fù)用。css理論OOCSS也是在追求這一點(diǎn)。
CSS Modules提供一個(gè)composes方法用于樣式復(fù)用。例如,你有一個(gè)btn.css里有一條:
.btn{
display: inline-block;
}
然后,你在另一個(gè)CSS Module的module_sp.css里可以這樣引入它:
.btn-sp{
composes: btn from "./btn.css";
font-size: 16px;
}
那么,這個(gè)div.btn-sp的DOM元素將會(huì)是:
可以看到,composes的用法比較類(lèi)似sass的@extend,但不同于@extend的是,composes并不增加css里的選擇符總量,而是采用組合多個(gè)class名的形式。在這個(gè)例子里,原本僅有1個(gè)class的div.btn-sp,變成了2個(gè)class。
因此,CSS Modules建議只使用1個(gè)class就定義好對(duì)應(yīng)元素所需的全部樣式。它們會(huì)再由CSS Modules轉(zhuǎn)換為適當(dāng)?shù)腸lass組合。
CSS Modules團(tuán)隊(duì)成員認(rèn)為composes是CSS Modules里最強(qiáng)大的功能:
For me, the most powerful idea in CSS Modules is composition, where you can deconstruct your visual inventory into atomic classes, and assemble them at a module level, without duplicating markup or hindering performance.
更詳細(xì)的composes的用法及其理解,推薦閱讀CSS Modules: Welcome to the Future。
其他可能有用的補(bǔ)充
和已有的普通css共存
很多項(xiàng)目會(huì)引入Bootstrap、Materialize等框架,它們是普通的、全局的css。此外,你也可能自己會(huì)寫(xiě)一些普通css。如何共存呢?CSS Modules團(tuán)隊(duì)成員對(duì)此提到過(guò):
a CSS Module should only import information relative to it
意思是,建議把CSS Modules看做一種新的css,和原來(lái)的普通css區(qū)分開(kāi)來(lái)。比如,composes的時(shí)候,不要從那些普通的css里去取。
在css-loader里通過(guò)指定test、include、exclude來(lái)區(qū)分它們。保持CSS Modules的純凈,只有想要應(yīng)用CSS Modules的css文件,才啟用CSS Modules。
只轉(zhuǎn)換class和id
經(jīng)過(guò)我自己的測(cè)試,CSS Modules只轉(zhuǎn)換class和id,此外的標(biāo)簽選擇符、偽類(lèi)等都不會(huì)被轉(zhuǎn)換。
建議只使用class。
一個(gè)CSS Module的輸出
簡(jiǎn)單用console.log()就可以查看CSS Module的輸出:
var styles = require("./main.css");
console.log("styles = ", styles);
結(jié)果類(lèi)似這樣:
{
"btn-sp": "_2SCQ7Kuv31NIIiVU-Q2ubA _2r6eZFEKnJgc7GLy11yRmV",
title: "_1m-KkPQynpIso3ofWhMVuK"
}
這可以幫助理解CSS Modules是怎樣工作的。
預(yù)編譯器
sass等預(yù)編譯器也可以用CSS Modules,對(duì)應(yīng)的loader可能是這樣:
{
test: /\.scss$/,
loader: 'style!css?modules!resolve-url!sass?sourceMap'
}
注意不要因?yàn)槭莝ass就習(xí)慣性地用嵌套寫(xiě)法,CSS Modules并不適合使用包含選擇符。
建議的命名方式
CSS Modules會(huì)把.title轉(zhuǎn)換為styles.title,由于后者是用在javascript中,因此駝峰命名會(huì)更適合。
如果像我之前那樣寫(xiě).btn-sp,需要注意在javascript中寫(xiě)為styles["btn-sp"]。
此外,你還可以為css-loader增加camelCase參數(shù)來(lái)實(shí)現(xiàn)自動(dòng)轉(zhuǎn)換:
{
test: /\.css$/,
loader: 'style!css?modulescamelCase',
}
這樣即便你寫(xiě).btn-sp,你也可以直接在javascript里用styles.btnSp。
結(jié)語(yǔ)
無(wú)論是一直以來(lái)我們認(rèn)真遵循的命名約定,還是這個(gè)新的CSS Modules,目的都是一樣的:可維護(hù)的css代碼。我覺(jué)得就CSS Modules基本還是在寫(xiě)css這一點(diǎn)來(lái)說(shuō),它還是很友好的。
雖然本文為了嚴(yán)謹(jǐn),結(jié)果寫(xiě)了相當(dāng)長(zhǎng)的篇幅,但希望你讀過(guò)之后,還能覺(jué)得CSS Modules是簡(jiǎn)單易懂的。因?yàn)檫@樣,我就達(dá)成我的目的:扣題,了。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
您可能感興趣的文章:- 在Create React App中使用CSS Modules的方法示例
- 在create-react-app中使用css modules的示例代碼
- Vue 中使用 CSS Modules優(yōu)雅方法