郭培

PostCSS深入学习:创建自己的插件

原文链接: webdesign.tutsplus.com

原文链接

PostCSS深入学习:创建自己的插件

这是PostCSS深入学习系列中的一篇,上一篇是PostCSS深入学习:多样的插件

到目前为止是PostCSS欣欣向荣的插件生态系统使得PostCSS如此惊艳,我确信你也已经认识到了这一点。这里有很多很好的插件,随着时间的推移,未来会涌现出更多优秀的插件,主要的原因是PostCSS开发插件对于有一些JavaScript开发经验的人来说非常容易。

开发PostCSS插件,不需要特别的许可;如果你想开发一个,那就立马去做吧。通过这种自由的状态,你将有能力使CSS的开发流程逐步演变成你所希望的样子,更不必说快速增长的PostCSS社区,使你有机会与其他成员分享你的工作。

在这个教程里你将学习如何开发一个基本的PostCSS插件。我们不会插件在API上讲太多,也不会使用任何超级的编码。我本身是一个前端开发者,我的JavaScript技能水平属于前端开发人员的基本水平,然而这并没有阻止我,我在仅仅一两个小时里就完成了我的第一个PostCSS插件。

参与其中亲自看看,你才能知道PostCSS插件开发有多么的容易!

我们将要创建什么

我们要创建一个插件,这个插件可以通过下面的语法轻松的在 font-family 声明中插入一组字体。

h1 {
    font-family: "Open Sans", fontstack("Arial");
}

经过编译,上面的代码将被转变成:

h1 {
    font-family: "Open Sans", Arial, "Helvetica Neue", Helvetica, sans-serif;
}

建立一个工作的项目

虽然我们是在创建自己的插件,但是仍然需要先创建一个空的Gulp或Grunt项目。在整个系列文章中,我们一直在使用他人创建的插件,现在我们就以同样的方式在项目中载入自己的插件。

你可以阅读前面关于如何为PostCSS创建Gulp或Grunt项目的教程:

如果你不想从零开始手动创建项目,你可以下载附于本教程的源文件,下载后提取Gulp或Grunt开始项目到一个空项目文件夹。然后打开终端或命令行窗口指向这个文件夹,执行npm install命令。

创建一个基础的插件壳

在“node_modules”中创建一个文件夹命名为 “postcss-myplugin”。常见的命名方式是使用postcss-前缀,明确插件是PostCSS插件。

所有的PostCSS插件都是node的模块,所以我们需要在node_modules中新增一个文件夹。打开一个终端或命令行窗口,指向这个新创建的文件夹,执行npm init。按着终端中命令行的提示,你就可以完成一个node模块的初始化,“entry point”设置保留默认的“index.js”。

完成node模块的初始化后,保持你的终端始终指向文件夹,然后执行npm install postcss --save命令。这样就会安装PostCSS做为该模块的依赖,这些都是开发PostCSS插件需要做的。

在“postcss-myplugin” 文件夹中创建一个文件,命名为“index.js”,添加此代码加载postcss模块。

var postcss = require('postcss');

在它的下面添加这个基本的包装器,用来包围我们的插件处理代码:

var postcss = require('postcss');
module.exports = postcss.plugin('myplugin', function myplugin(options) {
    return function (css) {
    options = options || {};
        // Processing code will be added here
  }
});

载入新插件

现在我们在项目中载入新创建的插件。到目前为止插件没有任何功能,我们只是在适当的位置进行设置来获取它。

通过Gulp载入插件

如果你在使用Gulp,在文件中添加下面的变量:

var myplugin = require('postcss-myplugin');

现在在processors 数组中添加新的变量名:

var processors = [
      myplugin
  ];

通过执行 gulp css命令做个快速测试,验证是否一切运行正常。检查项目的“dest”文件夹中是否出现了一个新的“style.css”文件。

通过Grunt载入插件

如果你使用Grunt,更新processors 对象,该对象嵌套在options 对象中,如下:

processors: [
          require('postcss-myplugin')()
]

通过执行 grunt postcss 命令做个快速测试,验证是否一切运行正常。检查项目的“dest”文件夹中是否出现了一个新的“style.css”文件。

添加插件功能

添加测试CSS

为我们的插件添加处理代码之前,我们先来给插件影响的样式表中添加一些测试代码。

在“src/style.css”文件中添加这些CSS:

h1 {
    font-family: "Open Sans", fontstack("Arial");
}

我们的插件没有做任何事情,如果现在编译你的CSS,你将看到同样的代码直接拷贝到了“dest”文件夹中的 “style.css”文件中。

循环规则和声明

现在开始扫描我们的CSS文件,我们要找到fontstack()的实例并处理它们。在options = options || {};行后添加下面的代码,开始进行扫描:

