認識 Gulp.js — 建立監聽任務

監聽檔案變動,自動執行 Gulp 流程

Zong-Rong Huang
18 min readJan 19, 2023
https://github.com/gulpjs/artwork

概述

若要執行 Gulp 任務,通常要在終端機上手動輸入指令觸發。然而,開發時可能遇上需要反覆執行任務的情境,若是只能透過手動觸發,效率極低。

為了解決上述問題,Gulp 提供非常方便的監聽功能:Gulp 會監聽指定檔案,當檔案內容發生變動時,就會自動執行對應任務。

本文內容如下:

.認識監聽任務

.更多任務設定

.建立更複雜的監聽任務

.匯出和執行監聽任務

.用 chokidar 實例建立監聽任務 (不推薦)

.動手練習

認識監聽任務

Gulp 內建 watch 方法,用來建立監聽任務。當 Gulp 監聽到檔案發生指定的事件,就會立即執行對應任務。

watch 方法能夠監聽下列事件:

  • add:新增檔案
  • change:修改檔案
  • unlink:移除檔案
  • addDir:新增資料夾
  • unlinkDir:移除資料夾
  • error:發生錯誤
  • ready:完成檔案/資料夾初次掃瞄,能夠開始監聽 change 事件
  • raw:所有事件 (能夠取得所有事件的名稱、路徑及細節)
  • all:上述所有事件 (不含 readyrawerror 事件)

各個事件可提供不同的詳細資料,請參考:https://www.npmjs.com/package/chokidar

撰寫監聽任務時,要提供檔案路徑、要監聽的事件類型和任務函式。如果自行指定監聽事件,透過 {events: ...} 就能帶入。

任務函式內要傳入 callback 函式,負責通知 Gulp 這個監聽任務已完成。callback 函式是由 Gulp 自行產生,開發者不需另行宣告。

// gulpfile.js

const {src, dest, watch} = require('gulp')

function watchStyles () {
watch('./src/css/**', {events: 'change'}, function(cb) {
src(...)
.pipe(...)
.pipe(dest(...))

cb()
})
}

若要監聽多個事件,透過 {events: []} 以陣列帶入所有要監聽的事件:

// gulpfile.js

const {src, dest, watch} = require('gulp')

function watchStyles () {
watch('./src/css/**', {events: ['change', 'add']}, function(cb) {
src(...)
.pipe(...)
.pipe(dest(...))

cb()
})
}

如果任務未指定要監聽的事件,預設會監聽 addchangeunlink 事件。

// gulpfile.js

const {src, dest, watch} = require('gulp')

// 監聽 add、change 和 unlink 事件
function watchStyles () {
watch('./src/css/**', function(cb) {
src(...)
.pipe(...)
.pipe(dest(...))

cb()
})
}

更多任務設定

監聽任務函式中的第二個 argument {...} 不僅能夠帶入要監聽的事件,還能帶入更多設定,讓監聽任務更加符合需求。

以下介紹其中三種設定:ignoreInitialqueuedelay

  • ignoreInitial

執行監聽任務時,預設會在檔案出現變動後才會觸發任務執行。

如果希望在已經開始監聽檔案、但檔案尚未出現變動的時候就執行任務,可以傳入 {ignoreInitial: false} 設定。當監聽任務一開始執行、還未修改檔案時,就會對已有的 CSS 檔案作處理。

// gulpfile.js

const {src, dest, watch} = require('gulp')

function watchStyles () {
watch('./src/css/*.css', {ignoreInitial: false}, function(cb) {
src(...)
.pipe(...)
.pipe(dest(...))

cb()
})
}
  • queue

由於監聽的事件可能在短時間內觸發多次 (例如快速修改檔案並存檔),Gulp 會讓每一次觸發的任務依時間順序執行。

如果有特殊考量,希望能讓多個觸發的任務同時執行,而非依序執行,可以傳入 {queue: false} 關閉預設行為。

// gulpfile.js

const {src, dest, watch} = require('gulp')

function watchStyles () {
watch('./src/css/**', {queue: false}, function(cb) {
src(...)
.pipe(...)
.pipe(dest(...))

cb()
})
}
  • delay

監聽函式預設會在檔案修改後 200 毫秒才執行任務,避免在一次性大量修改檔案時就執行任務,導致錯誤發生。

在特定情況下,可能需要更長的延遲時間才執行監聽任務。可以傳入 {delay: ...} 來調整延遲時間。時間單位為毫秒。

// gulpfile.js

const {src, dest, watch} = require('gulp')

// 等待 500 毫秒後再執行
function watchStyles () {
watch('./src/css/**', {delay: 500}, function(cb) {
src(...)
.pipe(...)
.pipe(dest(...))

cb()
})
}

如果想要了解更多監聽任務設定,參考: https://gulpjs.com/docs/en/api/watch

建立更複雜的監聽任務

先前所介紹的監聽任務都相當單純,只監聽單一檔案路徑內是否有變更;如果有變更的話,則執行一項指定任務。

