天天看點

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

說起前端自動化建構,相信做過前端的小夥伴們都不會陌生,可能第一感覺就會想到webpack。但是,其實webpack本質意義上應該是一個強大的子產品打包器,以入口檔案為起點,結合檔案間各種引用關系,将各種複雜的檔案最終打包成一個或多個浏覽器可識别的檔案。是以說,webpack更大意義上是一個子產品打包器,而非自動化建構工具。今天我們來介紹的是一款強大的自動化建構工具gulp

什麼是自動化建構

自動化建構簡單了解就是将源代碼轉化為生産環境代碼的過程。

它的出現,省去了我們很大一部分人工的重複性工作,一定程度的提升了我們的開發效率

常用的自動化建構工具

  • Grunt
  • Gulp
  • FIS

差別

  • grunt 和 gulp本身更像一個建構平台,而實際完成建構需要借助各種插件來實作各個具體的建構任務。故gurnt和gulp之間其實是可以互相轉化得,即能用grunt完成得事情,用gulp也能完成,能用gulp完成的事情,用grunt同樣能完成。
  • grunt 任務的建構是基于臨時檔案完成的,也就是說,grunt去解析一個檔案時,會先讀取這個檔案,然後經過插件處理後,先寫入到一個臨時檔案中,然後另一個插件做下一步處理時,會去讀取這個臨時檔案中的内容,然後經過插件處理後,再寫入到另一個臨時檔案中,直到全部處理完成,再寫入到目标檔案中(生産代碼)。故可以看出,grunt的每一步任務的建構,都會伴随磁盤的讀寫。故其建構速度會比較慢。故現在用的人也少了
  • gulp 任務的建構是基于記憶體完成的,也就是說,gulp解析一個檔案是以檔案流的形式,先讀取檔案的檔案流,寫入到記憶體中,然後經過中間各種插件處理,最終才寫入到目标檔案中(生産代碼)。故gulp一個任務的建構過程,隻有第一步和最後一步是設計到磁盤讀寫的,其他中間環節都是在記憶體中完成,故其建構速度會非常快。故gulp應該是目前最主流的自動化建構工具
  • FIS 百度團隊推出的自動化建構工具,大而全,內建了很多功能,更容易上手。但現在沒怎麼維護了,用的人也非常少了

初識Gulp

gulp工作原理

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

以上圖檔是對gulp工作原理很好的一個解讀。gulp主要工作原理就是将檔案讀取出來,然後中間經過一系列的處理,最終轉換成我們生産環境所需要的内容,然後寫入到目标檔案中。

而這個過程中最重要的就是gulp的管道pipe(),gulp就是利用pipe()來實作一個流程到下一個流程的過渡。詳情請看代碼

const fs = require('fs')

const stream = (done) => {
  const readStream = fs.createReadStream('package.json') // 讀取流,讀取檔案
  const writeStream = fs.createWriteStream('temp.txt') // 寫入流,寫入檔案
  const transform = new Transform({
    transform: (chunk, encoding, callback) => {
      // 這裡可以對讀取的流進行各種轉換操作,具體如何轉換我就不寫了
    }
  }) // 轉換流
  return readStream // 讀取
    .pipe(transform) // 轉換
    .pipe(writeStream) // 寫入
    // return 讀取流 實際會調用readStream的end事件,告知結束任務
}
module.exports = {
  stream
}
           

如上,gulp核心工作原理就是這樣,通過pipe這樣一個管道将上一步處理完的東西傳遞給下一步進行處理。全部處理完成後,最終寫入目标檔案

gulp需要有一個gulpfile.js檔案,實作這些建構任務的代碼一般就寫在這個gulpfile.js檔案中,如以上代碼就是寫在gulpfile.js中的

但是,以上代碼我們是通過node.js原生實作的,實際讀取檔案,寫入檔案以及中間對檔案進行各種處理,gulp都給我們提供了各種插件以及方法,我們都可以直接安裝或者直接使用

gulp常用Api

  • src:建立讀取流,可直接src(‘源檔案路徑’) 來讀取檔案流
  • dest:建立寫入流,可直接dest(‘目标檔案路徑’) 來将檔案流寫入目标檔案中
  • parallel:建立一個并行的建構任務,可并行執行多個建構任務 parallel(‘任務1’,‘任務2’,‘任務3’,…)
  • series:建立一個串行的建構任務,會按照順序依次執行多個任務 series(‘任務1’,‘任務2’,‘任務3’,…)
  • watch:對檔案進行監視,當檔案發生變化時,可執行相關任務

    watch(‘src/assets/styles/*.scss’, 執行某個任務)

從0到1實作一個完整的自動化工作流

下面我們利用一個例子來從0到1實作一個完整的自動化工作流

首先,我們得準備一份開發時得源代碼

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

代碼目錄大家可以通過腳手架去生成

目錄介紹

1、public下存放不需要經過轉換得靜态資源

2、src下存放項目源檔案

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

3、assets下存放其他資源檔案,如,樣式檔案,腳本檔案,圖檔,字型等

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

下面,我們要利用gulp來實作一個自動化建構工作流,将這些檔案都能夠自動轉化為生産環境可用得資源檔案

目标

1、将html檔案轉化為html檔案,存放到dist下,并且處理html中得一些模闆解析,以及資源檔案得引入問題(如html檔案中引入了css,js 等)。并對html檔案進行壓縮處理

2、将scss檔案轉化為浏覽器可識别得css檔案,并壓縮

3、将js檔案轉化為js檔案,并處理js代碼中一些浏覽器無法識别得文法轉化為可識别得。如ES6.ES7轉ES5

4、将圖檔進行壓縮

5、将字型進行壓縮

6、實作一個開發伺服器,實作邊開發,邊建構

7、相關優化

8、封裝自動化工作流,将我們完成得gulpfile.js 封裝成一個公用子產品,便于後續其他類似項目可以直接按照這個子產品就可立即使用

開始實作

準備工作

按照gulp,并引入相關api

yarn add gulp --dev

在項目根目錄下建立gulpfile.js檔案,在檔案中引入gulp相關方法

1、建立相關得建構任務,并測試

建立樣式編譯任務

// 定義樣式編譯任務
const sass = require('gulp-sass') // 編譯scss檔案得

const scss = () => {
  return src('./src/assets/styles/main.scss', {base: 'src'}) // 讀取檔案
    .pipe(sass()) // sass編譯處理
    .pipe(dest('./dist')) // 寫入到dist檔案夾下
}

// 導出相關任務
module.exports = {
  scss
}
           

以上src方法中第二個參數 是為了指定基礎路徑。如果不指定,打包後則會丢失路徑,直接将打包後的css檔案放在dist目錄下。

如果指定了,就會将指定的目錄後面的目錄都保留下來,即 assets/styles/main.css

運作yarn gulp scss 運作建構任務

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

其他建構任務也都一樣建立

思路:

先建立不同類型檔案的編譯建構任務,将需要編譯的各個任務進行編譯建構,并一個個進行測試,確定建構沒問題

當然,編譯不同檔案需要用到不同的插件。故同時需要安裝相應的插件,并引入相關插件(引入的代碼我就不貼了)

  • 編譯scss 需要gulp-scss插件 (任務scss)
  • 編譯腳本 需要gulp-babel插件,同時需要安裝@babel/core,gulp-babel的作用主要就是去調用@babel/core插件,

    同時為了能夠轉換ES6及以上新特性代碼,還需要安裝@babel/preset-env插件,用于轉換新特性 (任務script)

  • 編譯html 需要gulp-swig插件,用于傳入模闆所需要的資料 (任務html)
  • 編譯image圖檔以及font字型檔案,需要 gulp-imagemin插件,用于對圖檔和字型進行壓縮 (任務image和font)
  • 建立其他不需要編譯的檔案的建構任務,不需要編譯的就直接拷貝到目标路徑中 (任務copy)

    附上以上6個任務代碼

// html模闆中需要的資料
const data = {
  menus: [
    {
      name: 'Home',
      icon: 'aperture',
      link: 'index.html'
    },
    {
      name: 'About',
      link: 'about.html'
    },
    {
      name: 'Contact',
      link: '#',
      children: [
        {
          name: 'Twitter',
          link: 'https://twitter.com/w_zce'
        },
        {
          name: 'About',
          link: 'https://weibo.com/zceme'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/zce'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}
// 定義樣式編譯任務
const scss = () => {
  return src('./src/assets/styles/*.scss', { base: 'src' })
    .pipe(sass())
    .pipe(dest('./dist'))
}

// 定義腳本編譯任務
const script = () => {
  return src('./src/assets/scripts/*.js', { base: 'src'})
    .pipe(babel({ presets: ['@babel/preset-env'] })) // 指定babel去解析ECMAScript新特性代碼
    .pipe(dest('./dist'))
}

// 定義html模闆編譯任務
const html = () => {
  return src('./src/**/*.html', { base: 'src' })
    .pipe(swig({ data })) // 指定html模闆中的資料
    .pipe(dest('./dist'))
}

// 定義圖檔編譯任務
const image = () => {
  return src('./src/assets/images/**', { base: 'src' })
    .pipe(imagemin())
    .pipe(dest('./dist'))
}

// 定義字型編譯任務
const font = () => {
  return src('./src/assets/fonts/**', { base: 'src' })
    .pipe(imagemin())
    .pipe(dest('./dist'))
}

// 定義其他不需要經過編譯的任務
const copy = () => {
  return src('./public/**', { base: 'public' })
    .pipe(dest('./dist'))
}

module.exports = { scss, script, html, image, font, copy }
           

然後運作yarn gulp 任務名 來運作建構任務進行測試

這裡說明下,html任務中傳入的data,因為html源檔案中用到了模闆引擎,裡面用到了相關資料,故我們解析時,需要傳入相關的資料

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

2、合并任務

因以上6個任務在建構過程中戶不影響,故可以進行并行建構,故此時,我們可以利用gulp提供的parallel方法來建立一個并行任務

但在建立任務之前,我們可以把任務進行分類,前面5個為都需要進行編譯的任務,我們可以先合并為一個compile任務。然後再用這個compile任務

和copy任務并行合并為一個新的任務build

// 因以上任務都是需要編譯的任務,且工作過程互相不受影響,故可以并行執行,故将以上5個任務合并成一個并行任務
const compile = parallel(scss, script, html, image, font)

// 将需要編譯的任務和不需要進行編譯的任務合并為一個建構任務
const build = parallel(compile, copy)
           

下面我們測試一下

運作 yarn gulp build

一文讓你學會封裝自己的前端自動化建構工作流(gulp)
一文讓你學會封裝自己的前端自動化建構工作流(gulp)

可以看到,相關任務,就都被打包了

3、任務初步優化

1、 每次建構時,都會把建構後的檔案寫入到dist目錄下,那麼我們是不是要在每次寫入dist之前,将dist目前清空一下會比較好啊,可以防止多餘無用代碼的出現

怎麼做:新增del子產品,可以用于幫我們删除指定目錄下的檔案(yarn add del --dev)

const del = require('del')
// 定義清除目錄下的檔案任務
const clean = () => {
  return del(['dist'])
}
           

此時,我們需要将新增的這個clean任務加入到建構流程中,此時,我們要想,我們是不是希望在其他任務将檔案寫入dist之前去清除dist目錄下的檔案啊

那麼,此時,clean任務是不是就得在其他建構任務之前去執行啊。是以此時,我們需要将原來得build任務,串行加上一個clean任務

// 合并建構任務
const build = series(clean, parallel(compile, copy))
           

2、我們之前安裝了很多gulp插件(gulp-開頭得插件),每次我們新安裝一個,就得引入一次,如果以後插件多了,是不是就會有很多插件得引用啊,此時我們可以借助gulp得另一個插件來解決這個問題gulp-load-plugins, 此插件會幫我們加載gulp下得所有插件,故我們隻需要引入這個插件後,就可以直接通過這個插件,拿到gulp下得所有插件,下面,我們來修改一下代碼,前面插件得引入,我們就不需要了

const loadPlugins = require('gulp-load-plugins')
 const plugins = loadPlugins()

  // 定義樣式編譯任務
  const scss = () => {
    return src('./src/assets/styles/*.scss', { base: 'src' })
      .pipe(plugins.sass())
      .pipe(dest('./dist'))
  }

  // 定義腳本編譯任務
  const script = () => {
    return src('./src/assets/scripts/*.js', { base: 'src'})
      .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
      .pipe(dest('./dist'))
  }

  // 定義html模闆編譯任務
  const html = () => {
    return src('./src/**/*.html', { base: 'src' })
      .pipe(plugins.swig({ data }))
      .pipe(dest('./dist'))
  }

  // 定義圖檔編譯任務
  const image = () => {
    return src('./src/assets/images/**', { base: 'src' })
      .pipe(plugins.imagemin())
      .pipe(dest('./dist'))
  }

  // 定義字型編譯任務
  const font = () => {
    return src('./src/assets/fonts/**', { base: 'src' })
      .pipe(plugins.imagemin())
      .pipe(dest('./dist'))
  }
           

4、起一個開發伺服器

下面,我們開始起一個開發伺服器,完成開發時邊開發邊建構的功能

起一個開發伺服器需要用到插件browser-sync

安裝browser-sync插件 yarn add browser-sync

引入插,并建立一個開發伺服器

const browserSync = require('browser-sync')
// 建立一個開發伺服器
const bs = browserSync.create()

const serve = () => {
  bs.init({
    notify: false, // 關閉頁面打開時browser-sync的頁面提示
    port: 2080, // 設定端口
    server: {
      baseDir: 'dist', // 設定開發伺服器的根目錄,會取此目錄下的檔案運作
      routes: {
        '/node_modules': 'node_modules' // 解決dist後的檔案直接引入node_modules下檔案的問題
      }
    }
  })
}
           

上面說一下routes選項

主要是指定打包後,html檔案中直接引入的node_modules下的封包件的問題,告知開發伺服器直接去根目錄下的node_modules檔案夾下面找對應的檔案

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

此時,我們開發伺服器已經起了一個了,并告知了伺服器去取dist下的檔案作為運作檔案。但是此時,還會有問題,那就是,如果dist下的檔案發生了變化後,我們的開發伺服器是無法得知的,此時我們需要配置一個files屬性,來對dist下的檔案進行監視。

const serve = () => {
  bs.init({
    notify: false, // 關閉頁面打開時browser-sync的頁面提示
    port: 2080, // 設定端口
    files: 'dist/**', // 監聽dist下所有檔案
    server: {
      baseDir: 'dist', // 設定開發伺服器的根目錄,會取此目錄下的檔案運作
      routes: {
        '/node_modules': 'node_modules' // 解決dist後的檔案直接引入node_modules下檔案的問題
      }
    }
  })
}
           

此時,我們已經可以監聽dist下的檔案了。

5、開發伺服器優化

雖然我們現在能對dist下的檔案進行監視了,但是,依然是無法實作開發過程中,頁面能即時響應的目的的。因為我們開發過程中修改的是源代碼,而不是dist下的代碼。那如何實作呢。繼續往下看

5.1 監聽建構前的源檔案,保證開發過程中能夠實作修改代碼後,頁面立刻得到相應

實作方式:利用gulp自帶的watch子產品對src下的源檔案進行監聽,源檔案發生變化時,重新執行對應的建構任務,那麼會重新建構,建構後,dist下的檔案就會發生變化,serve通過files屬性就能監聽到

