在 Web 應(yīng)用開發(fā)中,CSS 代碼的編寫是重要的一部分。CSS 規(guī)范從最初的 CSS1 到現(xiàn)在的 CSS3,再到 CSS 規(guī)范的下一步版本,規(guī)范本身一直在不斷的發(fā)展演化之中。這給開發(fā)人員帶來(lái)了效率上的提高。不過與其他 Web 領(lǐng)域的規(guī)范相似的處境是,CSS 規(guī)范在瀏覽器兼容性方面一直存在各種各樣的問題。不同瀏覽器在 CSS 規(guī)范的實(shí)現(xiàn)方面的進(jìn)度也存在很大差異。另外,CSS 規(guī)范本身的發(fā)展速度與社區(qū)的期待還有一定的差距。這也是為什么 SASS 和 LESS 等 CSS 預(yù)處理語(yǔ)言可以流行的重要原因。SASS 和 LESS 等提供了很多更實(shí)用的功能,也體現(xiàn)了開發(fā)人員對(duì) CSS 語(yǔ)言的需求。本文中要介紹的 PostCSS 是目前流行的一個(gè)對(duì) CSS 進(jìn)行處理的工具。PostCSS 依托其強(qiáng)大的插件體系為 CSS 處理增加了無(wú)窮的可能性。
PostCSS 介紹
PostCSS 本身是一個(gè)功能比較單一的工具。它提供了一種方式用 JavaScript 代碼來(lái)處理 CSS。它負(fù)責(zé)把 CSS 代碼解析成抽象語(yǔ)法樹結(jié)構(gòu)(Abstract Syntax Tree,AST),再交由插件來(lái)進(jìn)行處理。插件基于 CSS 代碼的 AST 所能進(jìn)行的操作是多種多樣的,比如可以支持變量和混入(mixin),增加瀏覽器相關(guān)的聲明前綴,或是把使用將來(lái)的 CSS 規(guī)范的樣式規(guī)則轉(zhuǎn)譯(transpile)成當(dāng)前的 CSS 規(guī)范支持的格式。從這個(gè)角度來(lái)說(shuō),PostCSS 的強(qiáng)大之處在于其不斷發(fā)展的插件體系。目前 PostCSS 已經(jīng)有 200 多個(gè)功能各異的插件。開發(fā)人員也可以根據(jù)項(xiàng)目的需要,開發(fā)出自己的 PostCSS 插件。
PostCSS 從其誕生之時(shí)就帶來(lái)了社區(qū)對(duì)其類別劃分的爭(zhēng)議。這主要是由于其名稱中的 post,很容易讓人聯(lián)想到 PostCSS 是用來(lái)做 CSS 后處理(post-processor)的,從而與已有的 CSS 預(yù)處理(pre-processor)語(yǔ)言,如 SASS 和 LESS 等進(jìn)行類比。實(shí)際上,PostCSS 的主要功能只有兩個(gè):第一個(gè)就是前面提到的把 CSS 解析成 JavaScript 可以操作的 AST,第二個(gè)就是調(diào)用插件來(lái)處理 AST 并得到結(jié)果。因此,不能簡(jiǎn)單的把 PostCSS 歸類成 CSS 預(yù)處理或后處理工具。PostCSS 所能執(zhí)行的任務(wù)非常多,同時(shí)涵蓋了傳統(tǒng)意義上的預(yù)處理和后處理。PostCSS 是一個(gè)全新的工具,給前端開發(fā)人員帶來(lái)了不一樣的處理 CSS 的方式。
使用 PostCSS
PostCSS 一般不單獨(dú)使用,而是與已有的構(gòu)建工具進(jìn)行集成。PostCSS 與主流的構(gòu)建工具,如 Webpack、Grunt 和 Gulp 都可以進(jìn)行集成。完成集成之后,選擇滿足功能需求的 PostCSS 插件并進(jìn)行配置。下面將具體介紹如何在 Webpack、Grunt 和 Gulp 中使用 PostCSS 的 Autoprefixer 插件。
Webpack
Webpack 中使用 postcss-loader 來(lái)執(zhí)行插件處理。在清單 1 中,postcss-loader 用來(lái)對(duì).css 文件進(jìn)行處理,并添加在 style-loader 和 css-loader 之后。通過一個(gè)額外的 postcss 方法來(lái)返回所需要使用的 PostCSS 插件。require(‘a(chǎn)utoprefixer’) 的作用是加載 Autoprefixer 插件。
清單 1. 在 Webpack 中使用 PostCSS 插件
var path = require('path');
module.exports = {
context: path.join(__dirname, 'app'),
entry: './app',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /\.css$/,
loader: "style-loader!css-loader!postcss-loader"
}
]
},
postcss: function () {
return [require('autoprefixer')];
}
}
Gulp
Gulp 中使用 gulp-postcss
來(lái)集成 PostCSS。在清單 2 中,CSS 文件由 gulp-postcss 處理之后,產(chǎn)生的結(jié)果寫入到 dist 目錄。
清單 2. 在 Gulp 中使用 PostCSS 插件
var gulp = require('gulp');
gulp.task('css', function() {
var postcss = require('gulp-postcss');
return gulp.src('app/**/*.css')
.pipe(postcss([require('autoprefixer')]))
.pipe(gulp.dest('dist/'));
});
Grunt
Grunt 中使用 grunt-postcss
來(lái)集成 PostCSS。Grunt 中需要使用 grunt.loadNpmTasks
方法來(lái)加載插件,如清單 3 所示。
清單 3. 在 Grunt 中使用 PostCSS 插件
module.exports = function(grunt) {
grunt.initConfig({
postcss: {
options: {
processors: [
require('autoprefixer')()
]
},
dist: {
src: 'app/**/*.css',
expand: true,
dest: 'dist'
}
}
});
grunt.loadNpmTasks('grunt-postcss');
}
下面介紹常用的 PostCSS 插件。
常用插件
1、Autoprefixer
Autoprefixer 是一個(gè)流行的 PostCSS 插件,其作用是為 CSS 中的屬性添加瀏覽器特定的前綴。由于 CSS 規(guī)范的制定和完善一般需要花費(fèi)比較長(zhǎng)的時(shí)間,瀏覽器廠商在實(shí)現(xiàn)某個(gè) CSS 新功能時(shí),會(huì)使用特定的瀏覽器前綴來(lái)作為正式規(guī)范版本之前的實(shí)驗(yàn)性實(shí)現(xiàn)。比如 Webkit 核心的瀏覽器使用-webkit-,微軟的 IE 使用-ms-。為了兼容不同瀏覽器的不同版本,在編寫 CSS 樣式規(guī)則聲明時(shí)通常需要添加額外的帶前綴的屬性。這是一項(xiàng)繁瑣而無(wú)趣的工作。Autoprefixer 可以自動(dòng)的完成這項(xiàng)工作。Autoprefixer 可以根據(jù)需要指定支持的瀏覽器類型和版本,自動(dòng)添加所需的帶前綴的屬性聲明。開發(fā)人員在編寫 CSS 時(shí)只需要使用 CSS 規(guī)范中的標(biāo)準(zhǔn)屬性名即可。清單 4 中給出了使用 CSS 彈性盒模型的 display 屬性聲明。
清單 4. 標(biāo)準(zhǔn)的 CSS 彈性盒模型的 display 屬性聲明
#content {
display: flex;
}
在經(jīng)過 Autoprefixer 處理之后得到的 CSS 如清單 5 所示。
清單 5. 使用 Autoprefixer 處理之后的 CSS
#content {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
Autoprefixer 使用 Can I Use 網(wǎng)站提供的數(shù)據(jù)來(lái)確定所要添加的不同瀏覽器的前綴。隨著瀏覽器版本的升級(jí),瀏覽器在新版本中可能已經(jīng)提供了對(duì)標(biāo)準(zhǔn)屬性的支持,從而不再需要添加額外的前綴。Autoprefixer 可以配置需要支持的瀏覽器。如”last 2 versions”表示主流瀏覽器的最近兩個(gè)版本,”ie 6-8”表示 IE 6 到 8,”> 1%”表示全球使用率大于 1%的瀏覽器版本。清單 6 中給出了配置 Autoprefixer 插件的示例。
清單 6. 配置 Autoprefixer 插件
require('autoprefixer')({
browsers: ['last 2 versions']
})
Autoprefixer 除了添加所需要的屬性名稱前綴之外,還可以移除 CSS 代碼中冗余的屬性名稱前綴。遺留 CSS 代碼中可能包含由開發(fā)人員手動(dòng)添加的舊版本的瀏覽器所支持的帶前綴的屬性名稱。Autoprefixer 默認(rèn)情況下會(huì)移除這些冗余的前綴??梢酝ㄟ^配置對(duì)象中的 remove 屬性來(lái)配置該行為。
2、cssnext
cssnext 插件允許開發(fā)人員在當(dāng)前的項(xiàng)目中使用 CSS 將來(lái)版本中可能會(huì)加入的新特性。cssnext 負(fù)責(zé)把這些新特性轉(zhuǎn)譯成當(dāng)前瀏覽器中可以使用的語(yǔ)法。從實(shí)現(xiàn)角度來(lái)說(shuō),cssnext 是一系列與 CSS 將來(lái)版本相關(guān)的 PostCSS 插件的組合。比如,cssnext 中已經(jīng)包含了對(duì) Autoprefixer 的使用,因此使用了 cssnext 就不再需要使用 Autoprefixer。
自定義屬性和變量
CSS 的層疊變量的自定義屬性規(guī)范(CSS Custom Properties for Cascading Variables)允許在 CSS 中定義屬性并在樣式規(guī)則中作為變量來(lái)使用它們。自定義屬性的名稱以 – 開頭。當(dāng)聲明了自定義屬性之后,可以在樣式規(guī)則中使用 var() 函數(shù)來(lái)引用,如清單 7 所示。
清單 7. CSS 自定義屬性和變量
:root {
--text-color: black;
}
body {
color: var(--text-color);
}
在經(jīng)過 cssnext 轉(zhuǎn)換之后的 CSS 代碼如清單 8 所示。
清單 8. 轉(zhuǎn)換之后的 CSS 代碼
body {
color: black;
}
自定義選擇器
CSS 擴(kuò)展規(guī)范(CSS Extensions)中允許創(chuàng)建自定義選擇器,比如可以對(duì)所有的標(biāo)題元素(h1 到 h6)創(chuàng)建一個(gè)自定義選擇器并應(yīng)用樣式。通過 @custom-selector 來(lái)定義自定義選擇器。在清單 9 中,–heading 是自定義選擇器的名稱,其等同于選擇器聲明 h1, h2, h3, h4, h5, h6。
清單 9. 自定義選擇器
@custom-selector :--heading h1, h2, h3, h4, h5, h6;
:--heading {
font-weight: bold;
}
經(jīng)過 cssnext 處理之后的 CSS 如清單 10 所示。
清單 10. 轉(zhuǎn)換之后的 CSS 代碼
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: bold;
}
樣式規(guī)則嵌套
樣式規(guī)則嵌套是一個(gè)非常實(shí)用的功能,可以減少重復(fù)的選擇器聲明。這也是 SASS 和 LESS 等 CSS 預(yù)處理器流行的一個(gè)重要原因。CSS 嵌套模塊規(guī)范(CSS Nesting Module)中定義了標(biāo)準(zhǔn)的樣式規(guī)則嵌套方式??梢酝ㄟ^ cssnext 把標(biāo)準(zhǔn)的樣式嵌套格式轉(zhuǎn)換成當(dāng)前的格式。CSS 嵌套規(guī)范中定義了兩種嵌套方式:第一種方式要求嵌套的樣式聲明使用 & 作為前綴,& 只能作為聲明的起始位置;第二種方式的樣式聲明使用 @nest 作為前綴,并且 & 可以出現(xiàn)在任意位置。清單 11 中給出了兩種不同聲明方式的示例。
清單 11. 樣式規(guī)則嵌套
.message {
font-weight: normal;
& .header {
font-weight: bold;
}
@nest .body & {
color: black;
}
}
經(jīng)過 cssnext 轉(zhuǎn)換之后的 CSS 代碼如清單 12 所示。
清單 12. 轉(zhuǎn)換之后的 CSS 代碼
.message {
font-weight: normal
}
.message .header {
font-weight: bold;
}
.body .message {
color: black;
}
3、CSS 模塊化
在編寫 CSS 代碼時(shí)會(huì)遇到的一個(gè)很重要的問題是 CSS 代碼的組織方式。當(dāng)項(xiàng)目中包含的 CSS 樣式非常多時(shí),該問題尤其突出。這主要是由于不同 CSS 文件中的樣式聲明可能產(chǎn)生名稱沖突?,F(xiàn)在的 Web 開發(fā)大多采用組件化的組織方式,即把應(yīng)用劃分成多個(gè)不同的組件。每個(gè)組件都可以有自己的 CSS 樣式聲明。比如,兩個(gè)組件的 CSS 中可能都定義了對(duì)于 CSS 類 title 的樣式規(guī)則。當(dāng)這兩個(gè)組件被放在同一個(gè)頁(yè)面上時(shí),沖突的 CSS 類名稱可能造成樣式的錯(cuò)亂。對(duì)于此類 CSS 類名沖突問題,一般的做法是避免全局名稱空間中的樣式聲明,而是每個(gè)組件有自己的名稱空間。BEM 通過特殊的命名 CSS 類名的方式來(lái)解決名稱沖突的問題。兩個(gè)不同組件中的標(biāo)題的 CSS 類名可能為 component1title 和 component2title。
CSS 模塊(CSS modules)并不要求使用 BEM 那樣復(fù)雜的命名規(guī)范。每個(gè)組件可以自由選擇最合適的簡(jiǎn)單 CSS 類名。組件的 CSS 類名在使用時(shí)會(huì)被轉(zhuǎn)換成帶唯一標(biāo)識(shí)符的形式。這樣就避免了名稱沖突。在組件開發(fā)中可以繼續(xù)使用簡(jiǎn)單的 CSS 類名,而不用擔(dān)心名稱沖突問題。清單 13 中給出了使用 CSS 模塊規(guī)范的 CSS 代碼。樣式規(guī)則之前的 :global 表示這是一個(gè)全局樣式聲明。其他的樣式聲明是局部的。
清單 13. 使用 CSS 模塊規(guī)范的 CSS 代碼
:global .title {
font-size: 20px;
}
.content {
font-weight: bold;
}
經(jīng)過轉(zhuǎn)換之后的 CSS 樣式聲明如清單 14 所示。全局的 CSS 類名 title 保存不變,局部的 CSS 類名 content 被轉(zhuǎn)換成 _content_6xmce_5。這樣就確保了不會(huì)與其他組件中名稱為 content 的類名沖突。
清單 14. 轉(zhuǎn)換之后的 CSS 代碼
.title {
font-size: 20px;
}
._content_6xmce_5 {
font-weight: bold;
}
由于在組件的 HTML 代碼中引用的 CSS 類名和最終生成的類名并不相同,因此需要一個(gè)中間的過程來(lái)進(jìn)行類名的轉(zhuǎn)換。對(duì)于 React 來(lái)說(shuō),可以使用 react-css-modules 插件;在其他情況下,可以使用 PostHTML 對(duì) HTML 進(jìn)行處理。postcss-modules 插件把 CSS 模塊中的 CSS 類名的對(duì)應(yīng)關(guān)系保存在一個(gè) JavaScript 對(duì)象中,可以被 PostHTML 中的 posthtml-css-modules 插件來(lái)使用。
在清單 15 中,在使用 postcss-modules 插件時(shí)提供了一個(gè)方法 getJSON。當(dāng) CSS 模塊的轉(zhuǎn)換完成時(shí),該方法會(huì)被調(diào)用。該方法的參數(shù) json 參數(shù)表示的是轉(zhuǎn)換結(jié)果的 JavaScript 對(duì)象。該對(duì)象被以 JavaScript 文件的形式保存到 cssModules 目錄下,并添加了模塊導(dǎo)出的邏輯。
清單 15. 保存 CSS 模塊的輸出文件
require('postcss-modules')({
getJSON: function(cssFileName, json) {
var cssName = path.basename(cssFileName, '.css');
var jsonFileName = path.resolve(dist, 'cssModules', cssName + '.js');
mkdirp.sync(path.dirname(jsonFileName));
fs.writeFileSync(jsonFileName, "module.exports = " + JSON.stringify(json) + ";");
}
})
清單 16 中給出了使用 Gulp 的 gulp-posthtml 來(lái)處理 HTML 文件的任務(wù)。posthtml-css-modules 可以處理一個(gè)目錄下的多個(gè) CSS 模塊輸出文件。
清單 16. 使用 PostHTML 處理 HTML 里支持 CSS 模塊
gulp.task('posthtml', function() {
var posthtml = require('gulp-posthtml');
return gulp.src('app/**/*.html')
.pipe(posthtml([ require('posthtml-css-modules')(path.join(dist, 'cssModules')) ]))
.pipe(gulp.dest('dist/'));
});
在 HTML 文件中使用 css-module 屬性來(lái)指定對(duì)應(yīng)的 CSS 類名。在 清單 17 中,名稱 header.content 的 header 表示的是 CSS 文件名,而 content 是該文件中定義的 CSS 類名。
清單 17. 使用 CSS 模塊的 HTML 文件
<div css-module="header.content">Hello world</div>
在經(jīng)過處理之后,得到的 HTML 內(nèi)容如清單 18 所示。
清單 18. 轉(zhuǎn)換之后的 HTML 文件
<div class="_content_6xmce_5">Hello world</div>
4、資源文件處理
在 CSS 中經(jīng)常會(huì)需要引用外部資源,如圖片和字體等。在 CSS 代碼中處理這些資源時(shí)會(huì)遇到一些常見的問題,比如圖片的路徑問題,內(nèi)聯(lián)圖片內(nèi)容,生成圖片 Sprites 等。對(duì)于這些問題,都有專門的 PostCSS 插件來(lái)提供所需的功能。
postcss-assets 插件用來(lái)處理圖片和 SVG。在 CSS 聲明中引用圖片時(shí),可以使用 resolve 加上圖片路徑的形式,如 resolve('logo.png')。在插件處理時(shí),會(huì)按照一定的順序在指定的目錄中查找該文件,如果找到,會(huì)用圖片的真實(shí)路徑來(lái)替換??梢酝ㄟ^選項(xiàng) loadPaths 來(lái)指定查找的路徑,basePath 來(lái)指定項(xiàng)目的根目錄。在 CSS 聲明中,可以使用 width、height 和 size 方法來(lái)獲取到圖片的寬度、高度和尺寸。當(dāng)需要內(nèi)聯(lián)一個(gè)圖片時(shí),可以使用 inline 方法。inline 會(huì)把圖片轉(zhuǎn)換成 Base64 編碼的 data url 的格式,這樣可以減少對(duì)圖片的 HTTP 請(qǐng)求。清單 19 給出了使用示例。
清單 19. Postcss-assets 插件使用示例
require('postcss-assets')({
loadPaths: ['assets/images']
})
清單 20 中給出了使用 resolve
的 CSS 樣式聲明。
清單 20. 使用 resolve 的 CSS 樣式聲明
.logo {
background-image: resolve('logo.png');
}
清單 21 中給出了 cssnext 處理之后的 CSS 代碼。
清單 21. 轉(zhuǎn)換之后的 CSS 代碼
.logo {
background-image: url('/assets/images/logo.png');
}
5、其他插件
PostCSS 還提供了一些其他實(shí)用的插件供開發(fā)使用。如:
postcss-stylelint 用來(lái)檢查 CSS 中可能存在的格式問題
cssnano 用來(lái)壓縮 CSS 代碼
postcss-font-magician 用來(lái)生成 CSS 中的 @font-face 聲明
precss 允許在 CSS 中使用類似 SASS 的語(yǔ)法
更多插件,請(qǐng)參考 PostCSS 官方網(wǎng)站。
開發(fā) PostCSS 插件
雖然 PostCSS 已經(jīng)有 200 多個(gè)插件,但在開發(fā)中仍然可能存在已有插件不能滿足需求的情況。這個(gè)時(shí)候可以開發(fā)自己的 PostCSS 插件。開發(fā)插件是一件很容易的事情。每個(gè)插件本質(zhì)只是一個(gè) JavaScript 方法,用來(lái)對(duì)由 PostCSS 解析的 CSS AST 進(jìn)行處理。
每個(gè) PostCSS 插件都是一個(gè) NodeJS 的模塊。使用 postcss 的 plugin 方法來(lái)定義一個(gè)新的插件。插件需要一個(gè)名稱,一般以 postcss- 作為前綴。插件還需要一個(gè)進(jìn)行初始化的方法。該方法的參數(shù)是插件所支持的配置選項(xiàng),而返回值則是另外一個(gè)方法,用來(lái)進(jìn)行實(shí)際的處理。該處理方法會(huì)接受兩個(gè)參數(shù),css 代表的是表示 CSS AST 的對(duì)象,而 result 代表的是處理結(jié)果。清單 22 中給出了一個(gè)簡(jiǎn)單的 PostCSS 插件。該插件使用 css 對(duì)象的 walkDecls 方法來(lái)遍歷所有的 color 屬性聲明,并對(duì) color 屬性值進(jìn)行檢查。如果屬性值為 black,就使用 result 對(duì)象的 warn 方法添加一個(gè)警告消息。
清單 22. PostCSS 插件示例
var postcss = require('postcss');
module.exports = postcss.plugin('postcss-checkcolor', function(options) {
return function(css, result) {
css.walkDecls('color', function(decl) {
if (decl.value == 'black') {
result.warn('No black color.', {decl: decl});
}
});
};
})
清單 22 中的插件的功能比較簡(jiǎn)單。PostCSS 插件一般通過不同的方法來(lái)對(duì)當(dāng)前的 CSS 樣式規(guī)則進(jìn)行修改。如通過 insertBefore 和 insertAfter 方法來(lái)插入新的規(guī)則。
結(jié)語(yǔ)
對(duì)于 CSS 的處理一直都是 Web 開發(fā)中的一個(gè)復(fù)雜問題,其中一部分來(lái)源于 CSS 規(guī)范,一部分來(lái)源于不同瀏覽器實(shí)現(xiàn)帶來(lái)的兼容性問題。PostCSS 為處理 CSS 提供了一種新的思路。通過 PostCSS 強(qiáng)大的插件體系,可以對(duì) CSS 進(jìn)行各種不同的轉(zhuǎn)換和處理,從而盡可能的把繁瑣復(fù)雜的工作交由程序去處理,而把開發(fā)人員解放出來(lái)。
本文對(duì) PostCSS 及其常用插件進(jìn)行了詳細(xì)介紹,希望可以幫助開發(fā)人員提高開發(fā)效率。