前阵子为了满足工作上的一个需求开发了一个PostCSS 插件,后来也将这个插件提交给PostCSS 官方并得到认可。在这篇文章中笔者将记录开发过程中遇到的一些问题,且斗胆将之称为“最佳实践”,希望对有兴趣尝试PostCSS 插件开发的您有所帮助。
首先先上成果: https://github.com/Jeff2Ma/postcss-lazyimagecss (欢迎给个star 哦~)
postcss-lazyimagecss 插件实现的功能是为 CSS 中的 background-image
对应的图片自动添加 width
与 height
属性。简单形象化的效果展示如下:
/* Input ./src/index.css */ .icon-close { background-image: url(../slice/icon-close.png); //icon-close.png - 16x16 } .icon-new { background-image: url(../slice/icon-new@2x.png); //icon-new@2x.png - 16x16 } /* Output ./dist/index.css */ .icon-close { background-image: url(../slice/icon-close.png); width: 16px; height: 16px; } .icon-new { background-image: url(../slice/icon-new@2x.png); width: 8px; height: 8px; background-size: 8px 8px; }
开发这个PostCSS 插件的起因是原先工作流中使用的 gulp-lazyimagecss 插件在加入SourceMap 功能后运行不正常,多次尝试修复均告失败。后来笔者想到,PostCSS 本身天然支持SourceMap,那如果将这个功能开发成PostCSS 插件岂不是也完美支持SourceMap 了?
于是笔者便在 gulp-lazyimagecss 的基础上开发出了这么一个轮子。在此也感谢原开发者 hzlzh 与 littledu 的大力帮助与支持。对笔者而言,更像是站在巨人的肩膀上开发出来这个插件。
关于PostCSS 的原理,官方有这么一个图:
简单解释,PostCSS 会将上一步传入的 CSS 按照一条条样式规则(rule)进行解析(Parser)得到一个节点树;然后借助一系列插件在节点树上进行转换操作,并最终通过Stringifier 进行拼接。 source map
则记录了前后的对应关系。
当然,在实际的开发中其实不必深究原理,最重要的是看 其提供的API 来调用即可。
开发一个PostCSS 插件也是开发一个Node 模块,想到后面要发布到NPM 跟PostCSS 官方,那么作为一个开源项目的可维护性、可扩展性也是很重要的。因此在进入正式的开发之前,笔者做了如下的工作:
editorconfig 作为一套统一代码格式的解决方案,已经在团队不少项目中使用,其很好地解决了因为团队协作中因不同代码编辑器及不同的代码习惯产生的潜在风险。这里是 最终的配置文件 。
在整个开发插件过程前,笔者根据需求配了个基于Gulp 的开发工作流,主要配备如下功能(任务):
代码质量监控ESlint
优秀的开源代码必然是有着标准化的JavaScript 代码风格,因此在整个开发过程中借助ESlint 来严格控制自己的代码质量。 这里 是本项目的ESlint 配置文件。
var eslint = require('gulp-eslint'); gulp.task('lint', function () { return gulp.src(files) .pipe(eslint()) .pipe(eslint.format()) .pipe(eslint.failAfterError()); });
基础的CSS 转换
这个任务其实就是本PostCSS 插件实现的功能,之所以在开发过程中也要配置是为了下面的单元测试任务的调用。
单元测试
秉承TDD(测试驱动开发)的开发理念,单元测试的任务是必不可少的。
gulp.task('test', function () { return gulp.src('test/*.js', { read: false }) .pipe(mocha({ timeout: 1000000 })); });
watch 任务
gulp watch 任务是上面任务的集体调用,实现的功能是在开发过程中,每当按下保存键就自动运行ESlint 代码质量监控及进行单元测试任务。有效保障了整个开发过程中的质量。
整个开发过程使用Github 托管源代码并通过Travis-ci 持续集成。PostCSS 官方建议最低需要支持Node.js 0.12 的版本,所以整个Travis-ci 的配置文件如下:
sudo: false language: node_js node_js: - "0.12" - "4" - "5" - "6" - "stable" before_script: - npm install -g mocha
相应的在Travis-ci 管理后台配置push 操作作为动作钩子,这样每次有commit push 上去就会自动进行测试并在log 上展示出结果:
一个PostCSS 插件最基础的构成如下:
var postcss = require('postcss'); module.exports = postcss.plugin('PLUGIN_NAME', function (opts) { opts = opts || {}; // 传入配置相关的代码 return function (root, result) { // 转化CSS 的功能代码 }; });
然后就是不同的需求情况来决定是否引入第三方模块,是否有额外配置项,然后在包含root,result 的匿名函数中进行最为核心的转换代码功能编写。
如本文一开头的PostCSS 原理解析,CSS 文件在经过Parser 转化后的递归单个子单位可以归为如下:
root(css) :也是整个CSS 代码段,包含多个rule。
rule: 包含一个CSS class 范围内的代码段
.icon-close { background-image: url(../slice/icon-close.png); font-size: 14px; }
nodes: 代指rule 中 {}
中间的多个 decl 部分。
decl: 单行CSS ,即有属性与值的部分
background-image: url(../slice/icon-close.png);
prop,value
相应的CSS 属性与值,如上面 prop
为 background-image
, value
为 url(../slice/icon-close.png)
根据postcss-lazyimagecss 插件要实现的内容,涉及到CSS 转化的有如下情景:
增加 width 属性及获取到真实值
增加 height 属性及获取到真实值
二倍图情况下增加 background-size 属性并计算出值
结合上一小节,可以先写出如下简洁版伪代码:
css.walkRules(function (rule) { // 遍历所有 CSS rule.walkDecls(/^background(-image)?$/, function (decl) { // 遍历每条 CSS 规则,找出目标 rule // 一些传参等代码 nodes.forEach(function (node) { // 遍历其它 rules ... }); ... // 其它代码实现,如找出图片真实width 等 rule.append({prop: 'width', value: valueWidth}); // 在该decl 追加width 属性 }); });
接下来就是考虑不同情况增加一些逻辑判断:
判断url 中是否为网络地址或Base64 的data 形式: imageRegex.exec(value).indexOf('data:')
判断该rule 下是否已经有width 等属性,在nodes 循环中:
if (node.prop === 'width') { CSSWidth = true; }
判断2倍图图片宽高是否为偶数:
value.indexOf('@2x') > -1 && (info.width % 2 !== 0 || info.height % 2 !== 0
再具体的不再详述,完整的代码实现可以 见这里 。
postcss-lazyimagecss 插件使用了第三方模块 fast-image-size 来进行图片数据(文件类型、宽高)的获取,大大提高了开发效率。然而在寻找图片绝对路径的这个实现上还是绕了不少弯路。
插件的思路是需要获取CSS 中 background-image
属性对应值中 url()
的相对图片路径,以此来找到图片的绝对路径,之后用fast-image-size 模块获取到相应的数据。
然而在一些特殊情况并不能准确找到绝对路径。
在CSS 预处理器(如Less 或Sass)中,常借助 @import
来组件化CSS 代码,然而在层层 @import
下路径可能已经被产生变化。举个例子,有如下结构:
. ├── css ├── html ├── img │ └── icon.png └── scss ├── index.scss └── second └── _import.scss
上面的文件树中展示的 scss/index.scss
@import
了二级目录下的 _import.scss
,在 _import.scss
中有一个类需要用到 img/icon.png
。
因为同时也配置了local server(以上面的 ./
目录作为server 的根目录),那么在 url 中可以写成 ../../img/icon.png
或 ../img/icon.png
,甚至写成 ../../../../../img/icon.png
(N个 ../
)——这些情况下Sass 编译后的index.css 均可正常读取。原因相信也知道,因为root url的存在,上面的路径写法均相当于 /img/icon.png
。
在这个情况下于用户而言是感受不到错误的,但在插件中可就找不到真实绝对路径了。笔者对于这个情况是采用了如下方式进行解决:
借助Node.js 中的 fs.existsSync
函数检测绝对路径对应的文件是否存在。第一次为正常 fs.existsSync
,如果找到就跳出;如果没有则先对路径的字符串执行 replace('../', '');
然后再次执行 fs.existsSync
。如果两次均没有找到则在终端进行提示,但这种情况下并不会报错破坏进程的运行。
function fixAbsolutePath(dir, relative) { // find the first time var absolute = path.resolve(dir, relative); // check if is a image file var reg = //.(jpg|jpeg|png|gif|svg|bmp)/b/i; if (!reg.test(absolute)) { pluginLog('Not a image file: ', absolute); return; } if (!fs.existsSync(absolute) && (relative.indexOf('../') > -1)) { relative = relative.replace('../', ''); // find the second time absolute = path.resolve(dir, relative); } return absolute; }
不敢说这是一种最好的处理方式,但至少是一种可行的处理方式。
单元测试上采用Mocha 测试工具, should.js 做断言库。在笔者看来,结合TDD 进行开发,单元测试仅作为一种开发的辅助手段,规避开发过程中一些产生致命的报错。本文不展开如何写单元测试,具体实现可点击 这里 。
在Postcss 官方Github Repo,有一个 Plugin Guidelines 。对于其提倡的“Do one thing, and do it well” 深感认同,因此在基本完成插件功能后笔者又做了如下优化工作。
官方其实是建议用内置的 result.warn
来代替 console.log
或 console.warn
来展示log 信息(原因据说是一些PostCSS 处理器会忽略这类console log 输出)。不过笔者尝试后发现官方函数下提示的信息会非常长,后面采用了借助chalk 模块封装了 console.log
的形式增加了高亮态信息展示。
用户在写CSS 代码的时候, background-image
的url 可能会有如下情况:
输入的是目录
输入的非图片路径
输入了一半就保存了
根本就是瞎输入
场景很多,但对于插件而言仅仅是能否找到与否的结果。在处理这些错误场景的情况下也给出的细分到“File does not exist” 或 “Not a image file”的情况,让这类错误提醒更加友好一些。
如果用户引用的二倍图(类似xxx@2x.png)的宽度高度为非偶数的话,也会有相应的提醒。
以上的报错提示在实际运行效果如下:
PostCSS 官方建议是 README.md
用英文写,其余语种采用类似 README.zh.md
的方式。
按照建议,也将更新历史等数据放在了一个名为 CHANGELOG.md
文件上,并采用 语义化的版本号 。
根据自己的开发习惯,在Github 上的Repo 也放置了一份LICENSE 文件。
发布到NPM 官方的步骤在这里就不再详述。仅分享一个不错的版本号增加方式(告别packup.json 的手动改版本数字)。
npm version patch => z+1 npm version minor => y+1 && z=0 npm version major => x+1 && y=0 && z=0
与上文所讲的 语义化的版本号 相关,vX.Y.Z(主版本号.次版本号.修订号)三个选项分别对应三部分的版本号,每次运行命令会导致相应的版本号递增一,同时子版本号清零。记得运行上面命令前先将文件变动提交到git 上去。
之后运行 npm publish
命令即可。
Postcss 官方主页上有个 plugin list 文件展示了所有的第三方插件,提交的话Fork 一份然后在该文件增加自己的插件详细然后提交合并,等作者允许即可。
postcss.parts 是一个非官方的PostCSS 插件搜索平台。提交自己插件可按照这个 说明 。其实本质也是Fork 然后加信息在Pull request 的方式,在此不累述。
在开发完postcss-lazyimagecss 插件后,笔者按照上面的发布方式提交了给官方。后面效果还不错,PostCSS 作者也提了个star 跟 issue 。PostCSS 官方推特上的推荐也带来了第一批Stargazers。
因为这个缘故,在第三届中国CSS 大会上也有幸与PostCSS 作者ai 大神勾搭了下,并得到了大神赠送的俄罗斯巧克力。
在笔者看来,PostCSS 的作为一个CSS 转换引擎,其不参与细分功能实现仅交于第三方插件的设计理念,让其产生了一个非常的开放的生态。但对于个开放机制下的一些情况笔者并不是很赞同,如一些用中文写CSS 的插件(当然这个更多是for fun),一些自定义CSS 属性如用 size: 10px 2px
等代替 width/height
的插件——在笔者看来PostCSS 插件应该更多在遵从CSS 标准语法的基础上进行扩展。
但无论如何,还是挺佩作者开发出了这么个造福前端届的工具;也因为认同作者,笔者写了这篇文章为推广PostCSS 做了一点微小的工作;也希望对看到文末的您有所帮助,积极参与到开源创作的事业中。