gulp進階-自定義gulp插件

gulp已經成爲不少項目的標配了,gulp的插件生態也十分繁榮,截至2015.1.5,npm上已經有10190款gulp插件供咱們使用。咱們徹底能夠傻瓜式地搭起一套構建。 css

然而,咱們常常會遇到一種狀況,咱們好不容易按照文檔傳入對應的參數調用了插件,卻發現結果不如預期,這時候咱們就要一點點去排錯,這就要求咱們對gulp插件的工做原理有必定的瞭解。本文以實現一個gulp插件爲例,講解一下gulp插件是如何工做的。 html

需求描述

一般,咱們的構建資源爲js/css/html以及其它的一些資源文件,在開發或發佈階段,js/css會通過合併,壓縮,重命名等處理步驟。 node

有些場景下,咱們不能肯定通過構建後生成js/css的名稱或者數量,如此就不能在HTML文件中寫死資源的引用地址,那麼該如何實現一個Gulp的插件用以將最終生成的資源文件/地址注入到HTML中呢? git

假設咱們須要實現的插件是這樣使用方式: github

<html>
<head>
    <!--InlineResource:\.css$-->
</head>
<body>
    <!--InlineResource:\.js$-->
</body>
</html>

咱們經過一個HTML註釋用以聲明須要依賴的資源,InlineResource 是匹配的關鍵詞,":"作爲分割,/*.css$/,/*.js$/ 是聲明要依賴的文件的正則匹配。 正則表達式

在gulpfile.js咱們須要這邊配置: shell

gulp.task('dist', function () {
    return gulp.src('index.html')
               .pipe(InjectResources(
                    gulp.src(['*.js', '*.css'])
                        .pipe(hash(/*添加MD5做爲文件名*/))
               ))
               .pipe(gulp.dest('dist'))
})

這裏簡單介紹下其中的一些方法與步驟: npm

  • gulp.src('index.html') 會讀取文件系統中當前目錄下的index.html,並生成一個可讀的Stream,用於後續的步驟消費 gulp

  • InjectResources(stream) 是咱們將要實現的插件,它接受一個參數用以獲取要注入到HTML中的JS/CSS,此參數應該是一個 Stream 實例,用生成一個Stream實例,用於接收並處理上一步流進來的數據 api

  • hash(options) 是一個第三方插件,用於往當前流中的文件名添加md5串,如:gulp-hash

  • gulp.dest('dist') 用於將注入資源後的HTML文件生成到當前目錄下

咱們要關心的是第2點:如何接全部的資源文件並完成注入?

咱們能夠將該邏輯分紅4個步驟

  1. 獲取全部的js/css資源
  2. 獲取全部的HTML文件
  3. 定位HTML中的依賴聲明
  4. 匹配所依賴的資源
  5. 生成並注入依賴的資源標籤

在開編以前,咱們須要依賴一個重要的第三方庫:map-stream

map-stream 用於獲取當前流中的每個文件數據,而且修改數據內容。

步驟1 (JS/CSS資源)

module.exports = function (resourcesStream) {
    // step 1: TODO => 這裏要獲取全部的js/css資源
}

資源流會做爲參數的形式傳給InjectResources方法,在此經過一個異步的實例方法獲取全部的文件對象,放到一個資源列表:

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方法,第二個參數能夠用於替換當前處理的文件對象

到此,咱們就完成了第一步的封裝啦!

module.exports = function (resourcesStream) {
    // step 1:
    function getResources () {
        ...
    }
}

步驟2 (HTML文件)

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 (定位依賴)

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中的資源依賴:

html.replace(/<!--InlineResource:(.*?)-->/g, function (expr, fileRegexpStr){
    // fileRegexp是用以匹配依賴資源的正則字符串
})

到此,咱們完成了資源依賴的定位,下一步將是獲取所依賴的資源用以替換。

步驟4 (依賴匹配)

咱們將經過步驟1定義的 getResources 方法獲取所需的資源文件:

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 的回調方法中

根據依賴聲明中的正則表達式,對資源列表一一匹配:

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 (資源轉換/依賴注入)

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內置模塊。

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中的依賴聲明(注入):

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中,例如:

  • <!--InjectResources:*\.js$??inline-->
  • 支持命名空間,用於往同一個資源流中使用屢次資源注入的區分,例如:


  • gulp.src('index.html')
          .pipe(
              InjectResources(gulp.src('asserts/*.js'), { name: 'asserts'})
          )
          .pipe(
              InjectResources(gulp.src('components/*.js'), { name: 'components'})
          )
          ...


  • . . .

相關文章
相關標籤/搜索