为什么要使用PostCSS?
或许你已经在用Sass或Less来给你的CSS工作流添加点逻辑:变量、if/else语句、函数、还有混入。但是,这些预处理器都有它们的局限性。如果你需要基于其他CSS属性的存在来添加一两个CSS属性怎么办?
比如说,我们最近在DockYard疯狂地搞了一堆渐进式Web应用。我们想要那种nice的、有原生感的弹性/惯性滚动,每当我们有个元素设置了overflow: scroll(或overflow-x / overflow-y)的时候。每次我们让一个元素可滚动,我们就得加上-webkit-overflow-scrolling: touch
。预处理器没有办法检测一个给定选择器块里都有哪些属性,所以我们需要一个冗长的混入解决方案。另外,我们并不是在每个项目中都使用预处理器,所以我们需要一个PostCSS的解决方案。
比较一下,这里是我们可能会实现一个Sass混入来达到这种滚动行为的方式:
scss
@mixin overflow-scroll($direction: false) {
$property: if($direction, 'overflow-#{$direction}', 'overflow');
#{$property}: scroll;
-webkit-overflow-scrolling: touch;
}
看看这个混入在Sassmeister上的使用。
这种方法行得通,但它有一些明显的缺点。首先,你不再是在写规范的CSS了:你在写一个更啰嗦的"抽象"。任何来到这个代码库的开发者都得学习另一个"抽象"。其次,这种方法不是很自动化。如果你忘了使用混入,你就得不到那个额外的属性。
另一方面,PostCSS能够完全自动化这个过程,不需要写任何抽象。一个PostCSS插件可以找到任何带有滚动溢出的选择器块,并自动插入额外的属性。
PostCSS是什么?
在我们深入插件编写过程之前,让我们先理解一下PostCSS是什么。PostCSS允许我们用JavaScript函数来操作我们的CSS。它通过三种方式来实现这一点:
- PostCSS将你的CSS文件转换成一个JS对象。
- PostCSS插件可以遍历这个对象并添加/删除/修改选择器和属性。
- PostCSS将那个对象转换回一个CSS文件。
编写一个PostCSS插件
PostCSS团队在写你自己的插件上做得很好,消除了障碍。假设你已经掌握了如下基本知识:
- 你对git和命令行很熟悉,
- 你能写JavaScript函数,以及
- 你已经安装了node并且知道如何安装npm模块
克隆PostCSS插件样板Repo
到终端并拉取PostCSS插件样板repo:
bash
$ git clone git@github.com:postcss/postcss-plugin-boilerplate.git
接下来,运行repo中的向导脚本:
bash
$ node ./postcss-plugin-boilerplate/start
这个脚本会在你的终端中问你几个问题。它会从你的本地git配置(如果你设置了的话)中提取你的名字和邮箱地址,然后询问你的Github用户名。
接下来,你会选择你的插件名字。它会以postcss-开头,你来完成名字。向导之后会要求你完成描述你的插件将做什么的句子。最后,它会开始一个你来完成的逗号分隔的标签列表。
一旦你完成了这个设置,你将会有一个样板目录:向导用你在回答脚本问题时选择的插件名字创建了它。让我们前往那个目录:
bash
$ cd postcss-test-plugin
在里面,你会找到一些基于node的项目的熟悉组件:index.js,package.json,一个node_modules目录。你将在index.js中放置你的逻辑:操作CSS的函数。如果你的插件有任何其他node模块依赖,package.json会管理它们并且在node_modules中安装它们。
index.js中的样板代码
让我们开始看看index.js提供的样板代码:
javascript
var postcss = require('postcss');
它做的第一件事是获取必要的前提条件:PostCSS库本身。接下来的代码依赖于有访问PostCSS的权限。
javascript
module.exports = postcss.plugin('postcss-test-plugin', function (opts) {
opts = opts || {};
// 在这里处理选项
return function (root, result) {
// 在这里转换CSS AST
};
});
这段代码实际上包含了操作你的CSS的指令。
我们需要做的第一件事是遍历样式表中的
所有声明块。返回函数里的root参数有一个方法可以做到这一点:.walkRules()。
遍历每个选择器块
我们将用.walkRules()升级样板代码,以循环遍历每个声明块并让我们访问其中的样式:
javascript
root.walkRules(function(rule) {
// 一会儿我们会在这里放更多代码...
});
现在我们正在遍历每个选择器块,我们需要看看它是否包含overflow属性。要访问这些属性,我们将使用传递给上面函数的rule的.walkDecls()方法。
遍历每个属性
javascript
rule.walkDecls(function(decl) {
// 在这里我们处理每个`decl`对象。
});
在这个循环内部,decl是表示样式声明的对象。它包含关于属性-值对的数据以及一些操作它的方法。对我们的案例来说最重要的两件事是decl.prop(属性名)和decl.value(属性值)。
找到Overflow属性
为了检测一个decl是否与overflow相关,我们可以在这个循环内部放一个if语句:if (decl.prop.indexOf('overflow') === 0),但有一个更高效的方法。PostCSS允许我们在.walkDecls()方法中过滤特定的属性。你可以在PostCSS API文档中找到这个。如果我们像这样过滤overflow属性,我们就不需要那个if语句了:
javascript
rule.walkDecls('overflow', function(decl) {
// 在这里我们处理`decl`对象。
});
这还不够好。它只会找到overflow属性。如果我们也想要考虑到overflow-x和overflow-y(我们确实想),我们需要稍微调整一下那个过滤器。这个prop参数不接受属性名的数组(我试过['overflow', 'overflow-x', 'overflow-y'],但没用)。要匹配多个属性,我们将不得不使用一点RegEx:/^overflow-?/。这里有一个快速的语法解释:^意味着属性名必须以overflow开头;-?意味着"overflow这个词后面可能有也可能没有-"。注意我们不在regex周围使用''。这带我们到了:
javascript
rule.walkDecls(/^overflow-?/, function(decl) {
// 在这里我们处理`decl`对象。
});
防止重复的属性
我们已经走了一段路,但现在我们几乎到了:这段代码将遍历我们样式表中的所有选择器块,然后遍历这些选择器中所有与溢出相关的属性。剩下要做的就是插入我们的属性。下一块代码将检查溢出相关属性的值是否为scroll,如果是的话,添加让它感觉更原生的属性。
javascript
if (decl.value === 'scroll') {
rule.append({
prop: '-webkit-overflow-scrolling',
value: 'touch'
});
}
在这个案例中,我们借助于一个if语句。我们之前写的循环过滤了属性,所以这个函数只在decl对象的属性以overflow-?开始时运行。现在,如果decl.value是scroll,我们将添加一个属性-值对到父规则对象。我们现在几乎完成了。有可能某人已经包含了-webkit-overflow-scrolling属性。我们不想重复它。PostCSS有一个函数让我们检查一个给定的属性是否已经在选择器块中:
javascript
var hasTouch = rule.some(function(i) {
return i.prop === '-webkit-overflow-scrolling';
});
if (!hasTouch) {
rule.append({
prop: '-webkit-overflow-scrolling',
value: 'touch'
});
}
现在我们有了一个更好的函数:如果开发者有意将-webkit-overflow-scrolling放在需要的地方,我们不会重复它。
结论
只用20行代码,我们就创建了一个有用的PostCSS插件。
javascript
var postcss = require('postcss');
module.exports = postcss.plugin('postcss-test-plugin', function() {
return function(root) {
root.walkRules(function(rule) {
rule.walkDecls(/^overflow-?/, function(decl) {
if (decl.value === 'scroll') {
var hasTouch = rule.some(function(i) {
return i.prop === '-webkit-overflow-scrolling';
});
if (!hasTouch) {
rule.append({
prop: '-webkit-overflow-scrolling',
value: 'touch'
});
}
}
});
});
};
});
当然,如果我们考虑到生产目的,还有更多的复杂情况:
- 我们可以添加一个CSS注释语法,允许开发者排除某些选择器块,使它们不添加弹性滚动。
- 我们可能想要允许选项参数,使得这个插件只在x轴或y轴上自动化弹性滚动。
- 我们需要在index.test.js上工作,以便我们可以确保这通过任何代码更新都能继续工作。
但是,总的来说,我们相当快地就组装了一个工作插件。希望你能够利用这个走查,将来能够自己组装出你的PostCSS插件!