認識 Gulp.js — 建立 Gulp 任務

認識 src、pipe、dest、series 和 parallel 方法

Zong-Rong Huang
12 min readDec 10, 2022
https://github.com/gulpjs/artwork

概述

本文會深入介紹 Gulp 任務中的重要方法:srcpipedestseriesparallel

這些方法可以用來建立 Gulp 基本任務,或是將基本任務串連起來組成複合任務。

Gulp 官方文件並沒有區別基本任務和複合任務。由於兩者的寫法和一些特性差異,在本文中會介紹這兩種任務:

  • 基本任務:由 srcpipedest 組成的任務,對應到一則 JavaScript 函式。
  • 複合任務:由 seriesparallel 組裝多個基本任務而成的複雜任務。

認識基本任務

Gulp 基本任務是由 srcpipedest 組成的任務,對應到一則 JavaScript 函式。

了解如何使用 srcpipedest 可以讓我們更清楚如何撰寫 Gulp 任務。

src():用 glob 格式指定檔案來源

src() 用來指定要處理的檔案或資料夾路徑。

指定路徑時,要使用 glob 格式。glob 是一種用來表示檔案路徑的格式,可以寫出明確的檔案路徑或搭配特殊符號一次指定多個檔案。

如果來源為明確的單個檔案,在 src() 裡提供檔案路徑字串;如果是好幾個明確的檔案,用陣列標明所有檔案的路徑。

// gulpfile.js

// 來源為單一檔案
function singleFileTask () {
return src('css/style.css')
.pipe(…)
.pipe(dest(…))
}

// 來源為多個檔案
function multiFilesTask () {
return src(['css/style.css', 'css/mobile.css'])
.pipe(…)
.pipe(dest(…))
}

要注意的是,要提供相對於 gulpfile.js 的檔案路徑。

glob 格式也可以搭配特殊字元使用,能夠一次指定多個符合條件的檔案。

常見的 glob 特殊字元:

  • *:0 到多個任意字元
  • **:0 到多層的資料夾
  • ?:1 個任意字元,不區分大小寫
  • !:否定字元,排除指定的檔案和資料夾

一起來了解這些特殊字元的用法:

// gulpfile.js

// 'styles' 資料夾內 (第一層) 所有 CSS 檔案
src('styles/*.css')

// 'js' 資料夾內 (不論內部有多少層資料夾) 的所有 index.js 檔案
// ('js/index.js'、'js/core/index.js'、'js/core/apis/index.js' 等等)
src('js/**/index.js')

// 'img' 資料夾內 (不論內部有多少層資料夾) 的所有 PNG 檔案
// ('img/brand.png'、'img/icon/add.png'、'img/pages/banners/sale.png' 等等)
src('img/**/*.png')

// 'img' 資料夾內 (第一層), 所有名稱為任何字元 + ig 的 PNG 檔案
// (例如:big.png, dig.png, Pig.png, Fig.png)
src('img/?ig.png')

// 'app' 資料夾裡所有的 js 檔案,但是不包含 'node_modules' 資料夾
// 換句話說,'node_modules' 內任何 js 檔案皆略過不處理
// 必須先列出要處理的項目,最後才列出不要的項目 (也就是帶有 ! 的項目)
src(['app/**/*.js', '!node_modules/**'])

要注意的是,使用 ! 時,src() 中的檔案路徑要用陣列標示:先舉出要處理的檔案路徑,最後才舉出要排除的檔案路徑 (也就是加上 ! 的路徑)。

除了上述介紹的特殊字元外,glob 另有其他的特殊字元可以搭配使用。然而,對於一般專案來說,只要檔案放置得宜,*** 已可滿足大多數需求。

src() 找到指定檔案後,會將這些檔案轉換為 Node.js stream,再轉給接下來的 pipe() 處理。

pipe():結合外掛程式進行處理

pipe 是 Node.js stream 的內建方法。一個 pipe() 會搭配一個外掛程式,相等於一個處理步驟,也是 Gulp 任務中最重要的部份。

取得 src() 提供的 Node.js stream 後,pipe() 會依照外掛程式的功能進行處理。使用外掛程式時,可能要額外提供一些設定值 (參考外掛程式說明文件)。

pipe() 處理完後,會再回傳 stream 給下一個 pipe() 接力處理。串接多個 pipe() 即可將多個步驟串連成一組工作流程。

dest():輸出檔案到指定目的地

dest() 負責將上一個 pipe() 處理完的資料輸出到目的地。

dest() 要放在 pipe() 中使用,負責將上一個 pipe() 傳來的 stream 以檔案格式輸出到指定的位置。輸出路徑用字串標示。

試寫第一個基本任務

有了前面介紹的 srcpipedest 概念後,就可以來模擬撰寫第一個基本任務。

任務需求:

  • 壓縮所有 HTML 檔案,同時去除裡面不必要的空格。
  • 壓縮後的 HTML 檔案輸出到 dist 資料夾。

能夠壓縮 HTML 檔案的外掛程式眾多,在這裡以 gulp-htmlmin 作為外掛程式。

