iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 27
1
Modern Web

前端建置工具完全手冊系列 第 27

Day 27 介紹 gulp

插播一下, webpack 5 出來了

gulp 是個老牌的 task runner ,它就只是執行設定好的工作,設定寫起來其實不難,不過現在前端大家主要還是用 bundler ,所以像 gulp 這樣的 task runner 就用的比較少了,事實上還有一個更老的 grunt ,不過這邊不會介紹

安裝

gulp 本身只有一個套件:

$ yarn add --dev gulp

不過因為它本身並沒有做什麼事,通常需要搭配 plugin 來使用,等下會用到 delgulp-babel,同時這邊也先把 babel 安裝起來:

$ yarn add --dev del gulp-babel @babel/core @babel/preset-env

然後設定 babel ,這邊就不贅述了

gulpfile.js

要 gulp 做什麼都要寫在 gulpfile.js 這個檔案中 (或用 Gulpfile.js),沒有這個檔案(同時你也沒指定其它代替的檔案), gulp 根本什麼事也都不會做, gulpfile.js 基本格式很簡單:

const { series } = require('gulp')

// 這樣就會有一個 task 叫 cleanup
// 你也可以選擇不 export ,這樣就沒辦法直接用指令呼叫
exports.cleanup = async function cleanup () {}

// 另一個 task 叫 build
exports.build = async function build () {}

// default 是不指定 task 時預設執行的,這邊是依序呼叫 cleanup 跟 build
exports.default = series(cleanup, build)

gulp 的 task 一定是 async 的 (這邊不是指一定要是 async function),所以一定要用某種方式回報自己完成,可以用 callback 也可以用 Promise ,另外還有 stream 可以用

const { src, dest } = require('gulp')
// 用 callback
exports.callback = function (cb) {
  cb()
}

// 用 Promise
exports.promise = function () {
  return Promise.resolve()
}

// 用 async function
exports.asyncFunc = async function () {}

// 用 async function
exports.asyncFunc = async function () {}

// 用 stream ,這通常是在用 gulp 的 plugin 時
exports.stream = function () {
  return src('./src/**/*.js').pipe(dest('lib'))
}

另外其實還有 Observable 跟 EventEmitter 可以用,不過通常 stream 跟 async function (promise) 就很夠用了,有需要可以自己去看

stream

Node.js 中有個用來處理大檔案用的叫 stream 的東西,它會一次只讀入一部份的檔案,用這種方式來處理大檔案,一來是因為 Node.js 可用的 heap 空間一般是有限制的,因此不一定一次能讀入很大的檔案,二來是把檔案一次讀入記憶體,然後在記憶體中一次處理完有點那麼違反了 Node.js 的 event driven 的架構,如果沒有刻意的把主執行緒轉交出去的話,在檔案處理完前其它的工作都會被擱置,而 gulp 則同樣的使用了 Node.js 的 stream ,在裡面傳遞的不是小塊小塊的檔案內容,而是要處理的檔案的資訊與它的內容,這麼說可能不太好懂,下一篇會再來詳細解釋

在上面你應該也有看到,我用到了 gulp 的兩個 API , srcdest

  • src: 建立輸入檔案的 stream ,裡面是放單一個 glob 字串,或是 glob 字串的 array
  • dest: 建立輸出的目標,參數一定要是一個資料夾名稱

一般使用 gulp 都是使用 src 來指定要處理的檔案有哪些,再用 dest 來指定處理完的檔案要放哪,如果中間都不處理的話,實際上就跟複製檔案沒兩樣:

// 對,就是上面的那個範例,這相當於把 src 下的 js 都照著原本資料夾結構複製到 lib
exports.copy = function () {
  return src('./src/**/*.js').pipe(dest('lib'))
}

glob

glob 是一種一般而言是描述要選擇哪些檔案的東西,你可能有聽過「萬用字元」這種稱呼,不過現在的 glob 包含的語法要來的更多,如果你知道怎麼用 glob 或是知道怎麼寫 gitignore (gitignore 就是 glob 的語法) ,你可以跳過這段,如果不清楚的可以看看,畢竟很多東西都支援這樣的語法,像正規表示法一樣

主要的幾個符號:

  • *: 0 ~ 多個字元,不包含 /
  • **: 同樣是 0 ~ 多個字元,但包含 / 這個分隔資料夾的符號,另外它不能在檔名的一部份 (src/**.js 是不行的),可以用來代表資料夾下包含子資料夾的檔案
  • !: 只能放在開頭,代表符合這個條件的不要選擇

所以底下的幾個範例是:

  • *.js: 這層資料夾下副檔名為 js 的檔案
  • src/*.js: src 下副檔名是 js 的檔案
  • src/**: src 下包含子資料夾的檔案
  • src/**/*: src 下包含子資料夾的檔案
  • src/**/*.js: src 下包含子資料夾的 js 檔
  • !src/**/*.js: 不要 src 下包含子資料夾的 js 檔

不是所有的東西都支援這種語法,有不少其實只支援 * 而已的,不過以 Node.js 而言,有 node-glob (gulp 底下用的),另一個系列用到,同時也是很常用的 globby ,還有 globby 底下的 fast-glob ,或是單純檢查路徑是不是符合 glob 的 micromatchminimatch

gulp-babel

終於要來用第一個 plugin 了,不過其實用起來也很簡單

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

exports.build = async function build () {
  return src('./src/**/*.js')
    // 大部份的 plugin 都是像這樣,夾在 src 跟 dest 中間,就能處理檔案了
    .pipe(babel(/* 通常這邊可以放選項 */))
    .pipe(dest('lib'))
}

cleanup task

這邊再來用另一個 task ,這次用的是 del ,它能遞迴的刪掉檔案與資料夾,並且會回傳 Promise

const del = require('del')

exports.cleanup = function cleanup () {
  // 回傳 Promise 讓 gulp 知道何時跑完
  return del('./lib')
}

兩個加起來就有一個能清理上次的檔案與用 babel 重 build 的功能了:

const { src, dest, series } = require('gulp')
const babel = require('gulp-babel')
const del = require('del')

exports.cleanup = function cleanup () {
  return del('./lib')
}

exports.build = async function build () {
  return src('./src/**/*.js')
    .pipe(babel())
    .pipe(dest('lib'))
}

exports.default = series(cleanup, build)

另外除了 series 這個照順序執行的,還有 parallel 這個可以同步執行的,兩個也可以組合:

const { series, parallel } = require('gulp')

async function a() {}
async function b() {}
async function c() {}
async function d() {}

exports.default = series(a, parallel(b, c), d)

這樣就是先執行 a ,然後同步執行 b 跟 c ,最後再 d

另外 gulp 還有 watch 的功能,有興趣可以去看看,因為現在其實也不常用了,我就不介紹太多了,下一篇來講 gulp 內部在做什麼


上一篇
Day 26 介紹 vite
下一篇
Day 28: gulp 是怎麼運作的
系列文
前端建置工具完全手冊30

尚未有邦友留言

立即登入留言