const serve = () => {
  // watch監聽相關源檔案
  watch('src/assets/styles/*.scss', scss)
  watch('src/assets/scripts/*.js', script)
  watch('src/*.html', html)
  watch('src/assets/images/**', image)
  watch('src/assets/fonts/**', font)
  watch('public/**', copy)

  bs.init({
    notify: false,
    port: 2080,
    files: 'dist/**',
    server: {
      baseDir: 'dist',
      routes: {
        '/node_modules': 'node_modules'
      }
    }
  })
}
           

5.2 進一步優化,上面我們已經實作了開發過程中,修改檔案頁面能即時響應。但是我們上面6個watch監聽了6類檔案,每類檔案發生變化後,我們都重新執行了對應的建構任務。

我們試想,在開發過程中,我們隻需要當檔案發生變化時,頁面能即時響應就行了,像html,scss,js等檔案,需要編譯成浏覽器可識别的檔案我們才能看到頁面發生變化,故每次這類檔案發生變化時,我們都去啟動對應的任務重新建構一次這無可厚非。但是,像圖檔,字型以及不需要編譯的靜态檔案。我們隻需要看到變化就行了,有必要調用對應建構任務嗎,像圖檔,字型,都是對它們進行了壓縮,但我們實際開發階段,這個完全沒必要。

故,我們對這類開發階段不需要處理的檔案做個特殊處理。

5.2.1 我們在監聽圖檔,字型,和public下的靜态檔案時,不再啟動對應的建構任務,而是直接調用browserSync的reload()方法去重新加載頁面

那麼此時,我們開發伺服器要拿到這些檔案是不是就不能在dist下拿了啊,因為我們沒有重新建構,故dist下不會有改變後的檔案。

此時,我們修改baseDir的根目錄為一個數組[‘dist’, ‘src’, ‘public’]。那麼,伺服器會優先去dist下找檔案,如果找不到,會依次去src和public目錄下尋找。像圖檔,字型,以及相關靜态檔案,開發伺服器是不是就會去src和public下去加載啊

const serve = () => {
  // watch監聽相關源檔案
  watch('src/assets/styles/*.scss', scss)
  watch('src/assets/scripts/*.js', script)
  watch('src/**/*.html', html)
  // watch('src/assets/images/**', image)
  // watch('src/assets/fonts/**', font)
  // watch('public/**', copy)
  watch(
    [
      'src/assets/images/**',
      'src/assets/fonts/**',
      'public/**'
    ],
    bs.reload
  )

  bs.init({
    notify: false,
    port: 2080,
    files: 'dist/**',
    server: {
      baseDir: ['dist', 'src', 'public'],
      routes: {
        '/node_modules': 'node_modules'
      }
    }
  })
}
           

5.3 有一個容易忽略的問題,我們上面serve伺服器是以dist下的檔案為跟目錄,也就是伺服器啟動,會預設去取dist目錄下的檔案,如果找不到,就會去取src和public下的檔案。那如果重來沒有執行過build指令,那麼dist下是不是空的啊,這麼一來,像樣式檔案,js檔案,html檔案,他都會取src下面找,那找到的檔案能運作嗎,是不是不能啊。是以,我們需要建立一個develop任務,此任務在啟動serve前,先執行一次compile任務。

// 因以上任務都是需要編譯的任務,且工作過程互相不受影響,故可以并行執行,故将以上5個任務合并成一個并行任務
const compile = parallel(scss, script, html)

// 合并建構任務
const build = series(clean, parallel(compile, copy, image, font))

// 開發建構任務
const develop = series(compile, serve)
           

我們建立了一個develop任務,讓起串行先執行compile和serve

同時,我們修改了一下compile任務,将image和font任務放入到build中了,這樣我們develop中便不需要執行這兩個任務了

5.4 上面我們說過,serve伺服器是通過files屬性去監聽dist目錄下的檔案變化來實作即時更新的。可是像上面的圖檔,字型以及靜态檔案,我們好像并沒有用到這個files屬性,也實作了浏覽器的實時更新吧。那我們其他檔案,是不是也可以這樣呢。對的,也可以這樣,具體用法,見下面代碼

// 定義樣式編譯任務
const scss = () => {
  return src('./src/assets/styles/*.scss', { base: 'src' })
    .pipe(plugins.sass({ outputStyle: 'expanded' }))
    .pipe(dest('./dist'))
    .pipe(bs.reload({ stream: true })) // 建構任務每次執行後,都reload一次
}

// 定義腳本編譯任務
const script = () => {
  return src('./src/assets/scripts/*.js', { base: 'src'})
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('./dist'))
    .pipe(bs.reload({ stream: true })) // 建構任務每次執行後,都reload一次
}

// 定義html模闆編譯任務
const html = () => {
  return src('./src/**/*.html', { base: 'src' })
    .pipe(plugins.swig({ data }))
    .pipe(dest('./dist'))
    .pipe(bs.reload({ stream: true })) // 建構任務每次執行後,都reload一次
}
           

以上添加了一個stream:true,意思是重新加載不需要進行讀寫操作,而是直接以流的方式往浏覽器中推

好了,開發伺服器的優化就到這了

下面,我們繼續來優化生産環境的建構build

6、build的建構任務的優化

6.1 上面我們說過,serve伺服器中配置了一個routes,是因為建構後的html檔案引入了一些外部的資源檔案,我們去處理那些資源檔案了。