// gulpfile.js

// 匯入 Gulp 方法和外掛程式
const { dest, src } = require("gulp");
const htmlmin = require("gulp-htmlmin");

// 建立任務函式
function minifyHTML() {
// 來源指定為專案資料夾內所有 HTML 檔案
return src("./**/*.html")
// 使用外掛程式進行壓縮,加上壓縮空白字元功能
.pipe(htmlmin({collapseWhitespace: true }))
// 輸出檔案至 'dist' 資料夾
.pipe(dest("dist"));
}

認識複合任務

了解如何建立基本任務後,一起來了解如何將多個基本任務組裝成複合任務。

Gulp 提供 seriesparallel 方法,可將多個任務組合成複合任務,控制基本任務的執行順序:

  • series(task1, task2, ..., taskN):依序從 task1 執行到 taskN。適合用在基本任務有相依性/特定執行順序時使用,確保處理結果正確。
  • parallel(task1, task2, ..., taskN):同時執行所有任務。適合當基本任務彼此獨立時使用,能夠縮短整體處理時間。

seriesparallel 組合基本任務就能建立複合任務。series()parallel() 也可以作為彼此的子任務:

// gulpfile.js

// 匯入 Gulp 方法和外掛程式
const { dest, src, series, parallel } = require("gulp");
const plugin1 = require("plugin1");
const plugin2 = require("plugin2");
const plugin3 = require("plugin3");
const plugin4 = require("plugin4");

// 建立基本任務
function task1 () {…}
function task2 () {…}
function task3 () {…}
function task4 () {…}

// 複合任務:依序從 task1 執行到 task4
series(task1, task2, task3, task4)

// 複合任務:同時執行四個任務
parallel(task1, task2, task3, task4)

// 複合任務:執行 task1,接著執行 parallel(task2, task3), 最後執行 task4
series(task1, parallel(task2, task3), task4)

// 複合任務:同時執行 task1、series(task2, task3) 和 task4
parallel(task1, series(task2, task3),task4)

// 其他可能任務組合…

不要重複執行任務

使用 seriesparallel 組裝基本任務時,記得避免重複執行特定任務,以免發生錯誤或拉長處理時間。

設計不良的複合任務 (取自 Gulp 官網範例):

// gulpfile.js

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);

在上述範例中,cssjavascript 複合任務都會執行clean 基本任務。也就是,clean 任務會執行兩次。

由於 clean 任務執行一次後就會清除目標檔案,再執行一次 clean 任務並沒有意義。另外,執行第二次 clean 任務時,指定的目標檔案已不存在,找不到要處理的檔案可能導致錯誤發生。

設計較佳的任務 (取自 Gulp 官網範例):

// gulpfile.js

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));

比較好的作法是將重覆用到的 clean 任務抽取出來。執行一次 clean 任務後,再同時執行 cssjavascript 任務。如此一來,執行結果能夠滿足需求,不會重覆執行導致錯誤發生。

試寫第一個複合任務

有了前面介紹的 seriesparallel 概念後,就可以來模擬撰寫第一個複合任務。

任務需求:

  • 將 Markdown 檔案轉換成 HTML 檔案。
  • 壓縮 HTML 檔案,同時去除裡面不必要的空格。
  • 壓縮後的 HTML 檔案輸出到 dist 資料夾。

在這裡會使用的外掛程式:

  • gulp-remarkable:將 Markdown 轉換成 HTML。
  • gulp-htmlmin:壓縮 HTML 內容。
// gulpfile.js

// 匯入 Gulp 方法和外掛程式
const { dest, src, series } = require("gulp");
const markdown = require("gulp-remarkable");
const htmlmin = require("gulp-htmlmin");

function convertMarkdownToHTML() {
return src('readme.md')
.pipe(markdown())
.pipe(dest('dist'))
}

function minifyHTML() {
return src("dist/readme.html")
.pipe(htmlmin({collapseWhitespace: true }))
.pipe(dest("dist"));
}

exports.build = series(convertMarkdownToHTML, minifyHTML)

從流程順序思考,必須先將 Markdown 轉換成 HTML 後才能進行壓縮,代表任務之間有相依性,因此使用 series 方法安排任務。

convertMarkdownToHTML 任務會將 Markdown 轉換成 HTML,輸出 HTML (readme.html) 至 dist 資料夾。minifyHTML 任務接著處理 readme.html,再將結果輸出至 dist 資料夾。由於輸出檔案與原有檔案同名,會自動覆蓋之前的檔案。

結論

Gulp 基本任務由 srcpipedest 組成,對應到一則 JavaScript 函式。在 src 內要用 glob 格式指定來源檔案;在 pipe 內指定外掛程式;在 dest 內指定檔案輸出位置。

Gulp 複合任務由 seriesparallel 組裝多個基本任務而成。series 能讓任務依序執行,parallel 能讓任務同時執行。要注意是否有任務重覆執行,以免拉長處理時間或發生錯誤。

--

--

Zong-Rong Huang

Frontend web developer/technical writer that writes to learn and self-entertain. I’m based in Taiwan.