支援 Gulp 4.0,允許巢狀配置任務及組態。以優雅、直覺的方式,重複使用 gulp 任務。
寫程式的時候你謹守 DRY 原則,那編寫 gulpfile.js 的時候,為什麼不呢?
注意:此專案目前仍處於早期開發階段,因此可能還存有錯誤。請協助回報問題並分享您的使用經驗,謝謝!
- 支援 Gulp 4.0,
- 自動載入本地通用任務 (recipe),
- 支援透過 npm 安裝 plugin,
- 支援巢狀任務並且允許子任務繼承組態配置,
- 支援向前、向後參照任務,
- 透過組態配置即可處理串流:譬如 merge, queue, 或者 concat,
- 透過組態配置即可控制子任務的執行: parallel 或者 series,
- 支援條件式組態配置,
- 支援命令列指令,查詢可用的 recpies 及使用方式,以及
- 支援命令列指令,查詢可用的任務說明及其組態配置。
答: 沒有, 你還是像平常一樣寫程式, 並且將可變動部份以組態配置的形式萃取出來。
Gulp-chef 透過簡化以下的工作來提昇使用彈性:
答: 有,像 gulp-cozy, gulp-load-subtasks, gulp-starter, elixir, 還有更多其他方案。
答:
- Gulp-chef 不是侵入式的。它不強迫也不限定你使用它的 API 來撰寫通用任務 (recipe)。
- Gulp-chef 強大且易用。它提供了最佳實務作法,如:合併串流、序列串流等。這表示,你可以讓任務『只做一件事並做好 (do one thing and do it well)』,然後使用組態配置來組合任務。
- Gulp-chef 本身以及共享任務 (plugin) 都是標準的 node 模組。你可以透過 npm 安裝並管理依賴關係,不再需要手動複製工具程式庫或任務程式碼,不再需要擔心忘記更新某個專案的任務,或者擔心專案之間的任務版本因各自修改而導致不一致的狀況。
- Gulp-chef 提供極大的彈性,讓你依喜好方式決定如何使用它: 『最精簡 (minimal)』 或 『最全面 (maximal)』,隨你選擇。
Gulp-chef 目前僅支援 gulp 4.0。如果你還沒開始使用 gulp 4.0,你需要先將全域安裝的舊 gulp 版本替換為新的 gulp-cli。
npm uninstall -g gulp
npm install -g "gulpjs/gulp-cli#4.0"
不用擔心,新的 gulp-cli 同時支援 gulp 4.0 與 gulp 3.x。所以你可以在既有的專案中繼續使用 gulp 3.x。
npm install --save-dev "gulpjs/gulp#4.0"
更詳細的安裝及 Gulp 4.0 入門請參閱 <<Gulp 4.0 前瞻>> 這篇文章。
$ npm install --save-dev gulp-chef
npm install --save-dev gulp-ccr-browserify gulp-ccr-postcss browserify-shim stringify stylelint postcss-import postcss-cssnext lost cssnano
var gulp = require('gulp');
var chef = require('gulp-chef');
var ingredients = {
src: 'src/',
dest: 'dist/',
clean: {},
make: {
postcss: {
src: 'styles.css',
processors: {
stylelint: {},
import: {},
cssnext: {
features: {
autoprefixer: {
browser: 'last 2 versions'
}
}
},
lost: {},
production: {
cssnano: {}
}
}
},
browserify: {
bundle: {
entry: 'main.js',
file: 'scripts.js',
transform: ['stringify', 'browserify-shim'],
production: {
uglify: true
}
}
},
assets: {
src: [
'index.html',
'favicon.ico',
'opensearch.xml'
],
recipe: 'copy'
}
},
build: ['clean', 'make'],
default: 'build'
};
var meals = chef(ingredients);
gulp.registry(meals);
$ gulp
示範只將 gulp-chef 作為任務的黏合工具,所有的任務都是沒有組態配置、單純的 JavaScript 函數。
根據 gulp-cheatsheet 的範例,展示 gulp-chef 的能耐。通常不建議以這裡採用的方式配置任務及組態。
根據現成完整可運作的範例程式 angularjs-gulp-example,示範如何將普通的 gulpfile.js 改用 gulp-chef 來撰寫。同時不要錯過了來自範例作者的好文章: "A complete toolchain for AngularJs - Gulp, Browserify, Sass"。
一個簡單的 web app 種子專案。同時也可以當做是一個示範使用本地 recipe 的專案。
一個 gulp task 只是普通的 JavaScript 函數,函數可以回傳 Promise, Observable, Stream, 子行程,或者是在完成任務時呼叫 done()
回呼函數。從 Gulp 4.0 開始,函數被呼叫時,其執行環境 (context),也就是 this
值,是 undefined
。
function gulpTask(done) {
assert(this === null);
// do things ...
done();
}
必須使用 gulp.task()
函數註冊之後,函數才會成為 gulp task。
gulp.task(gulpTask);
然後才能在命令列下執行。
$ gulp gulpTask
一個可組態配置的 gulp 任務在 gulp-chef 中稱為 configurable task,其函數參數配置與普通 gulp task 相同。但是被 gulp-chef 呼叫時,gulp-chef 將傳遞一個 { gulp, config, upstream }
物件做為其執行環境 (context)。
// 注意: configurable task 不能直接撰寫,必須透過配置組態的方式來產生。
function configurableTask(done) {
done();
}
你不能直接撰寫 configurable task,而是必須透過定義組態配置,並呼叫 chef()
函數來產生。
var gulp = require('gulp');
var chef = require('gulp-chef');
var meals = chef({
scripts: {
src: 'src/**/*.js',
dest: 'dist/'
}
});
gulp.registry(meals);
在這個範例中,gulp-chef 為你建立了一個名為 "scripts
" 的 configurable task。注意 chef()
函數回傳一個 gulp registry 物件,你可以透過回傳的 gulp registry 物件,以 meals.get('scripts')
的方式取得該 configurable task。但是通常你會呼叫 gulp.registry()
來註冊所有包含在 registry 之中的任務。
gulp.registry(meals);
一旦你呼叫了 gulp.registry()
之後,你就可以在命令列下執行那些已註冊的任務。
$ gulp scripts
當 configurable task 被呼叫時,將連同其一起配置的組態,經由其執行環境傳入,大致上是以如下的方式呼叫:
scripts.call({
gulp: gulp,
config: {
src: 'src/**/*.js',
dest: 'dist/'
}
}, done);
另外注意到在這個例子中,在組態中的 "scripts
" 項目,實際上對應到一個 recipe 或 plugin 的模組名稱。如果是 recipe 的話,該 recipe 模組檔案必須位於專案的 "gulp
" 目錄下。如果對應的是 plugin 的話,該 plugin 必須先安裝到專案中。更多細節請參考『撰寫 recipe』及『使用 plugin』的說明。
一個支援組態配置,可供 gulp 重複使用的任務,在 gulp-chef 中稱為 configurable recipe [註],其函數參數配置也與普通 gulp task 相同,在被 gulp-chef 呼叫時,gulp-chef 也將傳遞一個 { gulp, config, upstream }
物件做為其執行環境 (context)。這是你真正撰寫,並且重複使用的函數。事實上,前面提到的 "configurable task",就是透過名稱對應的方式,在組態配置中對應到真正的 configurable recipe,然後加以包裝、註冊為 gulp 任務。
function scripts(done) {
// 注意:在 configurable recipe 中,你可以直接由 context 取得 gulp 實體。
var gulp = this.gulp;
// 注意:在 configurable recipe 中,你可以直接由 context 取得 config 組態。
var config = this.config;
// 用力 ...
done();
}
註:在 gulp-chef 中,recipe 意指可重複使用的任務。就像一份『食譜』可以用來做出無數的菜餚一樣。
組態配置只是普通的 JSON 物件。組態中的每個項目、項目的子項目,要嘛是屬性 (property),要不然就是子任務。
任務可以巢狀配置。子任務依照組態的語法結構 (lexically),或稱靜態語彙結構 (statically),以層疊結構 (cascading) 的形式繼承 (inherit) 其父任務的組態。更棒的是,一些預先定義的屬性,譬如 "src
", "dest
" 等路徑性質的屬性,gulp-chef 會自動幫你連接好路徑。
var meals = chef({
src: 'src/',
dest: 'dist/',
build: {
scripts: {
src: '**/*.js'
},
styles: {
src: '**/*.css'
}
}
});
這個例子建立了__三個__ configurable tasks 任務:build
, scripts
以及 styles
。
在上面的例子中,當你執行 build
任務時,它的子任務 scripts
和 styles
會以並行 (parallel) 的方式同時執行。並且由於繼承的關係,它們將獲得如下的組態配置:
scripts: {
src: 'src/**/*.js',
dest: 'dist/'
},
styles: {
src: 'src/**/*.css',
dest: 'dist/'
}
如果你希望任務以序列 (series) 的順序執行,你可以使用 "series
" 流程控制器 (flow controller),並且在子任務的組態配置中,加上 "order
" 屬性:
var meals = chef({
src: 'src/',
dest: 'dist/',
build: {
series: {
scripts: {
src: '**/*.js',
order: 0
},
styles: {
src: '**/*.css',
order: 1
}
}
}
});
記住,你必須使用 "series
" 流程控制器,子任務才會以序列的順序執行,僅僅只是加上 "order
" 屬性並不會達到預期的效果。
var meals = chef({
src: 'src/',
dest: 'dist/',
build: {
scripts: {
src: '**/*.js',
order: 0
},
styles: {
src: '**/*.css',
order: 1
}
}
});
在這個例子中,scripts
和 styles
會以並行的方式同時執行。
其實有更簡單的方式,可以使子任務以序列的順序執行:使用陣列。
var meals = chef({
src: 'src/',
dest: 'dist/',
build: [{
name: 'scripts',
src: '**/*.js'
}, {
name: 'styles',
src: '**/*.css'
}]
};
不過,看起來似乎有點可笑?別急,請繼續往下看。
你可以使用名稱來參照其他任務。向前、向後參照皆可。
var meals = chef({
src: 'src/',
dest: 'dist/',
clean: {},
scripts: {
src: '**/*.js'
},
styles: {
src: '**/*.css'
},
build: ['clean', 'scripts', 'styles']
};
在這個例子中,build
任務有三個子任務,分別參照到 clean
, scripts
以及 styles
任務。參照任務並不會產生並註冊新的任務,所以,在這個例子中,你無法直接執行 build
任務的子任務,但是你可以透過執行 build
任務執行它們。
前面提到過,子任務依照組態的語法結構 (lexically),或稱靜態語彙結構 (statically),以層疊結構 (cascading) 的形式繼承 (inherit) 其父任務的組態。既然『被參照的任務』不是定義在『參照任務』之下,『被參照的任務』自然不會繼承『參照任務』及其父任務的靜態組態配置。不過,有另一種組態是執行時期動態產生的,動態組態會在執行時期注入到『被參照的任務』。更多細節請參考『動態組態』的說明。
在這個例子中,由於使用陣列來指定參照 clean
, scripts
及 styles
的任務,所以是以序列的順序執行。你可以使用 "parallel
" 流程控制器改變這個預設行為。
var meals = chef({
src: 'src/',
dest: 'dist/',
clean: {},
scripts: {
src: '**/*.js'
},
styles: {
src: '**/*.css'
},
build: ['clean', { parallel: ['scripts', 'styles'] }]
};
或者,其實你可以將子任務以物件屬性的方式,放在一個共同父任務之下,這樣它們就會預設以並行的方式執行。
var meals = chef({
src: 'src/',
dest: 'dist/',
clean: {},
make: {
scripts: {
src: '**/*.js'
},
styles: {
src: '**/*.css'
}
},
build: ['clean', 'make']
});
你可以另外使用 "task
" 關鍵字來引用『被參照的任務』,這樣『參照任務』本身就可以同時擁有其他屬性。
var meals = chef({
src: 'src/',
dest: 'dist/',
clean: {},
make: {
scripts: {
src: '**/*.js'
},
styles: {
src: '**/*.css'
}
},
build: {
description: 'Clean and make',
task: ['clean', 'make']
},
watch: {
description: 'Watch and run related task',
options: {
usePolling: true
},
task: ['scripts', 'styles']
}
};
任務也可以以普通函數的方式定義並且直接引用,或以內聯匿名函數的形式引用。
function clean() {
return del(this.config.dest.path);
}
var meals = chef({
src: 'src/',
dest: 'dist/',
scripts: function (done) {
},
styles: function (done) {
},
build: [clean, { parallel: ['scripts', 'styles'] }]
};
注意在這個例子中,在組態配置中並未定義 clean
項目,所以clean
並不會被註冊為 gulp task。
另外一個需要注意的地方是,即使只是純函數,gulp-chef 呼叫時,總是會以 { gulp, config, upstream }
做為執行環境來呼叫。
你一樣可以使用 "task
" 關鍵字來引用函數,這樣任務本身就可以同時擁有其他屬性。
function clean() {
return del(this.config.dest.path);
}
var meals = chef({
src: 'src/',
dest: 'dist/',
clean: {
options: {
dryRun: true
},
task: clean
},
make: {
scripts: {
src: '**/*.js',
task: function (done) {
}
},
styles: {
src: '**/*.css',
task: function (done) {
}
}
},
build: ['clean', 'make'],
watch: {
options: {
usePolling: true
},
task: ['scripts', 'styles']
}
};
注意到與上個例子相反地,在這裡組態配置中定義了 clean
項目,因此 gulp-chef 會產生並註冊 clean
任務,所以可以由命令列執行clean
任務。
有時候,某些任務永遠不需要單獨在命令列下執行。隱藏任務可以讓任務不要註冊,同時不可被其它任務引用。隱藏一個任務不會影響到它的子任務,子任務仍然會繼承它的組態配置並且註冊為 gulp 任務。隱藏任務仍然是具有功能的,但是只能透過它的父任務執行。
要隱藏一個任務,可以在項目的組態中加入具有 "hidden
" 值的 "visibility
" 屬性。
var meals = chef({
src: 'src/',
dest: 'dist/',
scripts: {
concat: {
visibility: 'hidden',
file: 'bundle.js',
src: 'lib/',
coffee: {
src: '**/*.coffee'
},
js: {
src: '**/*.js'
}
}
}
};
在這個例子中,concat
任務已經被隱藏了,然而它的子任務 coffee
和 js
依然可見。
為了簡化組態配置,你也可以使用在任務名稱前面附加上一個 ".
" 字元的方式來隱藏任務,就像 UNIX 系統的 dot-files 一樣。
var meals = chef({
src: 'src/',
dest: 'dist/',
scripts: {
'.concat': {
file: 'bundle.js',
src: 'lib',
coffee: {
src: '**/*.coffee'
},
js: {
src: '**/*.js'
}
}
}
};
這將產出與上一個例子完全相同的結果。
有時候,當你在調整 gulpfile.js 時,你可能需要暫時移除某些任務,找出發生問題的根源。這時候你可以停用任務。停用任務時,連同其全部的子任務都將被停用,就如同未曾定義過一樣。
要停用一個任務,可以在項目的組態中加入具有 "disabled
" 值的 "visibility
" 屬性。
var meals = chef({
src: 'src/',
dest: 'dist/',
scripts: {
concat: {
file: 'bundle.js',
src: 'lib/',
coffee: {
visibility: 'disabled',
src: '**/*.coffee'
},
js: {
src: '**/*.js'
}
}
}
};
在這個例子中,coffee
任務已經被停用了。
為了簡化組態配置,你也可以使用在任務名稱前面附加上一個 "#
" 字元的方式來停用任務,就像 UNIX 系統的 bash 指令檔的註解一樣。
var meals = chef({
src: 'src/',
dest: 'dist/',
scripts: {
concat: {
file: 'bundle.js',
src: 'lib',
'#coffee': {
src: '**/*.coffee'
},
js: {
src: '**/*.js'
}
}
}
};
這將產出與上一個例子完全相同的結果。
在使用 gulp-chef 時,建議你為所有的任務,分別取用唯一、容易區別的名稱。
然而,如果你有非常多的任務,那麼將有很高的機率,有一個以上的任務必須使用相同的 recipe 或 plugin。
在預設情況下,任務名稱必須與 recipe 名稱相同,這樣 gulp-chef 才有辦法找到對應的 recipe。那麼,當發生名稱衝突時,gulp-chef 是怎麼處理的呢?gulp-chef 會自動為發生衝突的的任務,在前方附加父任務的名稱,像這樣:"make:scripts:concat
"。
事實上,你也可以將這個附加名稱的行為變成預設行為:在呼叫 chef()
函數時,在 settings
參數傳入值為 true
的 "exposeWithPrefix
" 屬性即可。 "exposeWithPrefix
" 屬性的預設值為 "auto"
。
var ingredients = { ... };
var settings = { exposeWithPrefix: true };
var meals = chef(ingredients, settings);
不是你的菜?沒關係,你也可以使用其他辦法。
{
scripts: {
concatScripts: {
'.concat': {
file: 'bundle.js'
}
}
},
styles: {
concatStyles: {
'.concat': {
file: 'main.css'
}
}
}
}
{
scripts: {
concatScripts: {
recipe: 'concat',
file: 'bundle.js'
}
},
styles: {
concatStyles: {
recipe: 'concat',
file: 'main.css'
}
}
}
注意:為了盡量避免發生名稱衝突的可能性,並且簡化任務樹,某些特定種類的任務預設是隱藏的。主要是『串流處理器 (stream processor)』及『流程控制器 (flow controller)』。請參考 撰寫串流處理器 and 撰寫流程控制器 的說明。
有時候,你所撰寫的任務所做的,只不過是轉呼叫一個 plugin。如果只是這樣的話,事實上你完全可以不用費心寫一個 recipe,你可以直接在組態配置中使用 "plugin
" 關鍵字做為屬性來引用 plugin。
{
concat: {
plugin: 'gulp-concat',
options: 'bundle.js'
}
}
這個 "plugin
" 屬性可以接受 string
和 function
類型的值。當指定的值不是 function
而是 string
類型時,gulp-chef 將以此字串做為模組名稱,嘗試去 "require()
" 該模組。使用 "plugin
" 屬性時,另外還可以指定 "options
" 屬性,該屬性的值將直接做為唯一參數,用來呼叫 plugin 函數。
任何 gulp plugin,只要它只接受 0 或 1 個參數,並且回傳一個 Stream 或 Promise 物件,就可以使用 plugin
" 關鍵字來加以引用。前提當然是 plugin 已經使用 npm install
指令先安裝好了。
千萬不要將 gulp plugin 與 gulp-chef 專用的 plugin 搞混了。gulp-chef 專用的 plugin 稱為 "Cascading Configurable Recipe for Gulp" 或簡稱 "gulp-ccr",意思是『可層疊組態配置、可重複使用的 Gulp 任務』。
如同你到目前為止所看到的,在組態配置中的項目,要嘛是任務的屬性,要不然就是子任務。你要如何區別兩者?基本的規則是,除了 "config
", "description
", "dest
", "name
", "order
", "parallel
", "plugin
", "recipe
", "series
", "spit
", "src
", "task
" 以及 "visibility
" 這些關鍵字之外,其餘的項目都將被視為子任務。
那麼,你要如何傳遞組態值給你的 recipe 函數呢?其實,"config
" 關鍵字就是特地為了這個目的而保留的。
{
myPlugin: {
config: {
file: 'bundle.js'
}
}
}
這裡 "config
" 屬性連同其 "file
" 屬性,將一起被傳遞給 recipe 函數,而 recipe 函數則透過執行環境依序取得 "config
" 屬性及 "file
" 屬性 (在『撰寫 recipe』中詳細說明)。
function myPlugin(done) {
var file = this.config.file;
done();
}
module.exports = myPlugin;
只為了傳遞一個屬性,就必須特地寫一個 "config
" 項目來傳遞它,如果你覺得這樣做太超過了,你也可以直接在任意屬性名稱前面附加一個 "$
" 字元,這樣它們就會被視為是組態屬性,而不再會被當作是子任務。
{
myPlugin: {
$file: 'bundle.js'
}
}
這樣 "$file
" 項目就會被當作是組態屬性,而你在組態配置及 recipe 中,可以透過 "file
" 名稱來存取它。 (注意,名稱不是 "$file
",這是為了允許使用者可以交換使用 "$
" 字元和 "config
" 項目來傳遞組態屬性。)
Recipe 以及 plugin 可以使用 JSON Schema 來定義它們的組態屬性及架構。如果它們確實定義了組態架構,那麼你就可以在組態配置項目中,直接列舉專屬的屬性,而不需要透過 "$
" 字元和 "config
" 關鍵字。
舉例,在 "gulp-ccr-browserify" plugin 中,它定義了 "bundles
" 及 "options
" 屬性,因此你可以在組態項目中直接使用這兩個屬性。
原本需要這樣寫:
{
src: 'src/',
dest: 'dest/',
browserify: {
config: {
bundles: {
entry: 'main.ts'
},
options: {
plugins: 'tsify',
sourcemaps: 'external'
}
}
}
}
現在可以省略寫成這樣:
{
src: 'src/',
dest: 'dest/',
browserify: {
bundles: {
entry: 'main.ts'
},
options: {
plugins: 'tsify',
sourcemaps: 'external'
}
}
}
為了方便起見,當組態項目中包含有 "task
", "series
", "parallel
" 或 "plugin
" 關鍵字的時候,這時候除了保留屬性之外,其餘的屬性都將自動認定為組態屬性,而不是子任務。
有些『串流處理器』 (譬如 "gulp-ccr-each-dir"),會以程序化或動態的方式產生新的組態屬性。這些新產生的屬性,將在執行時期,插入到子任務的的組態中。除了 recipe 及 plugin 可以透過 "config
" 屬性取得這些值之外,子任務也可以透過使用模板的方式,以 "{{var}}
" 這樣的語法,直接在組態中引用這些值。
{
src: 'src/',
dest: 'dist/',
'each-dir': {
dir: 'modules/',
concat: {
file: '{{dir}}',
spit: true
}
}
}
這個例子裡,"each-dir" plugin 會根據 "dir
" 屬性指定的內容,也就是 "modules
" 目錄,找出其下的所有子目錄,然後產生新的 "dir
" 屬性,透過這個屬性將子目錄資訊傳遞給每個子任務 (這裡只有 "concat" 任務)。子任務可以透過 "config
" 屬性讀取這個值。使用者也可以使用 "{{dir}}
" 這樣的語法,在組態配置中引用這個值。
Gulp-chef 支援條件式組態配置。可以透過設定執行時期環境的模式來啟用不同的條件式組態配置。這個功能的實作是基於 json-regulator 這個模組,可以參考該模組的說明以便獲得更多的相關資訊。
預設提供了 development
, production
及 staging
三個模式。你可以在組態配置中,將相關的組態內容,分別寫在對應的 development
或 dev
, production
或 prod
,或 staging
項目之下。
譬如,如果將組態配置寫成這樣:
{
scripts: {
// common configs
src: 'src/',
development: {
// development configs
description: 'development mode',
dest: 'build/',
options: {
// development options
debug: true
},
// sub tasks for development mode
lint: {
}
},
production: {
// production configs
description: 'production mode',
dest: 'dist/',
options: {
// production options
debug: false
}
},
options: {
// common options
dev: {
// development options
description: 'development mode',
sourcemap: false
},
prod: {
// production options
description: 'production mode',
sourcemap: 'external'
}
},
// sub tasks
pipe: [{
typescript: {
src: '**/*.ts'
},
js: {
src: '**/*.js'
}
}, {
production: {
// production configs
description: 'production mode',
// sub tasks for production mode
uglify: {
}
}
}, {
production: {
// production configs
description: 'production mode',
// sub tasks for production mode
concat: {
}
}
}]
}
}
當啟用 development
模式時,組態配置將被轉換為:
{
scripts: {
src: 'src/',
description: 'development mode',
dest: 'build/',
options: {
description: 'development mode',
sourcemap: false,
debug: true
},
lint: {
},
pipe: [{
typescript: {
src: '**/*.ts'
},
js: {
src: '**/*.js'
}
}]
}
}
而啟用 production
模式時,組態配置將被轉換為:
{
scripts: {
src: 'src/',
description: 'production mode',
dest: 'dist/',
options: {
description: 'production mode',
sourcemap: 'external',
debug: false
},
pipe: [{
typescript: {
src: '**/*.ts'
},
js: {
src: '**/*.js'
}
}, {
description: 'production mode',
uglify: {
}
}, {
description: 'production mode',
concat: {
}
}]
}
}
超強的!
$ gulp --development build
也可以使用簡寫:
$ gulp --dev build
在 Linux/Unix 下:
$ NODE_ENV=development gulp build
同樣地,若使用簡寫:
$ NODE_ENV=dev gulp build
Gulp-chef 允許你自訂執行時期環境模式。如果你崇尚極簡主義,你甚至可以分別使用 d
, p
及 s
代表 development
, production
及 staging
模式。只是要記得,組態配置必須與執行時期環境模式配套才行。
var ingredients = {
scripts: {
src: 'src/',
lint: {
},
d: {
debug: true
},
p: {
debug: false,
sourcemap: 'external',
uglify: {
},
concat: {
}
}
}
};
var settings = {
modes: {
production: ['p'],
development: ['d'],
staging: ['s'],
default: 'production'
}
};
var meals = chef(ingredients, settings);
注意到在 settings.modes
之下的 default
屬性。這個屬性不會定義新的模式,它是用來指定預設的模式。如果沒有指定 settings.modes.default
,那麼,預設模式會成為列在 settings.modes
之下的第一個模式。建議最好不要省略。
除了改變模式的代號,你甚至可以設計自己的模式,並且還能一次提供多個代號。
var settings = {
modes = {
build: ['b', 'build'],
compile: ['c', 'compile'],
deploy: ['d', 'deploy', 'deployment'],
review: ['r', 'review']
default: 'build'
}
};
但是要注意的是,不要使用到保留給任務使用的關鍵字。
清除 dest
屬性指定的目錄。
複製由 src
屬性指定的檔案,到由 dest
屬性指定的目錄,可以選擇是否移除或改變檔案的相對路徑。
這是一個串流處理器。回傳一個新的串流,該串流只有在所有的子任務的串流都停止時才會停止。
更多資訊請參考 merge-stream 。
這是一個串流處理器。可以匯集子任務所回傳的串流,並回傳一個新的串流,該串流會將子任務回傳的串流,依照子任務的順序排列在一起。
更多資訊請參考 streamqueue 。
這是一個串流處理器。提供與 stream.Readable.pipe()
相同的功能。方便在子任務之間遞送 (pipe) 串流。
這是一個流程控制器。會以並行 (parallel) 的方式執行子任務,子任務之間不會互相等待。
這是一個流程控制器。會以序列 (series) 的方式執行子任務,前一個子任務結束之後才會執行下一個子任務。
這是一個流程控制器。負責監看指定的子任務、以及其所有子任務的來源檔案,當有任何檔案異動時,執行對應的指定任務。
在你撰寫自己的 recipe 之前,先看一下別人已經做了哪些東西,也許有現成的可以拿來用。你可以使用" gulp recipe
",或者,更建議使用 "gulp-ccr
",在 github.com 和 npmjs.com 上搜尋。這個 "gulp-ccr
" 是 "Cascading Configurable Recipe for Gulp" 的簡寫,意思是『可層疊組態配置、可重複使用的 Gulp 任務』。
一旦你找到了,譬如,gulp-ccr-browserify
,將它安裝為專案的 devDependencies:
$ npm install --save-dev gulp-ccr-browserify
Gulp-chef 會為你移除附加在前面的 "gulp-ccr-
" 名稱,所以你在使用 plugin 的時候,請移除 "gulp-ccr-
" 部份。
{
browserify: {
description: 'Using the gulp-ccr-browserify plugin'
}
}
斯斯,不是,recipe 有三種: 任務型 (task)、串流處理器 (stream processor) 以及 流程控制器 (flow controller)。
大多數時候,你想要寫的是任務型 recipe。任務型 recipe 負責做苦工,而串流處理器及流程控制器則負責操弄其它 recipe。
更多關於串流處理器及流程控制器的說明,或者你樂於分享你的 recipe,你可以寫成 plugin,請參考 撰寫 Plugin 的說明。
如果你撰寫的 recipe 只打算給特定專案使用,你可以將它們放在專案根目錄下的特定子目錄下:
類型 | 目錄 |
---|---|
任務型 | gulp, gulp/tasks |
串流處理器 | gulp/streams |
流程控制器 | gulp/flows |
如果你的 recipe 不需要組態配置,你可以像平常寫 gulp task 一樣的方式撰寫 recipe。知道這代表什麼意思嗎?這代表你以前寫的 gulp task 都可以直接拿來當作 recipe 用。你只需要將它們個別存放到專屬的模組檔案,然後放到專案根目錄下的 "gulp" 目錄下即可。
使用 recipe 的時候,在組態配置中,使用一個屬性名稱與 recipe 模組名稱一模一樣的項目來引用該 recipe。
譬如,假設你有一個 "my-recipe.js
" recipe 放在 <your-project>/gulp
目錄下。可以這樣撰寫組態配置來引用它:
var gulp = require('gulp');
var chef = require('gulp-chef');
var meals = chef({
"my-recipe": {}
});
gulp.registry(meals);
就是這麼簡單。之後你就可以在命令列下,以 gulp my-recipe
指令執行它。
然而,提供組態配置的能力,才能最大化 recipe 的重複使用價值。
要讓 recipe 可以處理組態內容,可以在 recipe 函數中,透過執行環境,也就是 this
變數,取得組態。
function scripts(done) {
var gulp = this.gulp;
var config = this.config;
return gulp.src(config.src.globs)
.pipe(eslint())
.pipe(concat(config.file))
.pipe(uglify())
.pipe(gulp.dest(config.dest.path));
}
module.exports = scripts;
上面的 "scripts
" recipe,在使用的時候可以像這樣配置:
var meals = chef({
src: 'src/',
dest: 'dist/',
scripts: {
src: '**/*.js',
file: 'bundle.js'
}
});
Gulp-chef 的 recipe 不需要自行處理條件式組態配置。組態配置在傳遞給 recipe 之前,已經先根據執行環境模式處理完畢。
Gulp-chef 的 plugin,只是普通的 Node.js 模組,再加上一些必要的資訊。
在前面 撰寫 Recipe 的部份提到過,recipe 有三種:任務型 (task)、串流處理器 (stream processor) 以及 流程控制器 (flow controller)。Gulp-chef 需要知道 plugin 的類型,才能安插必要的輔助功能。由於 plugin 必須使用 npm install
安裝,gulp-chef 無法像本地的 recipe 一樣,由目錄決定 recipe 的類型,因此 plugin 必須自行提供類型資訊。
function myPlugin(done) {
done();
}
module.exports = myPlugin;
module.exports.type = 'flow';
有效的類型為: "flow
"、"stream
" 以及 "task
"。
為了簡化組態配置的處理過程,gulp-chef 鼓勵使用 JSON Schema 來驗證和轉換組態配置。Gulp-chef 使用 json-normalizer 來為 JSON Schema 提供擴充功能,並且協助將組態內容一般化 (或稱正規化),以提供最大的組態配置彈性。你可以為你的 plugin 定義組態架構,以提供屬性別名、類型轉換、預設值等功能。同時,組態架構的定義內容還可以顯示在命令列中,使用者可以使用指令 gulp --recipe <recipe-name>
查詢,不必另外查閱文件,就可以了解如何撰寫組態配置。請參考 json-normalizer 的說明,了解如何定義組態架構,甚至加以擴充。
以下是一個簡單的 plugin,示範如何定義組態架構:
var gulpif = require('gulp-if');
var concat = require('gulp-concat');
var sourcemaps = require('gulp-sourcemaps');
var uglify = require('gulp-uglify');
function myPlugin() {
var gulp = this.gulp;
var config = this.config;
var options = this.config.options || {};
var maps = (options.sourcemaps === 'external') ? './' : null;
return gulp.src(config.src.globs)
.pipe(gulpif(config.sourcemaps, sourcemaps.init())
.pipe(concat(config.file))
.pipe(gulpif(options.uglify, uglify()))
.pipe(gulpif(options.sourcemaps, sourcemaps.write(maps)))
.pipe(gulp.dest(config.dest.path));
}
module.exports = myPlugin;
module.exports.type = 'task';
module.exports.schema = {
title: 'My Plugin',
description: 'My first plugin',
type: 'object',
properties: {
src: {
type: 'glob'
},
dest: {
type: 'path'
},
file: {
description: 'Output file name',
type: 'string'
},
options: {
type: 'object',
properties: {
sourcemaps: {
description: 'Sourcemap support',
alias: ['sourcemap'],
enum: [false, 'inline', 'external'],
default: false
},
uglify: {
description: 'Uglify bundle file',
type: 'boolean',
default: false
}
}
}
},
required: ['file']
};
首先,注意到 "file
" 被標示為『必須』,plugin 可以利用組態驗證工具自動進行檢查,因此在程式中就不須要再自行判斷。
另外注意到 "sourcemaps
" 選項允許 "sourcemap
" 別名,因此使用者可以在組態配置中隨意使用 "sourcemaps
" 或 "sourcemap
",但是同時在 plugin 中,卻只需要處理 "sourcemaps
" 即可。
Gulp-chef 提供兩個擴充的 JSON Schema 資料型別: "glob
" 及 "path
"。
一個屬性如果是 "glob
" 型別,它可以接受一個路徑、一個路徑匹配表達式 (glob),或者是一個由路徑或路徑匹配表達式組成的陣列。另外還可以額外附帶選項資料。
以下都是正確的 "glob
" 數值:
// 一個路徑字串
'src'
// 一個由路徑字串組成的陣列
['src', 'lib']
// 一個路徑匹配表達式
'**/*.js'
// 一個由路徑或路徑匹配表達式組成的陣列
['**/*.{js,ts}', '!test*']
// 非正規化的『物件表達形式』(注意 "glob" 屬性)
{ glob: '**/*.js' }
上面所有的數值,都會被正規化為所謂的『物件表達形式』:
// 一個路徑字串
{ globs: ['src'] }
// 一個由路徑字串組成的陣列
{ globs: ['src', 'lib'] }
// 一個路徑匹配表達式
{ globs: ['**/*.js'] }
// 一個由路徑或路徑匹配表達式組成的陣列
{ globs: ['**/*.{js,ts}', '!test*'] }
// 正規化之後的『物件表達形式』(注意 "glob" 屬性已經正規化為 "globs")
{ globs: ['**/*.js'] }
注意到 "glob
" 是 "globs
" 屬性的別名,在正規化之後,被更正為 "globs
"。同時,"glob
" 型別的 "globs
" 屬性的型態為陣列,因此,所有的值都將自動被轉換為陣列。
當以『物件表達形式』呈現時,還可以使用 "options
" 屬性額外附帶選項資料。
{
globs: ['**/*.{js,ts}', '!test*'],
options: {
base: 'src',
buffer: true,
dot: true
}
}
更多選項資料,請參考 node-glob 的說明。
在任務中,任何具有 "glob
" 型別的組態屬性,都會繼承其父任務的 "src
" 屬性。這意謂著,當父任務定義了 "src
" 屬性時,gulp-chef 會為子任務的 "glob
" 型別的組態屬性,自動連接好父任務的 "src
" 屬性的路徑。
{
src: 'src',
browserify: {
bundles: {
entries: 'main.js'
}
}
}
在這個例子中,"browserify" plugin 具有一個 "bundles
" 屬性,"bundles
" 屬性下又有一個 "entries
" 屬性,而該屬性為 "glob
" 型別。這個 "entries
" 屬性將繼承外部的 "src
" 屬性,因而變成: { globs: "src/main.js" }
。
如果這不是你要的,你可以指定 "join
" 選項來覆蓋這個行為。
{
src: 'src',
browserify: {
bundles: {
entry: {
glob: 'main.js',
options: {
join: false
}
}
}
}
}
現在 "entries
" 屬性的值將成為: { globs: "main.js" }
。
選項 "join
" 也可以接受字串,用來指定要從哪一個屬性繼承路徑,該屬性必須是 "glob
" 或 "path
" 型別。
在 plugin 中,也可以透過組態架構來定義預設要繼承的屬性。請記住,除非有好的理由,請永遠記得同時將 "options
" 傳遞給呼叫的 API,以便允許使用者指定選項。像這樣:
module.exports = function () {
var gulp = this.gulp;
var config = this.config;
return gulp.src(config.src.globs, config.src.options)
.pipe(...);
}
一個屬性如果是 "path
" 型別,它可以接受一個路徑字串。另外還可以額外附帶選項資料。
以下都是正確的 "path
" 數值:
// 一個路徑字串
'dist'
// 一個路徑字串
'src/lib/'
// 『物件表達形式』
{ path: 'maps/' }
上面所有的數值,都會被正規化為所謂的『物件表達形式』:
// 一個路徑字串
{ path: 'dist' }
// 一個路徑字串
{ path: 'src/lib/' }
// 『物件表達形式』
{ path: 'maps/' }
當以『物件表達形式』呈現時,還可以使用 "options
" 屬性額外附帶選項資料。
{
path: 'dist/',
options: {
cwd: './',
overwrite: true
}
}
更多選項資料,請參考 gulp.dest() 的說明。
在任務中,任何具有 "path
" 型別的組態屬性,都會繼承其父任務的 "dest
" 屬性。這意謂著,當父任務定義了 "dest
" 屬性時,gulp-chef 會為子任務的 "path
" 型別的組態屬性,自動連接好父任務的 "dest
" 屬性的路徑。
{
dest: 'dist/',
scripts: {
file: 'bundle.js'
}
}
假設這裡的 "file
" 屬性是 "path
" 型別,它將會繼承外部的 "dest
" 屬性,而成為: "{ path: 'dist/bundle.js' }
"。
如果這不是你要的,你可以指定 "join
" 選項來覆蓋這個行為。
{
dest: 'dist/',
scripts: {
file: {
path: 'bundle.js',
options: {
join: false
}
}
}
}
現在 "file
" 屬性將成為: "{ path: 'bundle.js' }
"。
選項 "join
" 也可以接受字串,用來指定要從哪一個屬性繼承路徑,該屬性必須是 "path
" 型別。
在 plugin 中,也可以透過組態架構來定義預設要繼承的屬性。請記住,除非有好的理由,請永遠記得同時將 "options
" 傳遞給呼叫的 API,以便允許使用者指定選項。像這樣:
module.exports = function () {
var gulp = this.gulp;
var config = this.config;
return gulp.src(config.src.globs, config.src.options)
.pipe(...)
.pipe(gulp.dest(config.dest.path, config.dest.options));
}
串流處理器負責操作它的子任務輸入或輸出串流。
串流處理器可以自己輸出串流,或者由其中的子任務輸出。串流處理器可以在子任務之間遞送串流;或合併;或串接子任務的串流。任何你能想像得到的處理方式。唯一必要的要求就是:串流處理器必須回傳一個串流。
串流處理器由執行環境中取得 "tasks
" 屬性,子任務即是經由此屬性,以陣列的方式傳入。
當呼叫子任務時,串流處理器必須為子任務建立適當的執行環境。
module.exports = function () {
var gulp = this.gulp;
var config = this.config;
var tasks = this.tasks;
var context, stream;
context = {
gulp: gulp,
// 傳入獲得的組態配置,以便將上層父任務動態插入的組態屬性傳遞給子任務
config: config
};
// 如果需要的話,可以額外插入新的組態屬性
context.config.injectedValue = 'hello!';
stream = tasks[0].call(context);
// ...
return stream;
};
注意父任務可以動態給子任務插入新的組態屬性。只有新的值可以成功插入,若子任務原本就配置了同名的屬性,則新插入的屬性不會覆蓋原本的屬性。
如果要傳遞串流給子任務,串流處理器必須透過 "upstream
" 屬性傳遞。
module.exports = function () {
var gulp = this.gulp;
var config = this.config;
var tasks = this.tasks;
var context, stream, i;
context = {
gulp: gulp,
config: config
};
stream = gulp.src(config.src.globs, config.src.options);
for (i = 0; i < tasks.length; ++i) {
context.upstream = stream;
stream = tasks[i].call(context);
}
return stream;
};
如果串流處理器期望子任務回傳一個串流,然而子任務卻沒有,那麼此時串流處理器必須拋出一個錯誤。
注意:官方關於撰寫 gulp plugin 的 指導方針 中提到: "不要在串流中拋出錯誤 (do not throw errors inside a stream)"。 沒錯,你不應該在串流中拋出錯誤。但是在串流處理器中,如果不是位於處理串流的程式流程中,而是在處理流程之外,那麼,拋出錯誤是沒有問題的。
你可以使用 gulp-ccr-stream-helper 來協助呼叫子任務,並且檢查其是否正確回傳一個串流。
你可以從 gulp-ccr-merge 以及 gulp-ccr-queue 專案,參考串流處理器的實作。
流程控制器負責控制子任務的執行時機,順序等,而且並不關心子任務的輸出、入串流。
流程控制器沒有什麼特別的限制,唯一的規則是,流程控制器必須正確處理子任務的結束事件。譬如,子任務可以呼叫 "done()
" 回呼函數;回傳一個串流或 Promise,等等。
你可以從 gulp-ccr-parallel 、 gulp-ccr-series 以及 gulp-ccr-watch 專案,參考流程控制器的實作。
建議你可以先寫供專案使用的本地 recipe,完成之後,再轉換為 plugin。大多數的 recipe 測試都是資料導向的,如果你的 recipe 也是這樣,也許你可以考慮使用我的另一個專案: mocha-cases 。
以下的關鍵字保留給任務屬性使用,你不能使用這些關鍵字做為你的任務或屬性名稱。
要傳遞給任務的組態配置。
描述任務的工作內容。
要寫出檔案的路徑。定義在子任務中的路徑,預設會繼承父任務的定義的 dest 路徑。屬性值可以是字串,或者是如下的物件形式: { path: '', options: {} }
。實際傳遞給任務的是後者的形式。
任務名稱。通常會自動由組態項目名稱獲得。除非任務是定義在陣列中,而你仍然希望能夠在命令列中執行。
任務的執行順序。只有在你以物件屬性的方式定義子任務時,又希望子任務能夠依序執行時才需要。數值僅用來排序,因此不需要是連續值。需要配合 "series
" 屬性才能發揮作用。
要求子任務以並行的方式同時執行。預設情形下,以物件屬性的方式定義的子任務才會並行執行。使用此關鍵字時,子任務不論是以陣列項目或物件屬性的方式定義,都將並行執行。
要使用的原生 gulp plugin,可以是模組名稱或函數。
任務所要對應的 recipe 模組名稱。預設與任務名稱 "name
" 屬性相同。
要求子任務以序列的方式逐一執行。預設情形下,以陣列項目的方式定義的子任務才會序列執行。使用此關鍵字時,子任務不論是以陣列項目或物件屬性的方式定義,都將序列執行。
要求任務寫出檔案。任務允許使用者決定要不要寫出檔案時才有作用。
要讀入的檔案來源的路徑或檔案匹配表達式。由於預設會繼承父任務的 "src
" 屬性,通常你會在父任務中定義路徑,在終端任務中才定義檔案匹配表達式。屬性值可以是任意合格的檔案匹配表達式,或由檔案匹配表達式組成的陣列,或者如下的物件形式: { globs: [], options: {} }
呈現。實際傳遞給任務的是後者的形式。
定義實際執行任務的方式。可以是普通函數的引用、內聯函數或對其它任務的參照。子任務如果以陣列的形式提供,子任務將以序列的順序執行,否則子任務將以並行的方式同時執行。
任務的可見性。有效值為 normal
、 hidden
以及 disabled
。
設定選項可以改變 gulp-chef 的預設行為,以及用來定義自訂條件式組態配置的執行時期環境模式。
設定選項是經由 chef()
方法的第二個參數傳遞:
var config = {
};
var settings = {
};
var meals = chef(config, settings);
開關自動附加任務名稱功能。預設值為 "auto"
,當發生名稱衝突時,gulp-chef 會自動為發生衝突的的任務,在前方附加父任務的名稱,像這樣:"make:scripts:concat
"。你可以設定為 true
強制開啟。設定為 false
強制關閉,此時若遇到名稱衝突時,會拋出錯誤。
設定本地通用任務模組 (recipe) 的查找目錄。預設值為:
{
lookups: {
flows: 'flows',
streams: 'streams',
tasks: 'tasks'
}
}
設定本地流程控制器的查找目錄。預設值為 "flows"
。
設定本地串流處理器的查找目錄。預設值為 "streams"
。
設定本地通用任務的查找目錄。預設值為 "tasks"
。
傳遞給 "gulp-load-plugins" 的選項。
Gulp-chef 使用 "gulp-load-plugins" 來載入共享任務模組,或稱為 "gulp-ccr" 模組。
預設情形下,不是以 "gulp-ccr"
名稱開頭的共享任務模組將不會被載入。你可以透過更改 "plugins
" 選項來載入這些模組。
預設選項為:
{
plugins: {
camelize: false,
config: process.cwd() + '/package.json',
pattern: ['gulp-ccr-*'],
replaceString: /^gulp[-.]ccr[-.]/g
}
}
當設定為 true
時,"gulp-load-plugins" 將輸出 log 訊息到 console。
若設定為 true
,使用 "-"
連接的名稱將被改為駝峰形式。
由何處查找共享任務模組的資訊。預設為專案的 package.json。
共享任務模組的路徑匹配表達式 (glob)。預設為 "gulp-ccr-*"
。
要查找哪些相依範圍。預設為:
['dependencies', 'devDependencies', 'peerDependencies'].
要移除的模組附加名稱。預設為: /^gulp[-.]ccr[-.]/g
。
是否延遲載入模組。預設為 true
。
指定改名。必須為 hash 物件。鍵為原始名稱,值為改名名稱。
改名函數。
定義自訂條件式組態配置的執行時期環境模式。
除了 default
屬性是用來指定預設模式之外,其餘的屬性名稱定義新的模式,而值必須是陣列,陣列的項目是可用於組態配置及命令列的識別字代號。注意不要使用到保留給任務使用的關鍵字。預設為:
{
modes: {
production: ['production', 'prod'],
development: ['development', 'dev'],
staging: ['staging'],
default: 'production'
}
}
查詢任務並顯示其工作內容說明以及組態配置內容。
$ gulp --task <task-name>
列舉可用的 recipe,包含內建的 recipe、本地的 recipe 以及已安裝的 plugin 。
你可以任意使用 "--recipes
" 、 "--recipe
" 以及 "--r
" 。
$ gulp --recipes
查詢指定 recipe,顯示其用途說明,以及,如果有定義的話,顯示其組態架構。
$ gulp --recipe <recipe-name>
$ git clone https://github.com/gulp-cookery/gulp-chef.git
$ cd gulp-chef
$ npm install
測試是以 mocha 撰寫,請在命令列下執行下列指令:
$ npm test