但是,build環境中,這些檔案可能就找不到了,因為dist下沒有node_modules檔案夾,那麼我們建構的時候該如何去處理這種建構後的資源引用問題呢

首先,我們可以看下建構後的html

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

可以看出,這種資源檔案,建構後,會生成對應的build注釋,辨別了後續可将兩個注釋中間的部分合并成為一個新的檔案(vendor.css)。那麼如何處理這種情況呢。

gulp提供了一種叫useref的插件來處理這種情況,他會将注釋中間引用的資源合并成為一個新的資源檔案

安裝 gulp-useref (yarn add gulp-useref --dev)

建立任務用此插件去處理這種情況

const useref = () => {
  return src('dist/*html', { base: 'dist' }) // 讀取的是建構後的檔案,故是dist下
    .pipe(plugins.useref({ searchPath: ['dist', '.']})) // 請求的資源路徑去哪找
    .pipe(dest('dist'))
}
           

上面的searchPath 是指定建構時,請求的資源檔案去什麼地方找,如上圖中的main.css,我們可以直接在dist下找,如果找不到,那麼我們去目前根目錄下找,故配置了第二個 ‘.’ 這個 . 就代表目前根目錄 。比如上面的bootstrap.css 就會去根目錄下找,找到後,直接将引入的這個css打包進dist下,并合并成vendor.css 。

這個合并,可能你們不大了解,看下圖 ,你們就了解了

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

這個注釋中間引入了3個檔案,那麼都會被打包成vendor.js一個檔案。同時會将注釋删除

此時,其實還會有點問題,大家可以看到讀取檔案是從dist下去讀取,寫入檔案又是寫入到dist下面,這其實會産生沖突,從同一個地方又讀又寫,是不是有問題啊。

此時,我們可以通過一個中間檔案來進行一個過度。如何過度,請看6.2

6.2 我們可以在建構的時候,可以先讓他建構到一個中間目錄中,比如temp,然後useref再去temp中去讀檔案,讀取後,再通過useref插件進行處理,然後再寫入到dist中。那麼我們原來的建構任務的寫入路徑就都要改了。但是這個隻針對html,style,js 因為useref是處理引入的html以及js,css等資源路徑的

// 定義樣式編譯任務
const scss = () => {
  return src('./src/assets/styles/*.scss', { base: 'src' })
    .pipe(plugins.sass({ outputStyle: 'expanded' }))
    .pipe(dest('./temp')) // 改成temp
    .pipe(bs.reload({ stream: true })) // 建構任務每次執行後,都reload一次
}

// 定義腳本編譯任務
const script = () => {
  return src('./src/assets/scripts/*.js', { base: 'src'})
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('./temp')) // 改成temp
    .pipe(bs.reload({ stream: true })) // 建構任務每次執行後,都reload一次
}

// 定義html模闆編譯任務
const html = () => {
  return src('./src/**/*.html', { base: 'src' })
    .pipe(plugins.swig({ data }))
    .pipe(dest('./temp')) // 改成temp
    .pipe(bs.reload({ stream: true })) // 建構任務每次執行後,都reload一次
}

const useref = () => {
  return src('temp/*html', { base: 'temp' }) // 改成從temp下去讀取檔案流
    .pipe(plugins.useref({ searchPath: ['dist', '.']})) // 改成從temp下去讀取檔案流
    .pipe(dest('dist')) // 寫入到dist
}

// 定義清除目錄下的檔案任務
const clean = () => {
  return del(['dist', 'temp']) // 添加清除temp
}
           

然後修改建構流程,将useref放到compile之後再執行,同時,我們建構完以後,是不是還要将temp目錄給清除啊,因為他隻是個臨時目錄

// 清除temp
const cleanTemp = () => {
  return del('temp')
}

// 合并建構任務
const build = series(clean, parallel(series(compile, useref, cleanTemp), copy, image, font))
           

6.3 檔案壓縮

前面我們利用useref建構的html,css,js等是不是還沒有給他進行壓縮處理啊,我們build任務一般是打包線上代碼,那麼這些檔案肯定都是要進行壓縮的。那麼如何壓縮呢

當然是針對不同的檔案利用不同的插件進行壓縮了

html 使用插件gulp-htmlmin yarn add gulp-htmlmin --dev

js 使用插件gulp-uglify yarn add gulp-uglify --dev

css 使用插件cleanCss yarn add gulp-clean-css --dev

同時,我們知道useref任務中是一個讀取流可能讀取到不同類型的檔案(html或css或js),是以,我們還需要一個gulp-if插件來做判斷

const useref = () => {
  return src('temp/*html', { base: 'temp' }) // 讀取的是建構後的檔案,故是dist下
    .pipe(plugins.useref({ searchPath: ['dist', '.']})) // 請求的資源路徑去哪找
    .pipe(plugins.if(/\.js$/, plugins.uglify()))  // 壓縮腳本檔案
    .pipe(plugins.if(/\.css$/, plugins.cleanCss())) // 壓縮樣式檔案
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
      collapseWhitespace: true, // 壓縮html
      minifyCss: true, // 壓縮html檔案中的内嵌樣式
      minifyJs: true // 壓縮html檔案中内嵌的js
    })))
    .pipe(dest('dist'))
}
           

6.4 導出相關指令

上面我們一般都是隻暴露了develop 和 build兩個任務,但一般還有個clean任務,我們也是比較常用的,我們将這個任務也單獨導出

