Study Notes
本博主會(huì)持續(xù)更新各種前端的技術(shù),如果各位道友喜歡,可以關(guān)注播掷、收藏猛蔽、點(diǎn)贊下本博主的文章匹厘。
Gulp
用自動(dòng)化構(gòu)建工具增強(qiáng)你的工作流程脚线!
gulp 將開(kāi)發(fā)流程中讓人痛苦或耗時(shí)的任務(wù)自動(dòng)化屎媳,從而減少你所浪費(fèi)的時(shí)間脖捻、創(chuàng)造更大價(jià)值阔逼。
基本使用
安裝 gulp
npm i gulp -D
在項(xiàng)目根目錄下創(chuàng)建gulpfile.js
文件
function defaultTask(cb) {
// 在此處寫(xiě)默認(rèn)任務(wù)的代碼
cb();
}
function fooTask(cb) {
// 在此處寫(xiě)自定義任務(wù)的代碼
cb();
}
exports.default = defaultTask;
exports.foo = fooTask;
在package.json
文件中配置
{
"script": {
"gulp": "gulp"
}
}
測(cè)試
在項(xiàng)目根目錄下執(zhí)行 gulp 命令:
默認(rèn)執(zhí)行 default 任務(wù)
npm run gulp
執(zhí)行 foo 任務(wù)
npm run gulp foo
<span id="createTask"></span>
創(chuàng)建任務(wù)
每個(gè) gulp 任務(wù)(task)都是一個(gè)異步的 JavaScript 函數(shù),此函數(shù)是一個(gè)可以接收 callback 作為參數(shù)的函數(shù)地沮,或者是一個(gè)返回 stream嗜浮、promise、event emitter摩疑、child process 或 observable (后面會(huì)詳細(xì)講解) 類型值的函數(shù)危融。由于某些平臺(tái)的限制而不支持異步任務(wù),因此 gulp 還提供了一個(gè)漂亮 替代品雷袋。
導(dǎo)出任務(wù)
任務(wù)(tasks)可以是 public(公開(kāi)) 或 private(私有) 類型的吉殃。
- 公開(kāi)任務(wù)(Public tasks) 從 gulpfile 中被導(dǎo)出(export),可以通過(guò) gulp 命令直接調(diào)用楷怒。
- 私有任務(wù)(Private tasks) 被設(shè)計(jì)為在內(nèi)部使用寨腔,通常作為 series() 或 parallel() 組合的組成部分。
一個(gè)私有(private)類型的任務(wù)(task)在外觀和行為上和其他任務(wù)(task)是一樣的率寡,但是不能夠被用戶直接調(diào)用迫卢。如需將一個(gè)任務(wù)(task)注冊(cè)為公開(kāi)(public)類型的,只需從 gulpfile 中導(dǎo)出(export)即可冶共。
const { series } = require('gulp');
// `clean` 函數(shù)并未被導(dǎo)出(export)乾蛤,因此被認(rèn)為是私有任務(wù)(private task)每界。
// 它仍然可以被用在 `series()` 組合中。
function clean(cb) {
// body omitted
cb();
}
// `build` 函數(shù)被導(dǎo)出(export)了家卖,因此它是一個(gè)公開(kāi)任務(wù)(public task)眨层,并且可以被 `gulp` 命令直接調(diào)用。
// 它也仍然可以被用在 `series()` 組合中上荡。
function build(cb) {
// body omitted
cb();
}
exports.build = build;
exports.default = series(clean, build);
組合任務(wù)
Gulp 提供了兩個(gè)強(qiáng)大的組合方法: series() 和 parallel()趴樱,允許將多個(gè)獨(dú)立的任務(wù)組合為一個(gè)更大的操作。這兩個(gè)方法都可以接受任意數(shù)目的任務(wù)(task)函數(shù)或已經(jīng)組合的操作酪捡。series() 和 parallel() 可以互相嵌套至任意深度叁征。
如果需要讓任務(wù)(task)按順序執(zhí)行,請(qǐng)使用 series() 方法逛薇。
const { series } = require('gulp');
function transpile(cb) {
// body omitted
cb();
}
function bundle(cb) {
// body omitted
cb();
}
exports.build = series(transpile, bundle);
對(duì)于希望以最大并發(fā)來(lái)運(yùn)行的任務(wù)(tasks)捺疼,可以使用 parallel() 方法將它們組合起來(lái)。
const { parallel } = require('gulp');
function javascript(cb) {
// body omitted
cb();
}
function css(cb) {
// body omitted
cb();
}
exports.build = parallel(javascript, css);
當(dāng) series() 或 parallel() 被調(diào)用時(shí)永罚,任務(wù)(tasks)被立即組合在一起啤呼。這就允許在組合中進(jìn)行改變,而不需要在單個(gè)任務(wù)(task)中進(jìn)行條件判斷呢袱。
const { series } = require('gulp');
function minify(cb) {
// body omitted
cb();
}
function transpile(cb) {
// body omitted
cb();
}
function livereload(cb) {
// body omitted
cb();
}
if (process.env.NODE_ENV === 'production') {
exports.build = series(transpile, minify);
} else {
exports.build = series(transpile, livereload);
}
series() 和 parallel() 可以被嵌套到任意深度官扣。
const { series, parallel } = require('gulp');
function clean(cb) {
// body omitted
cb();
}
function cssTranspile(cb) {
// body omitted
cb();
}
function cssMinify(cb) {
// body omitted
cb();
}
function jsTranspile(cb) {
// body omitted
cb();
}
function jsBundle(cb) {
// body omitted
cb();
}
function jsMinify(cb) {
// body omitted
cb();
}
function publish(cb) {
// body omitted
cb();
}
exports.build = series(
clean,
parallel(cssTranspile, series(jsTranspile, jsBundle)),
parallel(cssMinify, jsMinify),
publish,
);
當(dāng)一個(gè)組合操作執(zhí)行時(shí),這個(gè)組合中的每一個(gè)任務(wù)每次被調(diào)用時(shí)都會(huì)被執(zhí)行羞福。例如惕蹄,在兩個(gè)不同的任務(wù)(task)之間調(diào)用的 clean 任務(wù)(task)將被執(zhí)行兩次,并且將導(dǎo)致不可預(yù)期的結(jié)果坯临。因此,最好重構(gòu)組合中的 clean 任務(wù)(task)恋昼。
如果你有如下代碼:
// This is INCORRECT
const { series, parallel } = require('gulp');
const clean = function (cb) {
// body omitted
cb();
};
const css = series(clean, function (cb) {
// body omitted
cb();
});
const javascript = series(clean, function (cb) {
// body omitted
cb();
});
exports.build = parallel(css, javascript);
重構(gòu)為:
const { series, parallel } = require('gulp');
function clean(cb) {
// body omitted
cb();
}
function css(cb) {
// body omitted
cb();
}
function javascript(cb) {
// body omitted
cb();
}
exports.build = series(clean, parallel(css, javascript));
異步執(zhí)行
Node 庫(kù)以多種方式處理異步功能看靠。最常見(jiàn)的模式是 error-first callbacks,但是你還可能會(huì)遇到 streams液肌、promises挟炬、event emitters、child processes, 或 observables嗦哆。gulp 任務(wù)(task)規(guī)范化了所有這些類型的異步功能谤祖。
任務(wù)(task)完成通知
當(dāng)從任務(wù)(task)中返回 stream、promise老速、event emitter粥喜、child process 或 observable 時(shí),成功或錯(cuò)誤值將通知 gulp 是否繼續(xù)執(zhí)行或結(jié)束橘券。如果任務(wù)(task)出錯(cuò)额湘,gulp 將立即結(jié)束執(zhí)行并顯示該錯(cuò)誤卿吐。
當(dāng)使用 series() 組合多個(gè)任務(wù)(task)時(shí),任何一個(gè)任務(wù)(task)的錯(cuò)誤將導(dǎo)致整個(gè)任務(wù)組合結(jié)束锋华,并且不會(huì)進(jìn)一步執(zhí)行其他任務(wù)嗡官。當(dāng)使用 parallel() 組合多個(gè)任務(wù)(task)時(shí),一個(gè)任務(wù)的錯(cuò)誤將結(jié)束整個(gè)任務(wù)組合的結(jié)束毯焕,但是其他并行的任務(wù)(task)可能會(huì)執(zhí)行完衍腥,也可能沒(méi)有執(zhí)行完嘶居。
返回 stream
const { src, dest } = require('gulp');
const streamTask = () => src('src/css/*.css').pipe(dest('dist/css'));
exports.default = streamTask;
返回 promise
const promiseTask = () => Promise.resolve('the value is ignored');
exports.default = promiseTask;
返回 event emitter
const { EventEmitter } = require('events');
const eventEmitterTask = () => {
const emitter = new EventEmitter();
// 發(fā)射必須異步發(fā)生歌径,否則gulp尚未監(jiān)聽(tīng)
setTimeout(() => emitter.emit('finish'), 250);
return emitter;
};
exports.default = eventEmitterTask;
返回 child process
const { exec } = require('child_process');
const childProcessTask = () => exec('date');
exports.default = childProcessTask;
返回 observable
const { of } = require('rxjs');
const observableTask = () => of(1, 2, 3);
exports.default = observableTask;
使用 callback
如果任務(wù)(task)不返回任何內(nèi)容长赞,則必須使用 callback 來(lái)指示任務(wù)已完成趣席。在如下示例中炸客,callback 將作為唯一一個(gè)名為 cb() 的參數(shù)傳遞給你的任務(wù)(task)剃根。
const callbackTask = (cb) => {
// `cb()` 應(yīng)該由一些異步工作來(lái)調(diào)用
cb();
};
exports.default = callbackTask;
如需通過(guò) callback 把任務(wù)(task)中的錯(cuò)誤告知 gulp打毛,請(qǐng)將 Error 作為 callback 的唯一參數(shù)速蕊。
const callbackError = (cb) => {
// `cb()` 應(yīng)該由一些異步工作來(lái)調(diào)用
cb(new Error('error'));
};
exports.default = callbackError;
然而物遇,你通常會(huì)將此 callback 函數(shù)傳遞給另一個(gè) API 乖仇,而不是自己調(diào)用它。
const fs = require('fs');
const passingCallback = (cb) => {
fs.access('gulpfile.js', cb);
};
exports.default = passingCallback;
gulp 不再支持同步任務(wù)(Synchronous tasks)
gulp 不再支持同步任務(wù)(Synchronous tasks)了询兴。因?yàn)橥饺蝿?wù)常常會(huì)導(dǎo)致難以調(diào)試的細(xì)微錯(cuò)誤乃沙,例如忘記從任務(wù)(task)中返回 stream。
當(dāng)你看到 "Did you forget to signal async completion?" 警告時(shí)诗舰,說(shuō)明你并未使用前面提到的返回方式警儒。你需要使用 callback 或返回 stream、promise眶根、event emitter蜀铲、child process、observable 來(lái)解決此問(wèn)題属百。
使用 async/await
如果不使用前面提供到幾種方式记劝,你還可以將任務(wù)(task)定義為一個(gè) async 函數(shù),它將利用 promise 對(duì)你的任務(wù)(task)進(jìn)行包裝族扰。這將允許你使用 await 處理 promise厌丑,并使用其他同步代碼。
const fs = require('fs');
const asyncAwaitTask = async () => {
const { version } = JSON.parse(fs.readFileSync('package.json'));
console.log(version, '版本號(hào)');
await Promise.resolve('some result');
};
exports.default = asyncAwaitTask;
文件處理
gulp 暴露了 src() 和 dest() 方法用于處理計(jì)算機(jī)上存放的文件渔呵。
src() 接受 glob 參數(shù)怒竿,并從文件系統(tǒng)中讀取文件然后生成一個(gè) Node 流(stream)。它將所有匹配的文件讀取到內(nèi)存中并通過(guò)流(stream)進(jìn)行處理扩氢。
由 src() 產(chǎn)生的流(stream)應(yīng)當(dāng)從任務(wù)(task)中返回并發(fā)出異步完成的信號(hào)耕驰,就如 創(chuàng)建任務(wù)(task) 文檔中所述。
流(stream)所提供的主要的 API 是 .pipe() 方法录豺,用于連接轉(zhuǎn)換流(Transform streams)或可寫(xiě)流(Writable streams)耍属。
const { src, dest } = require('gulp');
const babel = require('gulp-babel');
const streamJsTask = () =>
src('src/js/*.js').pipe(babel()).pipe(dest('dist/js'));
exports.default = streamJsTask;
dest() 接受一個(gè)輸出目錄作為參數(shù)托嚣,并且它還會(huì)產(chǎn)生一個(gè) Node 流(stream),通常作為終止流(terminator stream)厚骗。當(dāng)它接收到通過(guò)管道(pipeline)傳輸?shù)奈募r(shí)示启,它會(huì)將文件內(nèi)容及文件屬性寫(xiě)入到指定的目錄中。gulp 還提供了 symlink() 方法领舰,其操作方式類似 dest()夫嗓,但是創(chuàng)建的是鏈接而不是文件( 詳情請(qǐng)參閱 symlink() )。
大多數(shù)情況下冲秽,利用 .pipe() 方法將插件放置在 src() 和 dest() 之間舍咖,并轉(zhuǎn)換流(stream)中的文件。
向流(stream)中添加文件
src() 也可以放在管道(pipeline)的中間锉桑,以根據(jù)給定的 glob 向流(stream)中添加文件排霉。新加入的文件只對(duì)后續(xù)的轉(zhuǎn)換可用。如果 glob 匹配的文件與之前的有重復(fù)民轴,仍然會(huì)再次添加文件攻柠。
這對(duì)于在添加普通的 JavaScript 文件之前先轉(zhuǎn)換部分文件的場(chǎng)景很有用,添加新的文件后可以對(duì)所有文件統(tǒng)一進(jìn)行壓縮并混淆(uglifying)后裸。
const { src, dest } = require('gulp');
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const streamAddJsTask = () =>
src('src/js/*.js')
.pipe(babel())
.pipe(src('src/vendor/*.js'))
.pipe(uglify())
.pipe(dest('dist/js'));
exports.default = streamAddJsTask;
分階段輸出
dest() 可以用在管道(pipeline)中間用于將文件的中間狀態(tài)寫(xiě)入文件系統(tǒng)瑰钮。當(dāng)接收到一個(gè)文件時(shí),當(dāng)前狀態(tài)的文件將被寫(xiě)入文件系統(tǒng)微驶,文件路徑也將被修改以反映輸出文件的新位置浪谴,然后該文件繼續(xù)沿著管道(pipeline)傳輸。
此功能可用于在同一個(gè)管道(pipeline)中創(chuàng)建未壓縮(unminified)和已壓縮(minified)的文件因苹。
const { src, dest } = require('gulp');
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const rename = require('gulp-rename');
const streamSegmentJsTask = () =>
src('src/js/*.js')
.pipe(babel())
.pipe(src('src/vendor/*.js'))
.pipe(dest('dist/js/'))
.pipe(uglify())
.pipe(rename('index.min.js'))
.pipe(dest('dist/js/'));
exports.default = streamSegmentJsTask;
模式:流動(dòng)(streaming)苟耻、緩沖(buffered)和空(empty)模式
src() 可以工作在三種模式下:緩沖(buffering)、流動(dòng)(streaming)和空(empty)模式扶檐。這些模式可以通過(guò)對(duì) src() 的 buffer 和 read 參數(shù) 進(jìn)行設(shè)置凶杖。
緩沖(Buffering)模式是默認(rèn)模式,將文件內(nèi)容加載內(nèi)存中蘸秘。插件通常運(yùn)行在緩沖(buffering)模式下官卡,并且許多插件不支持流動(dòng)(streaming)模式蝗茁。
流動(dòng)(Streaming)模式的存在主要用于操作無(wú)法放入內(nèi)存中的大文件醋虏,例如巨幅圖像或電影。文件內(nèi)容從文件系統(tǒng)中以小塊的方式流式傳輸哮翘,而不是一次性全部加載颈嚼。如果需要流動(dòng)(streaming)模式,請(qǐng)查找支持此模式的插件或自己編寫(xiě)饭寺。
空(Empty)模式不包含任何內(nèi)容阻课,僅在處理文件元數(shù)據(jù)時(shí)有用叫挟。