前阵子为了满足工作上的一个需求开发了一个 PostCSS 插件,后来也将这个插件提交给 PostCSS 官方并得到认可。在这篇文章中笔者将记录开发过程中遇到的一些问题,且斗胆将之称为 "最佳实践",希望对有兴趣尝试 PostCSS 插件开发的您有所帮助。
首先先上成果: (欢迎给个 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 插件的起因是原先工作流中使用的 插件在加入 SourceMap 功能后运行不正常,多次尝试修复均告失败。后来笔者想到,PostCSS 本身天然支持 SourceMap,那如果将这个功能开发成 PostCSS 插件岂不是也完美支持 SourceMap 了?
于是笔者便在 的基础上开发出了这么一个轮子。在此也感谢原开发者 与 的大力帮助与支持。对笔者而言,更像是站在巨人的肩膀上开发出来这个插件。
关于 PostCSS 的原理,官方有这么一个图:
简单解释,PostCSS 会将上一步传入的 CSS 按照一条条样式规则(rule)进行解析(Parser)得到一个节点树;然后借助一系列插件在节点树上进行转换操作,并最终通过 Stringifier 进行拼接。
则记录了前后的对应关系。
- source map
当然,在实际的开发中其实不必深究原理,最重要的是看 来调用即可。
开发一个 PostCSS 插件也是开发一个 Node 模块,想到后面要发布到 NPM 跟 PostCSS 官方,那么作为一个开源项目的可维护性、可扩展性也是很重要的。因此在进入正式的开发之前,笔者做了如下的工作:
作为一套统一代码格式的解决方案,已经在团队不少项目中使用,其很好地解决了因为团队协作中因不同代码编辑器及不同的代码习惯产生的潜在风险。这里是 。
在整个开发插件过程前,笔者根据需求配了个基于 Gulp 的开发工作流,主要配备如下功能(任务):
优秀的开源代码必然是有着标准化的 JavaScript 代码风格,因此在整个开发过程中借助 ESlint 来严格控制自己的代码质量。 是本项目的 ESlint 配置文件。
- var eslint = require('gulp-eslint');
- gulp.task('lint', function () {
- return gulp.src(files)
- .pipe(eslint())
- .pipe(eslint.format())
- .pipe(eslint.failAfterError());
- });
这个任务其实就是本 PostCSS 插件实现的功能,之所以在开发过程中也要配置是为了下面的单元测试任务的调用。
秉承 TDD(测试驱动开发)的开发理念,单元测试的任务是必不可少的。
- gulp.task('test',
- function() {
- return gulp.src('test/*.js', {
- read: false
- }).pipe(mocha({
- timeout: 1000000
- }));
- });
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 转化后的递归单个子单位可以归为如下:
- .icon-close {
- background-image: url(../slice/icon-close.png);
- font-size: 14px;
- }
中间的多个 decl 部分。
- {}
- background-image: url(../slice/icon-close.png);
相应的 CSS 属性与值,如上面
为
- prop
,
- background-image
为
- value
- url(../slice/icon-close.png)
根据 postcss-lazyimagecss 插件要实现的内容,涉及到 CSS 转化的有如下情景:
结合上一小节,可以先写出如下简洁版伪代码:
- 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 属性
- });
- });
接下来就是考虑不同情况增加一些逻辑判断:
- imageRegex.exec(value).indexOf('data:')
- if (node.prop === 'width') {
- CSSWidth = true;
- }
- value.indexOf('@2x') > -1 && (info.width % 2 !== 0 || info.height % 2 !== 0
再具体的不再详述,完整的代码实现可以 。
postcss-lazyimagecss 插件使用了第三方模块 来进行图片数据(文件类型、宽高)的获取,大大提高了开发效率。然而在寻找图片绝对路径的这个实现上还是绕了不少弯路。
插件的思路是需要获取 CSS 中
属性对应值中
- background-image
的相对图片路径,以此来找到图片的绝对路径,之后用 fast-image-size 模块获取到相应的数据。
- url()
然而在一些特殊情况并不能准确找到绝对路径。
在 CSS 预处理器(如 Less 或 Sass)中,常借助
来组件化 CSS 代码,然而在层层
- @import
下路径可能已经被产生变化。举个例子,有如下结构:
- @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
(N 个
- ../../../../../img/icon.png
)——这些情况下 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,有一个 。对于其提倡的 "Do one thing, and do it well" 深感认同,因此在基本完成插件功能后笔者又做了如下优化工作。
官方其实是建议用内置的
来代替
- result.warn
或
- console.log
来展示 log 信息(原因据说是一些 PostCSS 处理器会忽略这类 console log 输出)。不过笔者尝试后发现官方函数下提示的信息会非常长,后面采用了借助 chalk 模块封装了
- console.warn
的形式增加了高亮态信息展示。
- console.log
用户在写 CSS 代码的时候,
的 url 可能会有如下情况:
- background-image
场景很多,但对于插件而言仅仅是能否找到与否的结果。在处理这些错误场景的情况下也给出的细分到 "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 官方主页上有个 文件展示了所有的第三方插件,提交的话 Fork 一份然后在该文件增加自己的插件详细然后提交合并,等作者允许即可。
是一个非官方的 PostCSS 插件搜索平台。提交自己插件可按照这个 。其实本质也是 Fork 然后加信息在 Pull request 的方式,在此不累述。
在开发完 postcss-lazyimagecss 插件后,笔者按照上面的发布方式提交了给官方。后面效果还不错,PostCSS 作者也提了个 star 跟 。PostCSS 官方推特上的推荐也带来了第一批 Stargazers。
因为这个缘故,在第三届中国 CSS 大会上也有幸与 PostCSS 作者 ai 大神勾搭了下,并得到了大神赠送的俄罗斯巧克力。
在笔者看来,PostCSS 的作为一个 CSS 转换引擎,其不参与细分功能实现仅交于第三方插件的设计理念,让其产生了一个非常的开放的生态。但对于个开放机制下的一些情况笔者并不是很赞同,如一些用中文写 CSS 的插件(当然这个更多是 for fun),一些自定义 CSS 属性如用
等代替
- size: 10px 2px
的插件——在笔者看来 PostCSS 插件应该更多在遵从 CSS 标准语法的基础上进行扩展。
- width/height
但无论如何,还是挺佩作者开发出了这么个造福前端届的工具;也因为认同作者,笔者写了这篇文章为推广 PostCSS 做了一点微小的工作;也希望对看到文末的您有所帮助,积极参与到开源创作的事业中。
来源: