gulp已經成為很多專案的標配了,gulp的外掛生態也十分繁榮,截至2015.1.5,npm上已經有10190款gulp外掛供我們使用。我們完全可以傻瓜式地搭起一套構建。
然而,我們經常會遇到一種情況,我們好不容易按照文件傳入對應的引數呼叫了外掛,卻發現結果不如預期,這時候我們就要一點點去排錯,這就要求我們對gulp外掛的工作原理有一定的瞭解。本文以實現一個gulp外掛為例,講解一下gulp外掛是如何工作的。
需求描述
通常,我們的構建資源為js/css/html以及其它的一些資原始檔,在開發或釋出階段,js/css會經過合併,壓縮,重新命名等處理步驟。
有些場景下,我們不能確定經過構建後生成js/css的名稱或者數量,如此就不能在HTML檔案中寫死資源的引用地址,那麼該如何實現一個Gulp的外掛用以將最終生成的資原始檔/地址注入到HTML中呢?
假設我們需要實現的外掛是這樣使用方式:
1 2 3 4 5 6 7 8 |
<html> <head> <!--InlineResource:\.css$--> </head> <body> <!--InlineResource:\.js$--> </body> </html> |
我們通過一個HTML註釋用以宣告需要依賴的資源,InlineResource 是匹配的關鍵詞,”:”做為分割,/*.css$/,/*.js$/ 是宣告要依賴的檔案的正則匹配。
在gulpfile.js我們需要這邊配置:
1 2 3 4 5 6 7 8 |
gulp.task('dist', function () { return gulp.src('index.html') .pipe(InjectResources( gulp.src(['*.js', '*.css']) .pipe(hash(/*新增MD5作為檔名*/)) )) .pipe(gulp.dest('dist')) }) |
這裡簡單介紹下其中的一些方法與步驟:
- gulp.src(‘index.html’) 會讀取檔案系統中當前目錄下的index.html,並生成一個可讀的Stream,用於後續的步驟消費
- InjectResources(stream) 是我們將要實現的外掛,它接受一個引數用以獲取要注入到HTML中的JS/CSS,此引數應該是一個 Stream 例項,用生成一個Stream例項,用於接收並處理上一步流進來的資料
- hash(options) 是一個第三方外掛,用於往當前流中的檔名新增md5串,如:gulp-hash
- gulp.dest(‘dist’) 用於將注入資源後的HTML檔案生成到當前目錄下
我們要關心的是第2點:如何接所有的資原始檔並完成注入?
我們可以將該邏輯分成4個步驟
- 獲取所有的js/css資源
- 獲取所有的HTML檔案
- 定位HTML中的依賴宣告
- 匹配所依賴的資源
- 生成並注入依賴的資源標籤
在開編之前,我們需要依賴一個重要的第三方庫:map-stream
map-stream 用於獲取當前流中的每一個檔案資料,並且修改資料內容。
步驟1 (JS/CSS資源)
1 2 3 |
module.exports = function (resourcesStream) { // step 1: TODO => 這裡要獲取所有的js/css資源 } |
資源流會作為引數的形式傳給InjectResources方法,在此通過一個非同步的例項方法獲取所有的檔案物件,放到一個資源列表:
1 2 3 4 5 6 7 8 9 10 11 12 |
var resources = [] function getResources(done) { if (resources) return done(resources) // 由於下面的操作是非同步的,此處要有鎖... resourcesStream.pipe(mapStream(function (data, cb) { resources.push(data) cb(null, data) })) .on('end', function () { done(resources) }) } |
- mapStream的處理方法中獲取到的data是由gulp.src生成的vinyl物件,代表了一個檔案
- 每一個stream都會在接受後丟擲end事件
Note: mapStream的處理方法中的cb方法,第二個引數可以用於替換當前處理的檔案物件
到此,我們就完成了第一步的封裝啦!
1 2 3 4 5 6 |
module.exports = function (resourcesStream) { // step 1: function getResources () { ... } } |
步驟2 (HTML檔案)
1 2 3 4 5 6 7 8 |
module.exports = function (resourcesStream) { // step 1: ✔︎ // step 2: TODO => 獲取當前流中的所有目標HTML檔案 return mapStream(function (data, cb) { }) } |
InjectResources外掛方法會返回一個Writable Stream例項,用於接收並處理流到InjectResources的HTML檔案,mapStream的返回值就是一個writable stream。
此時,mapStream的處理方法拿到的data就是一個HTML檔案物件,接下來進行內容處理。
步驟3 (定位依賴)
1 2 3 4 5 6 7 8 9 10 |
module.exports = function (resourcesStream) { // step 1: ✔︎ // step 2: ✔ return mapStream(function (data, cb) { var html = data.contents.toString() // step 3: TODO => 獲取HTML中的資源依賴宣告 }) } |
我們拿到的data是一個vinyl物件,contents屬性是檔案的內容,型別可能是Buffer也可能是String, 通過toStraing()後可以獲取到字串內容。
所有的依賴宣告都有InlineResource關鍵詞,簡單點的做法,可以通過正則來定位並替換HTML中的資源依賴:
1 2 3 |
html.replace(/<!--InlineResource:(.*?)-->/g, function (expr, fileRegexpStr){ // fileRegexp是用以匹配依賴資源的正則字串 }) |
到此,我們完成了資源依賴的定位,下一步將是獲取所依賴的資源用以替換。
步驟4 (依賴匹配)
我們將通過步驟1定義的 getResources 方法獲取所需的資原始檔:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
module.exports = function (resourcesStream) { // step 1: ✔︎ // step 2: ✔ return mapStream(function (data, cb) { // step 3: ✔ getResources(function (list) { html.replace(depRegexp, function (expr, fileRegexpStr) { var fileRegexp = new RegExp(fileRegexpStr) // step 4: TODO => 獲取匹配的依賴 }) }) }) } |
由於 getResources 是非同步方法,因此需要把替換處理邏輯包裹在 getResources 的回撥方法中
根據依賴宣告中的正規表示式,對資源列表一一匹配:
1 2 3 4 5 6 7 8 9 10 |
function matchingDependences(list, regexp) { var deps = [] list.forEach(function (file) { var fpath = file.path if (fileRegexp.test(fpath)) { deps.push(fpath) } }) return deps } |
到此只差最後一步,將資源轉換為HTML標籤並注入到HTML中
步驟5 (資源轉換/依賴注入)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
module.exports = function (resourcesStream) { // step 1: ✔︎ // step 2: ✔ return mapStream(function (data, cb) { // step 3: ✔ // step 4: ✔ // ... html.replace(depRegexp, function (expr, fileRegexpStr) { var deps = matchingDependences(list, fileRegexpStr) // step 5: 檔案物件轉換為HTML標籤 }) }) } |
接下來的定義一個transform方法,用於將路徑列表轉換為HTML的資源標籤列表,其中引入了 path 模組用於解析獲取檔案路徑的一些資訊,該模組是node內建模組。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var path = require('path') function transform(deps) { return deps.map(function (dep) { var ext = path.extname(dep) switch (ext) { case 'js': '<script>' + dep + '</script>' break case 'css': return '<link rel="stylesheet" href="' + dep + '">' break } return '' }).join('') } |
最終,我們將標籤列表拼接為一個字串來HTML中的依賴宣告(注入):
1 2 3 4 5 6 7 8 9 |
html = html.replace(depRegexp, function (expr, fileRegexpStr) { var deps = matchingDependences(list, fileRegexpStr) // step 5: 檔案物件轉換為HTML標籤 return transform(deps) }) // html檔案物件 data.contents = new Buffer(html) // 把修改後的檔案物件放回HTML流中 cb(null, data) |
到此也就完整地實現了一個擁有基本注入功能的外掛~~~~~~
One More Thing
通過上面實現的示例步驟,可以清楚瞭解到gulp外掛的工作原理。 但要做一個易用/可定製性高的外掛,我們還要繼續完善一下,例如:
- 比較資源的路徑與HTML的路徑,輸出相對路徑作為預設的標籤資源路徑
- 提供 sort 選項方法用於修改資源的注入順序
- 提供 transform 選項方法用於定製標籤中的資源路徑
- 在依賴宣告中支援 inline 宣告,用以將資源內容內聯到HTML中,例如:
1<!--InjectResources:*\.js$??inline--> - 支援名稱空間,用於往同一個資源流中使用多次資源注入的區分,例如:
12345678gulp.src('index.html').pipe(InjectResources(gulp.src('asserts/*.js'), { name: 'asserts'})).pipe(InjectResources(gulp.src('components/*.js'), { name: 'components'}))...
-
…