從0到1:PostCSS 插件開發最佳實踐

本文原始來源:http://devework.com/postcss-p...。轉載請提供原始來源,謝謝!javascript

clipboard.png

前陣子爲了知足工做上的一個需求開發了一個PostCSS 插件,後來也將這個插件提交給PostCSS 官方並獲得承認。在這篇文章中筆者將記錄開發過程當中遇到的一些問題,且斗膽將之稱爲「最佳實踐」,但願對有興趣嘗試PostCSS 插件開發的您有所幫助。css

簡介篇

開發成果展現

首先先上成果:https://github.com/Jeff2Ma/postcss-lazyimagecss (歡迎給個star 哦~)html

postcss-lazyimagecss 插件實現的功能是爲 CSS 中的background-image 對應的圖片自動添加widthheight 屬性。簡單形象化的效果展現以下:前端

/* 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 的基礎上開發出了這麼一個輪子。在此也感謝原開發者hzlzhlittledu 的大力幫助與支持。對筆者而言,更像是站在巨人的肩膀上開發出來這個插件。node

準備篇

原理

關於PostCSS 的原理,官方有這麼一個圖:git

clipboard.png

簡單解釋,PostCSS 會將上一步傳入的 CSS 按照一條條樣式規則(rule)進行解析(Parser)獲得一個節點樹;而後藉助一系列插件在節點樹上進行轉換操做,並最終經過Stringifier 進行拼接。source map則記錄了先後的對應關係。github

固然,在實際的開發中其實沒必要深究原理,最重要的是看其提供的API 來調用便可。npm

工欲善其事必先利其器

開發一個PostCSS 插件也是開發一個Node 模塊,想到後面要發佈到NPM 跟PostCSS 官方,那麼做爲一個開源項目的可維護性、可擴展性也是很重要的。所以在進入正式的開發以前,筆者作了以下的工做:json

一、配置 editorconfig

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 代碼質量監控及進行單元測試任務。有效保障了整個開發過程當中的質量。

clipboard.png

三、託管到 Github 並配置Travis-ci 持續集成

整個開發過程使用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 上展現出結果:

clipboard.png

開發篇

從最小開始

一個PostCSS 插件最基礎的構成以下:

var postcss = require('postcss');
module.exports = postcss.plugin('PLUGIN_NAME', function (opts) {
    opts = opts || {};
    // 傳入配置相關的代碼
    return function (root, result) {
        // 轉化CSS 的功能代碼
    };
});

而後就是不一樣的需求狀況來決定是否引入第三方模塊,是否有額外配置項,而後在包含root,result 的匿名函數中進行最爲核心的轉換代碼功能編寫。

root(css),rule, nodes, decl, prop, value

如本文一開頭的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 屬性與值,如上面 propbackground-image,valueurl(../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」 深感認同,所以在基本完成插件功能後筆者又作了以下優化工做。

更友好的log 提示

官方實際上是建議用內置的result.warn來代替console.logconsole.warn來展現log 信息(緣由聽說是一些PostCSS 處理器會忽略這類console log 輸出)。不過筆者嘗試後發現官方函數下提示的信息會很是長,後面採用了藉助chalk 模塊封裝了console.log的形式增長了高亮態信息展現。

錦上添花

「找不到圖片文件」的場景處理

用戶在寫CSS 代碼的時候,background-image 的url 可能會有以下狀況:

  • 輸入的是目錄

  • 輸入的非圖片路徑

  • 輸入了一半就保存了

  • 根本就是瞎輸入

場景不少,但對於插件而言僅僅是可否找到與否的結果。在處理這些錯誤場景的狀況下也給出的細分到「File does not exist」 或 「Not a image file」的狀況,讓這類錯誤提醒更加友好一些。

提示二倍圖不正確

若是用戶引用的二倍圖(相似xxx@2x.png)的寬度高度爲非偶數的話,也會有相應的提醒。

以上的報錯提示在實際運行效果以下:

clipboard.png

英文版 README

PostCSS 官方建議是README.md用英文寫,其他語種採用相似README.zh.md的方式。

維護一份 changelog

按照建議,也將更新歷史等數據放在了一個名爲CHANGELOG.md文件上,並採用語義化的版本號

其它

根據本身的開發習慣,在Github 上的Repo 也放置了一份LICENSE 文件。

發佈篇

發佈到NPM 官方

發佈到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 官方

Postcss 官方主頁上有個plugin list 文件展現了全部的第三方插件,提交的話Fork 一份而後在該文件增長本身的插件詳細而後提交合並,等做者容許便可。

發佈到postcss.part

postcss.parts 是一個非官方的PostCSS 插件搜索平臺。提交本身插件可按照這個說明。其實本質也是Fork 而後加信息在Pull request 的方式,在此不累述。

結束篇

效果

在開發完postcss-lazyimagecss 插件後,筆者按照上面的發佈方式提交了給官方。後面效果還不錯,PostCSS 做者也提了個star 跟issue。PostCSS 官方推特上的推薦也帶來了第一批Stargazers。

clipboard.png

由於這個緣故,在第三屆中國CSS 大會上也有幸與PostCSS 做者ai 大神勾搭了下,並獲得了大神贈送的俄羅斯巧克力。

思考

在筆者看來,PostCSS 的做爲一個CSS 轉換引擎,其不參與細分功能實現僅交於第三方插件的設計理念,讓其產生了一個很是的開放的生態。但對於個開放機制下的一些狀況筆者並非很贊同,如一些用中文寫CSS 的插件(固然這個更可能是for fun),一些自定義CSS 屬性如用size: 10px 2px 等代替width/height的插件——在筆者看來PostCSS 插件應該更多在聽從CSS 標準語法的基礎上進行擴展。

但不管如何,仍是挺佩做者開發出了這麼個造福前端屆的工具;也由於認同做者,筆者寫了這篇文章爲推廣PostCSS 作了一點微小的工做;也但願對看到文末的您有所幫助,積極參與到開源創做的事業中。

參考文章:

http://ai.github.io/postcss-way/

https://github.com/postcss/po...

https://css-tricks.com/want-m...

相關文章
相關標籤/搜索