PostCSS
背景
在项目开发过程中,我们使用 rem 作为单位进行 CSS 样式开发。虽然 rem 用于适配各种型号的浏览器十分方便,但是它也有明显的缺点:
-
在把设计图上的尺寸转换为
rem时,我们需要自己进行计算,或者给开发工具安装插件,让开发工具来帮我们进行计算。

-
在修改和维护已经开发完成的代码时,面对如下代码:
.container { width: 2.66667rem; height: 1.6rem; }我们很难直观地知道这个
CSS样式设置的尺寸具体是多少。
那么有没有什么办法,能够让开发人员不用进行计算,只需要按照设计图上的尺寸写 px ,最终在浏览器渲染时,得到的是 rem 呢?
有!使用 PostCSS 。
1. 什么是 PostCSS ?
我们所说的 PostCSS 其实是由两部分组成的:PostCSS 工具和 PostCSS 插件。
PostCSS 工具有三个功能:
-
把
CSS代码解析为抽象语法树AST。 -
提供对
AST进行操作的API。 -
把
AST转换回CSS。
PostCSS 插件负责调用 API 来改变 AST 。
工具和插件协同工作,实现了对 CSS 代码的转换。
PostCSS 工作原理,如下图所示:

红色的 Parser 和 Stringifier 是 PostCSS 工具的解析器和字符串化工具,负责 CSS 代码和 AST 之间的转换工作。
黑色的 Plugin 1 和 Plugin 2 是 PostCSS 插件,负责从 old AST 到 new AST 的改变。
2. PostCSS 有什么用 ?
PostCSS 可以将 对开发人员友好的 CSS 代码转换为 对浏览器友好的 CSS 代码。
1. 提前使用先进的 CSS 特性。
CSS3 的新特性需要加入浏览器的私有前缀,才能被特定浏览器识别。例如:
.animate {
transform: rotate(180deg);
-ms-transform: rotate(180deg); /* IE 9 */
-moz-transform: rotate(180deg); /* Firefox */
-webkit-transform: rotate(180deg); /* Safari & Chrome */
-o-transform: rotate(180deg); /* Opera */
}
每次遇到这样的属性,我们都要写一大段,非常麻烦。
使用 autoprefixer 插件,开发时只需要编写:
/* CSS input */
.animate {
tranform: rotate(180deg);
}
/* CSS output */
.animate {
transform: rotate(180deg);
-ms-transform: rotate(180deg); /* IE 9 */
-moz-transform: rotate(180deg); /* Firefox */
-webkit-transform: rotate(180deg); /* Safari & Chrome */
-o-transform: rotate(180deg); /* Opera */
}
PostCSS 会帮我们为属性加上针对不同浏览器的私有前缀。
2. 解决全局 CSS 问题。
项目的不同模块之间, CSS 选择器难免会出现命名冲突的情况,这会造成样式污染。~~~~
postcss-modules 插件,可以自动以组件为单位隔绝 CSS 选择器。
/* CSS input */
.name {
color: gray;
}
/* CSS output */
.Logo__name__SVK0g {
color: gray;
}
Vue 中,在 <style> 标签上加入 scoped 来隔绝组件之间的 CSS 样式,就是通过 PostCSS 来实现的。感兴趣的同学可以到 Vue Loader 官网 了解一下。
3. 避免 CSS 代码中的错误。
stylelint 插件,可以检测错误的 CSS 代码,并给予开发人员提示。 stylefmt 插件,可以根据 stylelint 规则自动优化 CSS 的格式。
/* CSS input */
a {
color: #d3;
}
# console output
app.css
2:10 Invalid hex color
是不是很像 eslint ?
4. 其他
除了上面三种主要的功能, PostCSS 还可以把 CSS 代码转换成任意我们期望的形式,只要我们为它提供对应的插件。比如我们在背景中谈到的把 px 转换成 rem :
/* CSS input */
.container {
width: 200px;
height: 120px;
}
/* CSS output */
.container {
width: 2.66667rem;
height: 1.6rem;
}
还可以转换 @font-face 代码:
/* CSS input */
body {
font-family: "Alice";
}
/* CSS output */
@font-face {
font-family: "Alice";
font-style: normal;
font-weight: 400;
src: local("Alice"), local("Alice-Regular"),
url("http://fonts.gstatic.com/s/alice/v7/sZyKh5NKrCk1xkCk_F1S8A.eot?#") format("eot"),
url("http://fonts.gstatic.com/s/alice/v7/l5RFQT5MQiajQkFxjDLySg.woff2") format("woff2"),
url("http://fonts.gstatic.com/s/alice/v7/_H4kMcdhHr0B8RDaQcqpTA.woff") format("woff"),
url("http://fonts.gstatic.com/s/alice/v7/acf9XsUhgp1k2j79ATk2cw.ttf") format("truetype")
}
以及转换资源路径:
/* CSS input */
body {
background: url('foobar.jpg');
background: url('icons/baz.png');
}
/* CSS output */
body {
background: url('http://example.com/images/foobar.jpg');
background: url('http://example.com/images/baz.png');
}
PostCSS 和不同的插件结合使用,可以成为 CSS 预处理器、后处理器、代码优化工具,或者其他任何工具。
非常灵活。
我们还可以根据需求,开发属于自己的 PostCSS 插件。
3. 怎么使用 PostCSS ?
PostCSS 官方提供了很多种使用方式,而我们最常见的方式是在 Webpack 中用于处理 CSS。
-
安装
postcss和postcss-loader。$ npm i -D postcss postcss-loader -
在
webpack.config.js文件中加入配置:module.exports = { module: { rules: [ { test: /\.css$/i, use: [ 'style-loader', 'css-loader', { loader: 'postcss-loader', options: { postcssOptions: { plugins: [ [ 'autoprefixer', { // 其他选项 }, ], ], }, }, }, ], }, ], }, }; -
也可以使用
PostCSS的配置文件:// postcss.config.js module.exports = { plugins: [ [ 'autoprefixer', { // 其他选项 }, ], ], }; // webpack.config.js module.exports = { module: { rules: [ { test: /\.css$/i, use: ['style-loader', 'css-loader', 'postcss-loader'], }, ], }, }; -
这样
webpack运行时就会使用PostCSS来处理CSS文件了。
4. PostCSS 原理
用 node 读取 CSS 文件内容,把内容交给 PostCSS :
// node 的 fs 模块读取文件后,再传给 PostCSS
const css = fs.readFileSync("./index.css");
postcss([autoprefixer]).process(css);
1. 解析 CSS
在接到 CSS 代码之后,我们先把它以字符串格式存储起来:
this.css = css.toString();
然后我们需要对字符串进行扫描,把字符串解析成 token 格式:
// token
[
'word', // 类型
'.className', // 匹配的内容
1, // 起始位置
10 // 结束位置
]
-
为什么要解析成
token格式呢?因为字符串格式的数据操作起来麻烦而且效率低下,而
token格式对计算机来说易于理解和操作。 -
怎么才能解析成
token格式呢?如果你对编译原理有所了解,那么就会知道,这个解析的过程被称为词法分析。
词法分析:从左向右逐行扫描源程序的字符,识别出各个单词,确定单词的类型。将识别出的单词转换成统一的词法单元
token。
那么如何进行词法分析呢?
假设有这样一段 CSS 代码,我们来对它进行词法分析:
.my-font {
color: #FFF;
font-size: 12px;
}
那么我们首先需要定义一些变量,用来存储扫描时的数据:
class Tokenizer {
constructor(css) {
this.css = css.toString(); // 输入的 css 代码
this.length = css.length; // 字符串长度
this.pos = 0; // 储存当前位置
this.next; // 储存下一个位置
this.code = ""; // 当前字符的 charCode
this.currentToken; // 当前 token
this.tokens = []; // 结果 tokens
}
}
还需要声明一些变量,用来判断符号类型:
const NEWLINE = '\n'.charCodeAt(0); // 换行符
const SPACE = ' '.charCodeAt(0); // 空格
const TAB = '\t'.charCodeAt(0); // 制表符
const OPEN_CURLY = '{'.charCodeAt(0); // 左大括号
const CLOSE_CURLY = '}'.charCodeAt(0); // 右大括号
const SEMICOLON = ';'.charCodeAt(0); // 分号
const COLON = ':'.charCodeAt(0); // 冒号
以及查找符号的正则表达式:
const RE_WORD_END = /[\t\n\f\r !"#'():;@[\\\]{}]|\/(?=\*)/g;
接下来,我们再规定几种 token 的类型:
space 空格、换行符、制表符
word 单词
string 字符串
brackets 括号
; 分号
: 冒号
然后我们需要对 CSS 代码从左向右逐个字符进行扫描,一旦发现满足某一种 token 类型,那就把这段字符串截取,存储到 token 中,如下图所示:
我们定义一个方法 nextToken 来实现识别 token 的功能:
nextToken() {
if (this.pos >= this.length) return; // 如果指针位置超过文件长度,终止扫描
this.code = this.css.charCodeAt(this.pos); // 当前字符
// 识别不同类型的 token
switch (this.code) {
// 当前字符是空格、换行符、制表符时,存入类型为 space 的 token 中
// 如果是连续的空格、换行符、制表符的情况,会一并截取,存入一个 token 中
case NEWLINE:
case SPACE:
case TAB: {
this.next = this.pos;
do {
this.next += 1;
this.code = this.css.charCodeAt(this.next);
} while (
this.code === SPACE ||
this.code === NEWLINE ||
this.code === TAB
)
this.currentToken = ['space', this.css.slice(this.pos, this.next)];
this.pos = this.next - 1;
break;
}
// 当前字符如果是大括号,存入类型为 { 或者 } 的 token 中
case OPEN_CURLY:
case CLOSE_CURLY: {
let controlChar = String.fromCharCode(this.code);
this.currentToken = [controlChar, controlChar, this.pos];
break;
}
default: {
// 从当前位置到下一个符号之间的内容,存入类型为 word 的 token
RE_WORD_END.lastIndex = this.pos + 1;
RE_WORD_END.test(this.css);
if (RE_WORD_END.lastIndex === 0) {
this.next = this.css.length - 1
} else {
this.next = RE_WORD_END.lastIndex - 2
}
this.currentToken = ['word', this.css.slice(this.pos, this.next + 1), this.pos, this.next];
this.pos = this.next;
break;
}
}
this.pos ++
return this.currentToken
}
}
最后定义一个 getTokens 方法,让它循环调用 nextToken 方法,直到文件结尾:
// 使用 while 循环,逐个获取 token ,然后保存到 tokens 中,直到文件结尾
getTokens() {
let token;
while (!this.endOfFile()) {
this.nextToken();
this.tokens.push(this.currentToken);
}
return this.tokens;
}
// 判断是否扫描到文件的结尾
endOfFile() {
return this.pos >= this.length;
}
调用 getTokens 方法,我们就会得到 token 格式的数据:
[
[ 'word', '.my-font', 0, 7 ],
[ 'space', ' ' ],
[ '{', '{', 9 ],
[ 'space', ' \n ' ],
[ 'word', 'color', 14, 18 ],
[ 'word', ':', 19, 19 ],
[ 'space', ' ' ],
[ 'word', '#FFF', 21, 24 ],
[ 'word', ';', 25, 25 ],
[ 'space', ' \n\t' ],
[ 'word', 'font-size', 29, 37 ],
[ 'word', ':', 38, 38 ],
[ 'space', ' ' ],
[ 'word', '12px', 40, 43 ],
[ 'word', ';', 44, 44 ],
[ 'space', '\n' ],
[ '}', '}', 46 ],
[ 'space', '\n' ]
]
2. 生成 AST
AST 是由一个个节点 Node 组成的树形数据结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qCDxo0vh-1646982702883)(./images/AST.png)]
在得到 token 格式的数据后,PostCSS 根据不同的 token 的类型,进行不同的处理,生成不同的 Node 节点。
parse() {
let token;
// 逐个获取 token
while (!this.tokenizer.endOfFile()) {
token = this.tokenizer.nextToken();
// 不同的 token 类型,使用不同的处理方式。
switch (token[0]) {
// 评论类型的 token , 生成 commet 节点
case 'comment':
this.comment(token);
break;
// @类型的 token , 生成 atrule 节点
case 'at-word':
this.atrule(token);
break;
// ...
default:
// rule 和 decl 节点,在 other 方法里生成
this.other(token);
break;
}
}
this.endFile();
}
PostCSS 把节点分为 5 种类型:AtRule, Comment, Declaration, Root, Rule ,每种节点存储了详细的描述信息。以Rule 节点为例:
Rule {
raws: { before: '', between: ' ', semicolon: true, after: ' ' }, // 符号信息
type: 'rule', // 节点类型
nodes: [ [Declaration] ], // 子节点列表
parent: Root { // 父节点信息
raws: [Object],
type: 'root',
nodes: [Circular *1],
source: [Object],
[Symbol(isClean)]: false,
[Symbol(my)]: true
},
source: { start: [Object], input: [Input], end: [Object] }, // 源代码信息
selector: '.className', // 选择器
[Symbol(isClean)]: false,
[Symbol(my)]: true
}
并且 PostCSS 提供了操作 AST 的 API ,例如:walkRules 、 insertBefore 、 removeChild 等,详细文档可以查看: PostCSS API 文档
3. 调用插件
得到了完整的 AST 之后,PostCSS 会把它交给插件来处理:
// 用 parser 把 css 解析成 AST ,保存到 root
let root = parser(css, opts)
// 实例化一个 Result ,用来保存结果
this.result = new Result(processor, root, opts)
// 遍历插件列表
for(let i = 0; i < this.plugins.length; i ++) {
let plugin = this.plugins[i]
let promise = this.runOnRoot(plugin);
}
// 把 AST 交给插件处理
runOnRoot(plugin) {
return plugin(this.result.root, this.result)
}
插件内部利用 API 对 AST 进行操作,我们以 postcss-pxtorem 为例:
css => {
// 遍历 AST 的所有属性节点 decl
css.walkDecls((decl, i) => {
// 属性值中没有 px 的不处理
if (decl.value.indexOf("px") === -1) return;
// 有 px 的,把属性值换算为 rem
const value = decl.value.replace(pxRegex, pxReplace);
decl.value = value;
});
}
4. 字符串化
在插件处理完 AST 之后,PostCSS 使用 Stringifier 把 AST 转换回字符串:
// 注释节点,会在两端拼接 /* */
comment(node) {
let left = this.raw(node, 'left', 'commentLeft')
let right = this.raw(node, 'right', 'commentRight')
this.builder('/*' + left + node.text + right + '*/', node)
}
// at选择器会在前面拼接@
atrule(name, semicolon) {
let name = "@" + node.name;
// ...
}
// 属性节点会根据情况在末尾拼接 ; 和 !important
decl(node, semicolon) {
let between = this.raw(node, 'between', 'colon')
let string = node.prop + between + this.rawValue(node, 'value')
if (node.important) {
string += node.raws.important || ' !important'
}
if (semicolon) string += ';'
this.builder(string, node)
}
最后我们再把字符串写入一个 CSS 文件,就得到了新的 CSS 代码。
fs.writeFile("dest/app.css", result.css);










