本文原始来源:http://devework.com/postcss-p...。转载请提供原始来源,谢谢!javascript
前阵子为了知足工做上的一个需求开发了一个PostCSS 插件,后来也将这个插件提交给PostCSS 官方并获得承认。在这篇文章中笔者将记录开发过程当中遇到的一些问题,且斗胆将之称为“最佳实践”,但愿对有兴趣尝试PostCSS 插件开发的您有所帮助。css
首先先上成果:https://github.com/Jeff2Ma/postcss-lazyimagecss (欢迎给个star 哦~)html
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 了?java
因而笔者便在gulp-lazyimagecss 的基础上开发出了这么一个轮子。在此也感谢原开发者hzlzh 与littledu 的大力帮助与支持。对笔者而言,更像是站在巨人的肩膀上开发出来这个插件。node
关于PostCSS 的原理,官方有这么一个图:git
简单解释,PostCSS 会将上一步传入的 CSS 按照一条条样式规则(rule)进行解析(Parser)获得一个节点树;而后借助一系列插件在节点树上进行转换操做,并最终经过Stringifier 进行拼接。source map
则记录了先后的对应关系。github
固然,在实际的开发中其实没必要深究原理,最重要的是看其提供的API 来调用便可。npm
开发一个PostCSS 插件也是开发一个Node 模块,想到后面要发布到NPM 跟PostCSS 官方,那么做为一个开源项目的可维护性、可扩展性也是很重要的。所以在进入正式的开发以前,笔者作了以下的工做:json
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 作了一点微小的工做;也但愿对看到文末的您有所帮助,积极参与到开源创做的事业中。
参考文章:
http://ai.github.io/postcss-way/