說起前端自動化構建,相信做過前端的小伙伴們都不會陌生,可能第一感覺就會想到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的管道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
const { src, dest, parallel, series, watch } = require('gulp')
- src:創建讀取流,可直接src(‘源檔案路徑’) 來讀取檔案流
- dest:創建寫入流,可直接dest(‘目標檔案路徑’) 來將檔案流寫入目標檔案中
- parallel:創建一個并行的構建任務,可并行執行多個構建任務 parallel(‘任務1’,‘任務2’,‘任務3’,…)
- series:創建一個串行的構建任務,會按照順序依次執行多個任務 series(‘任務1’,‘任務2’,‘任務3’,…)
- watch:對檔案進行監視,當檔案發生變化時,可執行相關任務
watch(‘src/assets/styles/*.scss’, 執行某個任務)
從0到1實作一個完整的自動化作業流
下面我們利用一個例子來從0到1實作一個完整的自動化作業流
首先,我們得準備一份開發時得源代碼

代碼目錄大家可以通過腳手架去生成
目錄介紹
1、public下存放不需要經過轉換得靜態資源
2、src下存放專案源檔案

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

下面,我們要利用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相關方法
const { src, dest, parallel, series } = require('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 運行構建任務

其他構建任務也都一樣創建
思路:
先建立不同型別檔案的編譯構建任務,將需要編譯的各個任務進行編譯構建,并一個個進行測驗,確保構建沒問題
當然,編譯不同檔案需要用到不同的插件,故同時需要安裝相應的插件,并引入相關插件(引入的代碼我就不貼了)
- 編譯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源檔案中用到了模板引擎,里面用到了相關資料,故我們決議時,需要傳入相關的資料

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


可以看到,相關任務,就都被打包了
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檔案夾下面找對應的檔案

此時,我們開發服務器已經起了一個了,并告知了服務器去取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

可以看出,這種資源檔案,構建后,會生成對應的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 ,
這個合并,可能你們不大理解,看下圖 ,你們就理解了

這個注釋中間引入了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生成我們的包的基本目錄

這個包中,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中

那么此時,后面有專案安裝了我們這個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的組態檔,格式如下

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

像這種路徑,不同專案,是不是可能不一樣啊,所以我們這樣寫死,也是不合理的,也應該抽象到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去指定去哪個目錄下找這個路徑

5、包裝gulp-cli
下面我們要包裝一下我們自己的cli命令,為什么要包裝呢,因為gulp構建時,默認是找gulpfile.js檔案的,而我們現在是放在/bin/index.js中,對于專案而言,這個檔案在/node_modules/cgp-build/lib/index.js中,所以在專案中運行yarn gulp build是會報錯的,報錯,找不到gulpfile.js檔案

此時,我們需要手動去指定gulpfile.js檔案為哪個檔案
yarn gulp build --gulpfile ./node_modules/cgp-build/lib/index.js --cwd .
–cwd . 的意思是以當前專案目錄作為根目錄,因為gulp會默認以gulpfile.js檔案所在目錄為根目錄,所以我們需要特別指定一下根目錄
那么這么弄,是不是很繁瑣啊,每次我需要執行下構建任務時,都要輸入這么一大串,此時,我們就可以自定義這個包檔案自己的cli指令,將這些 --gulpfile --cwd等引數都集成到指令中去
如何定義cli
在包檔案目錄下新建bin檔案夾,并在bin中新建cli.js,然后在package.json檔案中添加bin欄位


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

這里我解釋一下:一般包的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檔案就行了

這樣一來,當我們執行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方法往引數中添加引數

此時,整個cli的封裝就完成了,
npm提交
npm提交我們上篇文章已經說過了,這里就隨便提一下了,
1、將包上次至開源庫,如github
2、npm publish 或者 yarn publish上傳至npm庫中
提交完后,我們測驗下
先在本地準備一個專案目錄gulp-demo,里面放入我們之前那個專案
然后安裝我們提交至npm的包 cgp-build
yarn add cgp-build --dev

然后運行yarn cgp-build build 或者 cgp-build build

可以看出,是沒有問題的,正常打包成功
好了,自動化構建就寫到這了,喜歡請點個贊,謝謝
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/275498.html
標籤:其他