// 導出相關任務
module.exports = {
  clean,
  build,
  develop
}
           

導出後,我們可以在package.json檔案中去配置相關指令,以便我們更友善去執行我們的指令

"scripts": {
  "clean": "gulp clean",
  "build": "gulp build",
  "develop": "gulp develop"
}
           

此時,我們可以直接通過yarn build去進行項目建構了

整個建構流程基本已經完成了。

下面我們來附上gulpfile.js完整代碼

// 實作這個項目的建構任務

// 引入相關依賴

const { src, dest, parallel, series, watch } = require('gulp')
const del = require('del')
const browserSync = require('browser-sync')

const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()
// 建立一個開發伺服器
const bs = browserSync.create()
// const sass = require('gulp-sass')
// const babel = require('gulp-babel')
// const swig = require('gulp-swig')
// const imagemin = require('gulp-imagemin')

// 定義html模闆需要得資料
const data = {
  menus: [
    {
      name: 'Home',
      icon: 'aperture',
      link: 'index.html'
    },
    {
      name: 'About',
      link: 'about.html'
    },
    {
      name: 'Contact',
      link: '#',
      children: [
        {
          name: 'Twitter',
          link: 'https://twitter.com/w_zce'
        },
        {
          name: 'About',
          link: 'https://weibo.com/zceme'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/zce'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}

/* 定義相關建構任務 */

// 定義樣式編譯任務
const scss = () => {
  return src('./src/assets/styles/*.scss', { base: 'src' })
    .pipe(plugins.sass({ outputStyle: 'expanded' }))
    .pipe(dest('./temp'))
    .pipe(bs.reload({ stream: true })) // 建構任務每次執行後,都reload一次
}

// 定義腳本編譯任務
const script = () => {
  return src('./src/assets/scripts/*.js', { base: 'src'})
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('./temp'))
    .pipe(bs.reload({ stream: true })) // 建構任務每次執行後,都reload一次
}

// 定義html模闆編譯任務
const html = () => {
  return src('./src/**/*.html', { base: 'src' })
    .pipe(plugins.swig({ data }))
    .pipe(dest('./temp'))
    .pipe(bs.reload({ stream: true })) // 建構任務每次執行後,都reload一次
}

// 定義圖檔編譯任務
const image = () => {
  return src('./src/assets/images/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('./dist'))
}

// 定義字型編譯任務
const font = () => {
  return src('./src/assets/fonts/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('./dist'))
}

// 定義其他不需要經過編譯的任務
const copy = () => {
  return src('./public/**', { base: 'public' })
    .pipe(dest('./dist'))
}

// 定義清除目錄下的檔案任務
const clean = () => {
  return del(['dist', 'temp'])
}

// 清除temp
const cleanTemp = () => {
  return del('temp')
}

// 初始化開發伺服器
const serve = () => {
  // watch監聽相關源檔案
  watch('src/assets/styles/*.scss', scss)
  watch('src/assets/scripts/*.js', script)
  watch('src/**/*.html', html)
  // watch('src/assets/images/**', image)
  // watch('src/assets/fonts/**', font)
  // watch('public/**', copy)
  watch(
    [
      'src/assets/images/**',
      'src/assets/fonts/**',
      'public/**'
    ],
    bs.reload
  )

  bs.init({
    notify: false,
    port: 2080,
    // files: 'dist/**',
    server: {
      baseDir: ['dist', 'src', 'public'],
      routes: {
        '/node_modules': 'node_modules'
      }
    }
  })
}

const useref = () => {
  return src('temp/*html', { base: 'temp' }) // 讀取的是建構後的檔案,故是dist下
    .pipe(plugins.useref({ searchPath: ['dist', '.']})) // 請求的資源路徑去哪找
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
      collapseWhitespace: true, // 壓縮html
      minifyCss: true, // 壓縮html檔案中的内嵌樣式
      minifyJs: true // 壓縮html檔案中内嵌的js
    })))
    .pipe(dest('dist'))
}

// 因以上任務都是需要編譯的任務,且工作過程互相不受影響,故可以并行執行,故将以上5個任務合并成一個并行任務
const compile = parallel(scss, script, html)

// 合并建構任務
const build = series(clean, parallel(series(compile, useref, cleanTemp), copy, image, font))

// 開發建構任務
const develop = series(compile, serve)

// 導出相關任務
module.exports = {
  clean,
  build,
  develop
}

           

但是,此時,我們發現沒有,我們寫了這麼多,隻是用于處理了這一個項目的建構任務,但是我們肯定是希望我們所寫的這些東西,能夠作為和目前項目結構相似的一類項目的自動化建構工具。那麼,最好的辦法是不是将這個gulpfile.js封裝成一個子產品,然後釋出到npm上面去啊。

那麼以後人家需要使用的時候,是不是可以直接通過按照這個子產品,就立馬可以進行項目建構了啊。下面我們就來封裝一下這個工作流

自動化建構工作流封裝

1、首先,我們需要建立一個node_modules包。包名我們定為cgp-build

我這裡使用了一個腳手架工具(caz)生成node_modules包的一些基礎目錄

我們先全局安裝這個腳手架

yarn global add caz

運作caz nm cgp-build生成我們的包的基本目錄

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

這個包中,lib下的index.js就是我們這個包的入口檔案(一般包的入口檔案都是lib下的index.js檔案,而cli指令檔案的入口檔案一般是bin下的cli.js或者index.js)

也就是說,我們原來寫在gulpfile.js中的代碼,現在要放到lib/index.js中來,這裡當别人執行這個包時,才會執行到這些具體的建構代碼

2、将gulpfile.js中的代碼拷貝到index.js中來

此時,gulpfile.js這個依賴了很多插件,是以這些插件都會被作為我們封裝得這個包得生産依賴。故我們需要把之前那個打包項目中得package.json中devDependencies都拷貝到我們這個包目錄中得package.json檔案中的dependencies中

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

那麼此時,後面有項目安裝了我們這個cgp-build的時,就會自動安裝這個包所依賴的這些插件。

3、提取項目中的資料

此時,還有問題,我們往上去看gulpfile.js中的代碼,發現在解析html時,是不是傳入了一個data資料啊。而data我們是直接定義在gulpfile.js中的。但是我們都知道,這個data資料,是不是項目的資料啊,不同的項目可能這個資料就不一樣了,可能有的項目html檔案中還沒有這種模闆資料。是以說,這個data,是不是應該提到項目中去啊。那麼提到哪呢。

我們知道,很多項目中 是不是都有config.js檔案啊,比如vue的vue.config.js。那麼我們是不是也可以定義一個config檔案啊,比如就叫page.config.js。那麼用我們這個cgp-build進行自動化建構的項目都需要建立一個page.config.js檔案,那我們是不是可以把這個data放到config檔案中,當作配置資料傳入啊。

而此時,我們lib/index.js檔案中,我們就可以通過引入這個config.js檔案中的配置,然後在建構的時候再使用這個配置資料

那麼: 如何拿到項目目錄下的config.js檔案呢

我們分析下:我們這個包,最終是會被安裝在項目目錄的node_modules檔案夾下的

那麼我們這個包中的lib/index 相當于是在項目目錄(我們假設項目目錄是page-demo)下的node_models/cgp-build/lib/index.js 。

那麼我們不是拿到了項目的根目錄,就能拿到項目中的page.config.js檔案啊。

node,js提供了一個全局api process.cwd() 可以擷取到目前項目根目錄

// 擷取根目錄
const cwd = process.cwd()
let config = {} // 定義配置檔案,這裡面可能會有些預設配置

try {
  const loadConfig = require(`${cwd}/page.config.js`) // 擷取項目目錄中的配置檔案
  config = Object.assign({}, config, loadConfig) // 合并config和loadConfig
} catch (err) {
  throw err
}

// 定義html模闆編譯任務
const html = () => {
  return src('./src/**/*.html', { base: 'src' })
    .pipe(plugins.swig({ data: config.data })) // 修改為config中的data
    .pipe(dest('./temp'))
    .pipe(bs.reload({ stream: true })) // 建構任務每次執行後,都reload一次
}
           

那麼,page.config.js的配置檔案,格式如下

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

現在,資料問題解決了,但是很多檔案的路徑我們是不是還是寫死的啊

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

像這種路徑,不同項目,是不是可能不一樣啊,是以我們這樣寫死,也是不合理的,也應該抽象到page.config.js的配置中去

4、抽象路徑

我們先在/lib/index.js中寫入一份預設配置,當項目中配置了相關配置後,會覆寫index.js中的預設配置

// 擷取根目錄
const cwd = process.cwd()
let config = {
  build: {
    src: 'src',
    dist: 'dist',
    temp: 'temp',
    public: 'public',
    paths: {
      styles: 'assets/styles/*.scss',
      scripts: 'assets/styles/*.js',
      pages: '*.html',
      images: 'assets/images/**',
      fonts: 'assets/fonts/**'
    }
  }
} // 定義配置檔案,這裡面可能會有些預設配置
           

然後将index.js中的路徑都用config變量去代替

// 定義樣式編譯任務
const scss = () => {
  return src(config.build.paths.styles, { base: config.build.src, cwd: config.build.src })
    .pipe(plugins.sass({ outputStyle: 'expanded' }))
    .pipe(dest(config.build.temp))
    .pipe(bs.reload({ stream: true })) // 建構任務每次執行後,都reload一次
}

// 定義腳本編譯任務
const script = () => {
  return src(config.build.paths.scripts, { base: config.build.src, cwd: config.build.src })
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest(config.build.temp))
    .pipe(bs.reload({ stream: true })) // 建構任務每次執行後,都reload一次
}

// 定義html模闆編譯任務
const html = () => {
  return src(config.build.paths.pages, { base: config.build.src, cwd: config.build.src })
    .pipe(plugins.swig({ data: config.data })) // 修改為config中的data
    .pipe(dest(config.build.temp))
    .pipe(bs.reload({ stream: true })) // 建構任務每次執行後,都reload一次
}

// 定義圖檔編譯任務
const image = () => {
  return src(config.build.paths.images, { base: config.build.src, cwd: config.build.src })
    .pipe(plugins.imagemin())
    .pipe(dest(config.build.dist))
}

// 定義字型編譯任務
const font = () => {
  return src(config.build.paths.fonts, { base: config.build.src, cwd: config.build.src })
    .pipe(plugins.imagemin())
    .pipe(dest(config.build.dist))
}

// 定義其他不需要經過編譯的任務
const copy = () => {
  return src('**', { base: config.build.public, cwd: config.build.public })
    .pipe(dest(config.build.dist))
}

// 定義清除目錄下的檔案任務
const clean = () => {
  return del([config.build.dist, config.build.temp])
}

// 清除temp
const cleanTemp = () => {
  return del(config.build.temp)
}

// 初始化開發伺服器
const serve = () => {
  // watch監聽相關源檔案
  watch(config.build.paths.styles, {cwd: config.build.src}, scss)
  watch(config.build.paths.scripts, {cwd: config.build.src}, script)
  watch(config.build.paths.pages, {cwd: config.build.src}, html)
  // watch('src/assets/images/**', image)
  // watch('src/assets/fonts/**', font)
  // watch('public/**', copy)
  watch(
    [
      config.build.paths.images,
      config.build.paths.images,
      `${config.build.public}/**`
    ],
    bs.reload
  )

  bs.init({
    notify: false,
    port: 2080,
    // files: 'dist/**',
    server: {
      baseDir: [config.build.dist, config.build.src, config.build.public],
      routes: {
        '/node_modules': 'node_modules'
      }
    }
  })
}

const useref = () => {
  return src(config.build.paths.pages, { base: config.build.temp, cwd: config.build.temp }) // 讀取的是建構後的檔案,故是dist下
    .pipe(plugins.useref({ searchPath: [config.build.temp, '.']})) // 請求的資源路徑去哪找
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
      collapseWhitespace: true, // 壓縮html
      minifyCss: true, // 壓縮html檔案中的内嵌樣式
      minifyJs: true // 壓縮html檔案中内嵌的js
    })))
    .pipe(dest(config.build.dist))
}
           

