說起前端自動化建構,相信做過前端的小夥伴們都不會陌生,可能第一感覺就會想到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
- 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相關方法
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檔案為哪個檔案
–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
可以看出,是沒有問題的,正常打包成功
好了,自動化建構就寫到這了。喜歡請點個贊,謝謝