css.walkRules(function (rule) {
      rule.walkDecls(function (decl, i) {

      });
});

在第一行使用walkRules() 遍历你的CSS规则;从根本上说,规则是指选择器和大括号之间的样式。在你的测试CSS中,一条规则将是:

h1 {
    font-family: "Open Sans", fontstack("Arial");
}

然后在每条规则中,使用 walkDecls()遍历每个声明;声明本质上是每行的样式。在上面的CSS中,声明指的是:

font-family: "Open Sans", fontstack("Arial");

检查fontstack()语法的使用

使用刚才添加的代码遍历声明,decl表示当前的声明,通过decl.propdecl.value分别表示声明的属性和声明的值。

对于我们的CSS示例,decl.prop提供给我们font-familydecl.value提供给我们"Open Sans", fontstack("Arial")

我们想要检查样式表中的每个decl.value是否包含fontstack(字符串。如果包含我们就知道有人在试图使用我们的插件添加一组字体到他们的CSS。

walkDecl()循环中添加:

var value = decl.value;
if (value.indexOf( 'fontstack(' ) !== -1) {
    console.log("found fontstack");
}

首先我们获取decl.value存在变量value中。decl.value的任何变化都将发送到已编译的样式表中;我们将其内容存储在变量value中,所以我们可以与它玩玩儿。

于是我们使用 indexOf() 方法在我们的value变量中查找fontstack(字符串,如果发现,我们将在控制台中打印“found fontstack”。那么,我们来验证一下是否一切运行正常。

在你的终端或命令提示符中执行 gulp cssgrunt postcss,你应该会看到输出“found fontstack”。

定义字体组

现在,我们的插件能够在样式表中找到fontstack() 的实例,接下来我们就可以把实例转换成一组字体,即字体名称的列表。 在我们转换之前,需要先定义这些字体组。

在文件的顶部,postcss变量下边创建一个变量命名成fontstacks_config。我们将这个变量指向一个键值对对象。

在对象中的每组键值对,键名应该是字体组中的第一个字体,即 'Arial'。用户通过这个字符串,指定他们想使用的这组字体,即fontstack("Arial")fontstack("Times New Roman")

每组键值对中的键值将是这组字体中所包含字体的完整列表 ,即'Arial, "Helvetica Neue", Helvetica, sans-serif'

使用 CSS Font Stack提供的字体,在fontstacks_config对象中添加两组键值对,一组是'Arial',一组是 'Times New Roman'。

fontstacks_config 变量如下:

// Font stacks from http://www.cssfontstack.com/
var fontstacks_config = {
    'Arial': 'Arial, "Helvetica Neue", Helvetica, sans-serif',
    'Times New Roman': 'TimesNewRoman, "Times New Roman", Times, Baskerville, Georgia, serif'
}

检查请求的字体组

当发现使用fontstack() 的实例时,我们首先要判断用户需要请求哪组字体,即用户将在括号中设置什么字符串。

比如,如果用户输入fontstack("Arial"),我们需要取出字符串Arial。原因是我们要使用这个字符串做为键名,在fontstacks_config对象中找出该键名对应的键值。

在之前添加的if语句中直接添加下面的代码,替换掉console.log("found fontstack");这一行:

// 通过匹配fontstack()括号中的字符串,得到需要请求的字体组
// 然后替换其中的双引号或单引号
var fontstack_requested = value.match(/\(([^)]+)\)/)[1].replace(/["']/g, "");

在这里,我们执行两个步骤来提取字体组的字符串。

首先我们使用 match()方法在值中找到两个括号中的字符串,会返回"Arial"'Arial'

我们只需要字体名称,不需要双引号或单引号,所以我们使用 replace()方法从字符串中去掉引号,仅保留没有引号的字符串,比如Arial

这个字符串保存在fontstack_requested变量中。

请求字体组首字母转换

我们要使用新创建的fontstack_requested变量在我们的fontstack_config 选项中查找一组字体。这里棘手的部分是键名大小字母敏感,所以我们输入arial 查找 Arial键时会失败。

为了解决这个问题,我们要实现字符串 "首字母大写",例如times new roman要转换成Times New Roman。我们可以通过一个简短的自定义函数来实现。

fontstacks_config变量下添加toTitleCase()函数:

// 这个函数值得称赞 http://stackoverflow.com/questions/196972/convert-string-to-title-case-with-javascript/196991#196991
function toTitleCase(str) {
  return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
}

现在我们使用 fontstack_requested变量来调用这个函数。在创建fontstack_requested变量的这一行,添加此代码:

// 字体名称首字母大写转换
fontstack_requested = toTitleCase(fontstack_requested);

fontstack_requested变量凭借我们的toTitleCase()函数,更新它的值。

在配置中查找一组字体

fonstack_requested变量设置正确了,现在我们就可以使用它来查找相应的一组字体了。我们只需要在这一行下面插入此代码:

// 在fontstack_config对象中查找需要的字体
var fontstack = fontstacks_config[fontstack_requested];

这个用来在fontstacks_config对象中查找键名匹配fontstack_requested包含字符串,所对应的键值。

比如,如果fontstack_requested包含Arial字符串,就会在fontstacks_config 中找到Arial键,并返回Arial键对应的值'Arial, "Helvetica Neue", Helvetica, sans-serif'

然后,这个返回的值被保存到了fontstack变量中。

检查fontstack()前设置的字体

现在我们已经检索到了一组字体的字符串,接下来我们就要在CSS中插入这些字符串,在这之前我们需要做一件事儿。回顾我们的测试代码,代码中包括 "Open Sans" 字体做为首选字体,一组字体做为兜底字体。我们还需要从该值中检索该字体的名称(译者注:该值指decl.value,该字体指Open Sans),以便我们把该字体添加到CSS中。

fontstack变量行下,添加这些代码:

// 查找并存储fontstack()之前的任意字体名
var first_font = value.substr(0, value.indexOf('fontstack('));

这个代码使用 substr()方法查找从value的开始(用0表示)到fontstack()实例(使用 indexOf()定位)之间的内容。查找到内容都存到first_font变量中。

比始,在我们的测试代码中value 等于"Open Sans", fontstack("Arial"),所以first_font 变量值被设置成了"Open Sans",

创建新值

现在我们已经有了创建新值所需要的所有片段,该新值将用来替换测试代码中原始值"Open Sans", fontstack("Arial")

在最后添加的代码之后,插入这些代码:

// 通过合并first_font和fontstack变量创建一个新值
var new_value = first_font + fontstack;

在这里我们合并first_fontfontstack 变量为一个字体串,并存储到变量new_value中。

在我们的测试代码中,相当于合并"Open Sans",Arial, "Helvetica Neue", Helvetica, sans-serif

然后我们的new_value将保存字符串"Open Sans", 'Arial, "Helvetica Neue", Helvetica, sans-serif'

现在我们要添加到处理后样式表中的完整值为:

font-family: "Open Sans", fontstack("Arial"); 转变为... font-family: "Open Sans", 'Arial, "Helvetica Neue", Helvetica, sans-serif';

将新值发送给样式表

现在我们要把新值插入到处理后的样式表中,我们需要做的就是更新decl.value。PostCSS会处理其余的事情,为我们添加新值到处理后的CSS中。

在最后一行后添加这些代码:

// 将新值返回样式表中
decl.value = new_value;

设置decl.value 等于我们new_value变量的内容。

测试我们的插件

插件已经准备好了。我们来尝试使用gulp cssgrunt postcss 编译样式(在终端指向项目目录,而不是插件目录)。

现在 “dest/style.css”文件应该显示完整的一组字体:

h1 {
    font-family: "Open Sans", Arial, "Helvetica Neue", Helvetica, sans-serif;
}

(可选项) 添加用户-可配置的字体组选项

你也许希望你的插件用户可以配置选项,就像你在这个系列中使用PostCSS插件时,可以设置选项一样。

我们想让用户可以配置fontstacks选项,要么添加额外的字体组,要么重新定义存在的字体组,比如:

fontstacks: {
  'Extra Stack': '"Extra Stack", "Moar Fonts", Extra, serif',
  'Arial': 'Arial, "Comic Sans"'
}

注意:这一步是可选的。如果你希望跳过这一步,插件仍然可以完美的工作,只是没有用户的配置项。

在我们的插件中,我们已经有了可以让用户配置选项的最重要部分。在module.exports行中,你会注意到已经传过来的options参数。

module.exports = postcss.plugin('myplugin', function (options) {

通过options参数,我们可以接收到用户设置的选项。

你也会看到我们有这一行:

options = options || {};

这是用来检查options没有值,或不存在时,把它设置成一个空对象。这个可以确保程序执行时,如果options 未定义也不会报错。

开始时,我们要在项目中安装Underscore.js,我们要使用它方便的extend()方法。运行此命令将它安装到你正在构建的插件中:

npm install underscore --save

现在在postcss变量下,加载Underscore到你的插件中,通过添加变量_的方式来请求它。

var postcss = require('postcss');
var _ = require('underscore');

接下来我们要做的就是取到我们之前在插件中已经创建的fontstacks_config对象,使用用户通过选项配制设置的任意一组字体来扩展它。

直接在options = options || {};行下添加此代码:

//使用插件选项中任意自定义的一组字体扩展默认的fontstacks_config
    fontstacks_config = _.extend(fontstacks_config, options.fontstacks);

用户设置的 fontstacks选项通过options.fontstacks表示。

通过使用Underscore的extend()方法,所有options.fontstacks中的字体都被添加到 fontstacks_config。在任何情况下,如果存在一个匹配的键名,options.fontstacks中的值就会覆盖fontstacks_config中的值。这里允许用户重新定义任何已经存在的一组字体。

在你的Gulpfile或Gruntfile中,设置fontstacks选项,传递新的一组字体以及重新定义已经存在的一组字体:

/* Gulpfile */
var processors = [
    myplugin({
        fontstacks: {
      'Extra Stack': '"Extra Stack", "Moar Fonts", Extra, serif',
      'Arial': 'Arial, "Comic Sans"'
    }
    })
];

/* Gruntfile */
 processors: [
    require('postcss-myplugin')({
        fontstacks: {
            'Extra Stack': '"Extra Stack", "Moar Fonts", Extra, serif',
      'Arial': 'Arial, "Comic Sans"'
    }
    })
]

现在添加一些额外的CSS到你的 “src/style.css”文件中,这样我们可以测试刚刚通过配置选项添加的这组新字体。

h2 {
    font-family: "Droid Sans", fontstack("Extra Stack");
}

重新编译CSS你应该会看到 'Arial' 这组字体现在有了不同的输出, 'Extra Stack'这组字体输出正确。

h1 {
    font-family: "Open Sans", Arial, "Comic Sans";
}

h2 {
  font-family: "Droid Sans", "Extra Stack", "Moar Fonts", Extra, serif;
}

完成的插件

就是这样!你们都做完了。你已经完成了你的第一个PostCSS插件。

这里是GitHub上的完整代码,可以把你的代码与它作比较以供参考。

让我们回顾一下

你刚刚创建了一个完整的PostCSS插件,我希望在你的大脑里有一些关于其他插件的灵感迸发。在你写CSS时也许会有一些小麻烦,或许现在你就可以提出自己的方案来解决这些小麻烦。或者也许当你跳出固有的思维模式真正思考CSS时会有一些意料之外的想法——很棒,现在亲自来添加它吧!

现在总结一下我们所涵盖的内容:

  • 建立一个Gulp或Grunt项目用于开发一个新的插件

  • 在项目中创建一个node模块,这个模块将成为你的插件

  • 在项目中加载你的新插件

  • 使用你的插件提供的语法添加测试CSS

  • 使用PostCSS API 提供的方法扫描样式表

  • 定位正的使用的插件语法

  • 写JavaScript,使用 PostCSS API为原始代码实现适当的变换(和/或添加),并添加到处理后的CSS中。

对于TypeScript用户

做为 PostCSS 5.0版其中的一部分,Jed Mao贡献了一套TypeScript definitions,通过在键入时提供自动完成和内嵌文档的方式,对于应对插件的开发有很大的帮助。

TypeScript definitions

如果你发现自己已经习惯于PostCSS插件的开发,这些真的值得并到你的工作流中去。虽然我自己也不是TypeScript能手,但是我打算无论如何都要投入TypeScript写码中,纯粹是因为这样我就可以使用这些功能了。

如果你想试试这个,你不需要在Windows中使用Visual Studio,你可以使用免费开源的 Visual Studio Code,它可以运行在Windows,Mac,Linux中,基于Electron创建,类似的还有 Atom Editor

以如何在你的项目中添加TypeScript definitions为例,你可以检出postcss-font-pack插件,开个分支然后在 Visual Studio Code中演示,看一下自动提示和嵌入文档是如何工作的。

PostCSS 深入学习: 总结

非常感谢大家参与PostCSS深入学习系列。希望大家像我享受创建它一样享受阅读它。更重要的是,我希望在日常的网页开发生活中,把PostCSS应用于工作的意识可以充满你的大脑。

PostCSS确实是前端世界中新增的一种令人不可思议的新方式,这种方式推进插件在CSS开发中的应用,在之前CSS开发中从未有过。现在可用的插件范围已经足以重塑一个人的日常工作流程,而这些仅用了几年时间。

应该说PostCSS还没真正火起来,如果有一天CSS开发者都离不开它了,或者至少都知道它了,那它才算真正步入正轨。随着越来越多的前端开发者加入,我们将看到更多的对于插件生态系统的贡献,增加新的插件和帮助构造现有的插件。

这些插件免费满足一个开发人员可以设想出来的任意类型的CSS变换,PostCSS的未来是非常耐人寻味的。我期待着成为其中的一部分,也希望与你一起一路同行!