我們在上面很多地方加了個cwd選項,是因為我們抽象出來的路徑,去掉了src,是以我們需要通過cwd去指定去哪個目錄下找這個路徑

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

5、包裝gulp-cli

下面我們要包裝一下我們自己的cli指令,為什麼要包裝呢,因為gulp建構時,預設是找gulpfile.js檔案的,而我們現在是放在/bin/index.js中,對于項目而言,這個檔案在/node_modules/cgp-build/lib/index.js中,是以在項目中運作yarn gulp build是會報錯的,報錯,找不到gulpfile.js檔案

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

此時,我們需要手動去指定gulpfile.js檔案為哪個檔案

–cwd . 的意思是以目前項目目錄作為根目錄,因為gulp會預設以gulpfile.js檔案所在目錄為根目錄,是以我們需要特别指定一下根目錄

那麼這麼弄,是不是很繁瑣啊,每次我需要執行下建構任務時,都要輸入這麼一大串。此時,我們就可以自定義這個封包件自己的cli指令,将這些 --gulpfile --cwd等參數都內建到指令中去

如何定義cli

在封包件目錄下建立bin檔案夾,并在bin中建立cli.js。然後在package.json檔案中添加bin字段

一文讓你學會封裝自己的前端自動化建構工作流(gulp)
一文讓你學會封裝自己的前端自動化建構工作流(gulp)

