Gulp Under the Hood: Membangun Alat Otomatisasi Tugas Berbasis Aliran
Diterbitkan: 2022-03-11Pengembang front-end saat ini menggunakan beberapa alat untuk mengotomatisasi operasi rutin. Tiga dari solusi yang paling populer adalah Grunt, Gulp dan Webpack. Masing-masing alat ini dibangun di atas filosofi yang berbeda, tetapi mereka memiliki tujuan yang sama: untuk merampingkan proses pembuatan front-end. Misalnya, Grunt digerakkan oleh konfigurasi sementara Gulp hampir tidak menerapkan apa pun. Faktanya, Gulp bergantung pada pengembang yang menulis kode untuk mengimplementasikan alur proses build - berbagai tugas build.
Ketika memilih salah satu alat ini, favorit pribadi saya adalah Gulp. Secara keseluruhan, ini adalah solusi yang sederhana, cepat, dan andal. Pada artikel ini kita akan melihat bagaimana Gulp bekerja di bawah tenda dengan mencoba menerapkan alat mirip Gulp kita sendiri.
API Teguk
Gulp hadir hanya dengan empat fungsi sederhana:
- gulp.task
- gulp.src
- gulp.dest
- gulp.watch
Empat fungsi sederhana ini, dalam berbagai kombinasi menawarkan semua kekuatan dan fleksibilitas Gulp. Pada versi 4.0, Gulp memperkenalkan dua fungsi baru: gulp.series dan gulp.parallel. API ini memungkinkan tugas dijalankan secara seri atau paralel.
Dari empat fungsi ini, tiga yang pertama sangat penting untuk file Gulp apa pun. memungkinkan tugas untuk didefinisikan dan dipanggil dari antarmuka baris perintah. Yang keempat adalah yang membuat Gulp benar-benar otomatis dengan memungkinkan tugas dijalankan saat file berubah.
file gulp
Ini adalah gulpfile dasar:
gulp.task('test', function{ gulp.src('test.txt') .pipe(gulp.dest('out')); });
Ini menjelaskan tugas tes sederhana. Saat dipanggil, file test.txt di direktori kerja saat ini harus disalin ke direktori ./out . Cobalah dengan menjalankan Gulp:
touch test.txt # Create test.txt gulp test
Perhatikan bahwa metode .pipe
bukan bagian dari Gulp, ini node-stream API, menghubungkan aliran yang dapat dibaca (dihasilkan oleh gulp.src('test.txt')
) dengan aliran yang dapat ditulis (dihasilkan oleh gulp.dest('out')
). Semua komunikasi antara Gulp dan plugin didasarkan pada aliran. Ini memungkinkan kita menulis kode gulpfile dengan cara yang elegan.
Temui Plug
Sekarang kita memiliki beberapa gagasan tentang cara kerja Gulp, mari kita buat alat seperti Gulp kita sendiri: Plug.
Kita akan mulai dengan API plug.task. Seharusnya membiarkan kita mendaftarkan tugas, dan tugas harus dijalankan jika nama tugas dilewatkan dalam parameter perintah.
var plug = { task: onTask }; module.exports = plug; var tasks = {}; function onTask(name, callback){ tasks[name] = callback; }
Ini akan memungkinkan tugas untuk didaftarkan. Sekarang kita perlu membuat tugas ini dapat dieksekusi. Untuk mempermudah, kami tidak akan membuat peluncur tugas terpisah. Sebagai gantinya kami akan memasukkannya ke dalam implementasi plug kami.
Yang perlu kita lakukan adalah menjalankan tugas yang disebutkan dalam parameter baris perintah. Kami juga perlu memastikan bahwa kami mencoba melakukannya di loop eksekusi berikutnya, setelah semua tugas terdaftar. Cara termudah untuk melakukannya adalah menjalankan tugas dalam panggilan balik batas waktu, atau lebih disukai process.nextTick:
process.nextTick(function(){ var taskName = process.argv[2]; if (taskName && tasks[taskName]) { tasks[taskName](); } else { console.log('unknown task', taskName) } });
Tulis plugfile.js seperti ini:
var plug = require('./plug'); plug.task('test', function(){ console.log('hello plug'); })
… dan jalankan.
node plugfile.js test
Ini akan menampilkan:
hello plug
Subtugas
Gulp juga memungkinkan untuk menentukan subtugas pada pendaftaran tugas. Dalam hal ini, plug.task harus mengambil 3 parameter, nama, larik subtugas, dan fungsi panggilan balik. Mari kita terapkan ini.
Kami perlu memperbarui API tugas seperti:
var tasks = {}; function onTask(name) { if(Array.isArray(arguments[1]) && typeof arguments[2] === "function"){ tasks[name] = { subTasks: arguments[1], callback: arguments[2] }; } else if(typeof arguments[1] === "function"){ tasks[name] = { subTasks: [], callback: arguments[1] }; } else{ console.log('invalid task registration') } } function runTask(name){ if(tasks[name].subTasks){ tasks[name].subTasks.forEach(function(subTaskName){ runTask(subTaskName); }); } if(tasks[name].callback){ tasks[name].callback(); } } process.nextTick(function(){ if (taskName && tasks[taskName]) { runTask(taskName); } });
Sekarang jika plugfile.js kita terlihat seperti ini:
plug.task('subTask1', function(){ console.log('from sub task 1'); }) plug.task('subTask2', function(){ console.log('from sub task 2'); }) plug.task('test', ['subTask1', 'subTask2'], function(){ console.log('hello plug'); })
… menjalankannya
node plugfile.js test
… harus menampilkan:
from sub task 1 from sub task 2 hello plug
Perhatikan bahwa Gulp menjalankan subtugas secara paralel. Tetapi untuk menyederhanakannya, dalam implementasi kami, kami menjalankan subtugas secara berurutan. Gulp 4.0 memungkinkan ini untuk dikontrol menggunakan dua fungsi API barunya, yang akan kita terapkan nanti di artikel ini.
Sumber dan Tujuan
Plug tidak akan banyak berguna jika kita tidak mengizinkan file untuk dibaca dan ditulis. Jadi selanjutnya kita akan mengimplementasikan plug.src
. Metode di Gulp ini mengharapkan argumen yang berupa topeng file, nama file, atau larik topeng file. Ini mengembalikan aliran Node yang dapat dibaca.
Untuk saat ini, dalam implementasi src
kami, kami hanya akan mengizinkan nama file:
var plug = { task: onTask, src: onSrc }; var stream = require('stream'); var fs = require('fs'); function onSrc(fileName){ var src = new stream.Readable({ read: function (chunk) { }, objectMode: true }); //read file and send it to the stream fs.readFile(path, 'utf8', (e,data)=> { src.push({ name: path, buffer: data }); src.push(null); }); return src; }
Perhatikan bahwa kita menggunakan objectMode: true
, parameter opsional di sini. Ini karena aliran simpul bekerja dengan aliran biner secara default. Jika kita perlu meneruskan/menerima objek JavaScript melalui aliran, kita harus menggunakan parameter ini.
Seperti yang Anda lihat, kami membuat objek buatan:
{ name: path, //file name buffer: data //file content }
… dan meneruskannya ke sungai.
Di sisi lain, metode plug.dest harus menerima nama folder target dan mengembalikan aliran yang dapat ditulis yang akan menerima objek dari aliran .src. Segera setelah objek file diterima, itu akan disimpan ke folder target.
function onDest(path){ var writer = new stream.Writable({ write: function (chunk, encoding, next) { if (!fs.existsSync(path)) fs.mkdirSync(path); fs.writeFile(path +'/'+ chunk.name, chunk.buffer, (e)=> { next() }); }, objectMode: true }); return writer; }
Mari kita perbarui plugfile.js kita:

var plug = require('./plug'); plug.task('test', function(){ plug.src('test.txt') .pipe(plug.dest('out')) })
… buat test.txt
touch test.txt
… dan jalankan:
node plugfile.js test ls ./out
test.txt harus disalin ke folder ./out .
Gulp sendiri bekerja dengan cara yang sama, tetapi alih-alih objek file buatan kami, Gulp menggunakan objek vinil. Ini jauh lebih nyaman, karena tidak hanya berisi nama file dan konten tetapi juga informasi meta tambahan, seperti nama folder saat ini, path lengkap ke file, dan sebagainya. Ini mungkin tidak berisi seluruh buffer konten, tetapi memiliki aliran konten yang dapat dibaca sebagai gantinya.
Vinyl: Lebih Baik Dari File
Ada perpustakaan vinyl-fs yang luar biasa yang memungkinkan kita memanipulasi file yang direpresentasikan sebagai objek vinil. Ini pada dasarnya memungkinkan kita membuat aliran yang dapat dibaca dan ditulis berdasarkan topeng file.
Kita dapat menulis ulang fungsi plug menggunakan library vinyl-fs. Tetapi pertama-tama kita perlu menginstal vinyl-fs:
npm i vinyl-fs
Dengan ini diinstal, implementasi Plug baru kami akan terlihat seperti ini:
var vfs = require('vinyl-fs') function onSrc(fileName){ return vfs.src(fileName); } function onDest(path){ return vfs.dest(path); } // ...
… dan untuk mencobanya:
rm out/test.txt node plugFile.js test ls out/test.txt
Hasilnya harus tetap sama.
Plugin Gulp
Karena layanan Plug kami menggunakan konvensi aliran Gulp, kami dapat menggunakan plugin Gulp asli bersama dengan alat Plug kami.
Mari kita coba satu. Instal gulp-rename:
npm i gulp-rename
… dan perbarui plugfile.js untuk menggunakannya:
var plug = require('./app.js'); var rename = require('gulp-rename'); plug.task('test', function () { return plug.src('test.txt') .pipe(rename('renamed.txt')) .pipe(plug.dest('out')); });
Menjalankan plugfile.js sekarang seharusnya masih, Anda dapat menebaknya, menghasilkan hasil yang sama.
node plugFile.js test ls out/renamed.txt
Memantau Perubahan
Metode terakhir namun tidak kalah pentingnya adalah gulp.watch
Metode ini memungkinkan kita untuk mendaftarkan pendengar file dan memanggil tugas-tugas terdaftar ketika file berubah. Mari kita terapkan:
var plug = { task: onTask, src: onSrc, dest: onDest, watch: onWatch }; function onWatch(fileName, taskName){ fs.watchFile(fileName, (event, filename) => { if (filename) { tasks[taskName](); } }); }
Untuk mencobanya, tambahkan baris ini ke plugfile.js:
plug.watch('test.txt','test');
Sekarang pada setiap perubahan test.txt , file akan disalin ke folder keluar dengan namanya diubah.
Seri vs Paralel
Sekarang setelah semua fungsi dasar dari API Gulp diimplementasikan, mari kita melangkah lebih jauh. Versi Gulp yang akan datang akan berisi lebih banyak fungsi API. API baru ini akan membuat Gulp lebih kuat:
- gulp.parallel
- gulp.series
Metode ini memungkinkan pengguna untuk mengontrol urutan tugas yang dijalankan. Untuk mendaftarkan subtugas secara paralel gulp.parallel dapat digunakan, yang merupakan perilaku Gulp saat ini. Di sisi lain, gulp.series dapat digunakan untuk menjalankan subtugas secara berurutan, satu demi satu.
Asumsikan kita memiliki test1.txt dan test2.txt di folder saat ini. Untuk menyalin file-file itu ke folder luar secara paralel, mari kita buat plugfile:
var plug = require('./plug'); plug.task('subTask1', function(){ return plug.src('test1.txt') .pipe(plug.dest('out')) }) plug.task('subTask2', function(){ return plug.src('test2.txt') .pipe(plug.dest('out')) }) plug.task('test-parallel', plug.parallel(['subTask1', 'subTask2']), function(){ console.log('done') }) plug.task('test-series', plug.series(['subTask1', 'subTask2']), function(){ console.log('done') })
Untuk menyederhanakan implementasi, fungsi panggilan balik subtugas dibuat untuk mengembalikan alirannya. Ini akan membantu kami melacak siklus hidup streaming.
Kami akan mulai mengubah API kami:
var plug = { task: onTask, src: onSrc, dest: onDest, parallel: onParallel, series: onSeries };
Kami juga perlu memperbarui fungsi onTask , karena kami perlu menambahkan informasi meta tugas tambahan untuk membantu peluncur tugas kami menangani subtugas dengan benar.
function onTask(name, subTasks, callback){ if(arguments.length < 2){ console.error('invalid task registration',arguments); return; } if(arguments.length === 2){ if(typeof arguments[1] === 'function'){ callback = subTasks; subTasks = {series: []}; } } tasks[name] = subTasks; tasks[name].callback = function(){ if(callback) return callback(); }; } function onParallel(tasks){ return { parallel: tasks }; } function onSeries(tasks){ return { series: tasks }; }
Untuk mempermudah, kita akan menggunakan async.js, sebuah pustaka utilitas untuk menangani fungsi asinkron untuk menjalankan tugas secara paralel atau seri:
var async = require('async') function _processTask(taskName, callback){ var taskInfo = tasks[taskName]; console.log('task ' + taskName + ' is started'); var subTaskNames = taskInfo.series || taskInfo.parallel || []; var subTasks = subTaskNames.map(function(subTask){ return function(cb){ _processTask(subTask, cb); } }); if(subTasks.length>0){ if(taskInfo.series){ async.series(subTasks, taskInfo.callback); }else{ async.parallel(subTasks, taskInfo.callback); } }else{ var stream = taskInfo.callback(); if(stream){ stream.on('end', function(){ console.log('stream ' + taskName + ' is ended'); callback() }) }else{ console.log('task ' + taskName +' is completed'); callback(); } } }
Kami mengandalkan aliran simpul 'akhir' yang dipancarkan ketika aliran telah memproses semua pesan dan ditutup, yang merupakan indikasi bahwa subtugas selesai. Dengan async.js, kita tidak perlu berurusan dengan callback yang berantakan.
Untuk mencobanya, mari kita jalankan subtugas terlebih dahulu secara paralel:
node plugFile.js test-parallel
task test-parallel is started task subTask1 is started task subTask2 is started stream subTask2 is ended stream subTask1 is ended done
Dan jalankan subtugas yang sama secara seri:
node plugFile.js test-series
task test-series is started task subTask1 is started stream subTask1 is ended task subTask2 is started stream subTask2 is ended done
Kesimpulan
Itu saja, kami telah mengimplementasikan API Gulp dan dapat menggunakan plugin Gulp sekarang. Tentu saja, jangan gunakan Plug in proyek nyata, karena Gulp lebih dari sekadar apa yang telah kami implementasikan di sini. Saya harap latihan kecil ini akan membantu Anda memahami bagaimana Gulp bekerja di bawah tenda dan biarkan kami lebih lancar menggunakannya dan memperluasnya dengan plugin.