然而,實際開發時所需的監聽任務可能更為複雜:必須監聽多個檔案路徑或觸發多個指定任務。

接下來會介紹這些可能的複雜情境以及如何建立合適的監聽任務。

監聽多個檔案路徑 + 執行單一任務

若要同時監聽多個檔案,在陣列內列出要監聽的所有檔案即可。

// gulpfile.js
const {watch} = require('gulp')

function informMe(cb){
console.log('Changes detected!')
cb()
}

// 用陣列列出所有要監聽的檔案
function watcher () {
watch(['./src/js/**', './src/css/**'], informMe)
}

在上面的例子中,只要 Gulp 監聽到這些資料夾內的檔案有任何變更,就會執行任務印出訊息。

監聽單一檔案路徑 + 執行多個任務

當 Gulp 監聽到單一檔案路徑內有變更,即可觸發多個任務。只需用 seriesparallel 包住要觸發的任務。不需為個別任務撰寫對應的監聽任務,寫起來簡潔不少。

// gulpfile.js

const {src, dest, watch, series} = require('gulp')

function convertToJS () {...}
function uglifyJS () {...}
function concatJS () {...}

function watcher () {
watch('./src/ts/**', series(convertToJS, uglifyJS, concatJS))
}

在上面的例子中,當 TypeScript 檔案內發生變更,Gulp 會依序將所有的 .ts 檔案轉成 .js 檔案、進行壓縮,最後再將所有產出的 .js 檔案合併為單一 .js 檔案。

監聽多個檔案路徑 + 執行不同任務

如果必須同時監聽多個不同的檔案路徑,而且各個檔案路徑要執行的任務不相同時,還是得要為各個檔案路徑撰寫對應的監聽任務。

只是,當專案內有多個監聽任務分散四處時,在管理和執行時,可能有所遺漏,增加處理負擔。

很棒的是,可以只用一個任務函式整合多個監聽任務。如此一來,只須匯出、執行外層函式,就能同時執行多個監聽任務,維護監聽函式也更加輕鬆:

// gulpfile.js
const {watch} = require('gulp')

function watchAll () {
watch('./src/js/**', function() {...})

watch('./src/scss/**', function() {...})

watch('./src/index.html', function() {...})
}

匯出和執行監聽任務

監聽任務和一般的 Gulp 任務一樣,都必須匯出為公開任務才能執行。可以匯出為預設任務或一般任務:

// gulpfile.js

const {src, dest, watch} = require('gulp')

function watchStyles () {
watch('./src/css/**', function(cb) {...})
}

function watchHTML () {
watch('./src/index.html', function(cb) {...})
}

// 匯出為預設任務
exports.default = watchStyles

// 匯出為一般任務
exports.html = watchHTML

匯出監聽任務後,在終端機輸入 Gulp 指令即開始執行監聽任務。例如,輸入 gulpgulp default 會執行 watchStyles 任務,輸入 gulp html 則會執行 watchHTML 任務。

用 chokidar 實例建立監聽任務 (不推薦)

以上介紹的是用 Gulp 內建的 watch 建立監聽任務的方式,也是最安全的方式。

然而,你也有機會看到類似 JavaScript 事件監聽器的其他寫法。這種寫法是利用 Gulp 內部的 chokidar 套件,直接建立 chokidar 實例來執行監聽任務:

// gulpfile.js

const {watch} = require('gulp')

// 傳入要監聽的檔案路徑,建立 chokidar 實例
const watcher = watch(['styles/sass/*.scss'])

// 註冊監聽器
// path = 受到修改的檔案的路徑
// stats = 檔案狀態 (即檔案的 metadata)
watcher.on('change', function(path, stats) {
console.log({path, stats})
})

watcher.on('add', function (path, stats) {
// ...
})

依據官方文件說明,建立 chokidar 實例作為監聽器會失去 Gulp 的系統優勢:

When using the chokidar instance directly, you will not have access to the task system integrations, including async completion, queueing, and delay.

容易導致錯誤發生,使得處理結果不如預期或造成效能負擔。因此已不建議使用此種寫法。

動手練習

Gulp 監聽任務最經典的使用情境是將 SCSS 即時編譯為 CSS。SCSS 語法比 CSS 更加彈性,是不少人的開發首選。但是瀏覽器無法直接讀取 SCSS,必須先執行指令將 SCSS 編譯成 CSS,瀏覽器才能讀取到樣式。

Gulp 能夠監聽 SCSS 檔案變化,就能立即產生 CSS 檔案,不需反覆手動下指令編譯,非常方便!

不過,瀏覽器並不會因為 CSS 檔案有變化而自動重新載入。在這個練習中會搭配 Browsersync 套件,依賴它來建立開發伺服器。當 HTML 或 SCSS 檔案變動時,Gulp 會通知 Browsersync 重新整理頁面,重新渲染畫面。

練習的專案資料夾結構如下:

|-- 專案資料夾/
|-- styles/
|-- sass/
|-- index.scss
|-- index.html
  1. 依上述結構建立你的練習用專案。
  2. 在終端機輸入下列指令進行專案初始化:
npm init -y

3. 全域安裝 gulp-cli

npm install --global gulp-cli

4. 安裝 ‘gulp’、‘sass’、‘gulp-sass’ 和 ‘browser-sync’ 套件到專案資料夾中:

npm install gulp sass gulp-sass browser-sync --save-dev

5. 在根目錄處建立 gulpfile.js 檔案。

6. 開啟 gulpfile.js 來建立任務。首先引入需要的外掛程式和套件:

// gulpfile.js

// 取出 Gulp 提供的方法
const { src, dest, parallel, watch } = require("gulp");

// 建立 browserSync 實例 (開發伺服器)
const browserSync = require("browser-sync").create();

// 引入 SASS
const sass = require("gulp-sass")(require("sass"));

7. 建立將 SCSS 轉換為 CSS 的 Gulp 任務:

// gulpfile.js

function convertSCSSToCSS() {
return src("styles/scss/*.scss")
.pipe(sass())
.pipe(dest("app/styles/css"))
.pipe(browserSync.stream());
}

這個任務將會 styles/scss 裡所有的 SCSS 檔案轉換為 CSS 檔案,並將產生的檔案放置到 app/styles/css 資料夾中。app 資料夾內也會存放所有給開發伺服器使用的資源。

最後透過 browserSync.stream() 方法回傳 stream,通知瀏覽器 CSS 檔案有變化。

8. 建立將 HTML 檔案複製到 app 資料夾的 Gulp 任務:

// gulpfile.js

function copyHTML(){
return src('*.html')
.pipe(dest('app'))
.pipe(browserSync.stream())
}

一樣在最後加上 browserSync.stream() 方法回傳 stream,讓瀏覽器得知 HTML 檔案有變化。

9. 在 index.html 檔案裡要指定 CSS 檔案來源,瀏覽器才能讀取到樣式:

<!-- index.html -->

<head>
...
<link rel="stylesheet" href="styles/css/index.css" />
</head>

10. 建立要匯出執行的任務:

// gulpfile.js

function serve() {
browserSync.init({
server: "./app",
});

watch(
["styles/scss/*.scss", "*.html"],
{ ignoreInitial: false },
parallel(convertSCSSToCSS, copyHTML)
);

watch(["app/**"], browserSync.reload);
}

在這個任務中,先對 Browsersync 實例 (也就是開發用的伺服器) 進行初始化。開發伺服器的進入點設定為 ./app 資料夾。

裡面含有兩個監聽任務。第一個任務負責監聽 SCSS 檔案和 HTML 檔案是否有任何變動。如果有變動的話,依序將 SCSS 檔案轉成 CSS 檔案,再複製 HTML 檔案。產生的檔案都會放到 app 資料夾內,讓瀏覽器讀取。

監聽任務中的 {ignoreInitial: false} 設定。即使還沒修改 SCSS 檔案和 HTML 檔案,Gulp 會先產生對應的檔案到 app 資料夾。這樣一來,開啟開發網址時,就有檔案讓瀏覽器讀取。

第二個任務監聽 app 資料夾內是否有任何變動 (例如新增檔案或檔案內容有變化)。一旦發現變動,瀏覽器就會重新整理頁面,依據最新的CSS 和 HTML 內容渲染畫面。

11. 將 serve 任務匯出為公開任務,之後才能使用 Gulp 指令執行。

// gulpfile.js

function serve () {...}

exports.default = serve

12. 在終端機輸入 Gulp 指令來執行任務:

gulp

瀏覽器會自動開啟頁面 (或是點選終端機上的開發網址前往頁面)。接著就能進行開發!只要 HTML 和 SCSS 檔案有任何變化,監聽任務都會立刻執行,即時更新瀏覽器畫面。即使新增 SCSS 檔案或 HTML 檔案,Gulp 也會進行處理,讓開發伺服器和瀏覽器都能讀取到。

完整內容如下:

// gulpfile.js

const { src, dest, parallel, watch } = require("gulp");
const browserSync = require("browser-sync").create();
const sass = require("gulp-sass")(require("sass"));

function convertSCSSToCSS() {
return src("styles/scss/*.scss")
.pipe(sass())
.pipe(dest("app/styles/css"))
.pipe(browserSync.stream());
}

function copyHTML() {
return src("*.html").pipe(dest("app")).pipe(browserSync.stream());
}

function serve() {
browserSync.init({
server: "./app",
});

watch(
["styles/scss/*.scss", "*.html"],
{ ignoreInitial: false },
parallel(convertSCSSToCSS, copyHTML)
);

watch(["app/**"], browserSync.reload);
}

exports.default = serve;

結論

Gulp 的 watch 方法能用來建立監聽任務,自動進行處理。建立監聽任務時,除了指定監聽的檔案路徑及要執行的任務,還可以加上更多設定,讓監聽行為更加符合需求。

Gulp 監聽任務可以應付多個檔案路徑和多個執行任務,方便維護、執行監聽任務。

參考資料

--

--

Zong-Rong Huang

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