cli.js檔案需要加個檔案頭 #!/usr/bin/env node(cli入口檔案都需要的)

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

這裡我解釋一下:一般包的cli指令檔案都是在包目錄下的bin目錄下,比如webpack,當你運作webpack main.js指令去打包main.js時,也是會先去找node_modules/webpack/bin/*.js檔案的

那麼,此時,我們cli.js 檔案中需要寫什麼呢,我們分析下

大家想啊,我們本質是要去執行gulp build --gulpfile …這種指令,

隻是我們先去執行了我們自己的cli指令 cgp-build,那cgp-build執行後,去找了/bin/cli.js檔案後,我們是不是隻需要在這裡去執行gulp的建構指令就可以了啊,那執行gulp的建構指令本質上是不是去執行gulp/bin/**.js檔案啊。是以此時,我們隻需要在我們的cli.js檔案中去運作gulp/bin/gulp.js檔案就行了

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

這樣一來,當我們執行cgp-build build時,實際上就會執行gulp build指令

但這樣還不夠啊,我們前面是不是說了啊,我們需要攜帶參數去查找gulpfile.js檔案以及指定根目錄啊。此時,我們可以借助全局方法process.argv 這個可以拿到的其實就是參數清單,是個數組,如:–gulpfile /node_modules/cgp-build/lib/index.js 數組中就是[’–gulpfile’, ‘/node_modules/cgp-build/lib/index.js’]

那麼,我們可以通過push方法往參數中添加參數

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

此時,整個cli的封裝就完成了。

npm送出

npm送出我們上篇文章已經說過了,這裡就随便提一下了,

1、将包上次至開源庫,如github

2、npm publish 或者 yarn publish上傳至npm庫中

送出完後,我們測試下

先在本地準備一個項目目錄gulp-demo,裡面放入我們之前那個項目

然後安裝我們送出至npm的包 cgp-build

yarn add cgp-build --dev

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

然後運作yarn cgp-build build 或者 cgp-build build

一文讓你學會封裝自己的前端自動化建構工作流(gulp)

可以看出,是沒有問題的,正常打包成功

好了,自動化建構就寫到這了。喜歡請點個贊,謝謝