Gulp Under the Hood: การสร้างเครื่องมือทำงานอัตโนมัติบนสตรีม
เผยแพร่แล้ว: 2022-03-11นักพัฒนา Front-end ในปัจจุบันใช้เครื่องมือหลายอย่างเพื่อทำให้การปฏิบัติงานประจำวันเป็นไปโดยอัตโนมัติ โซลูชันยอดนิยมสามวิธี ได้แก่ Grunt, Gulp และ Webpack เครื่องมือเหล่านี้แต่ละอย่างสร้างขึ้นบนปรัชญาที่แตกต่างกัน แต่มีจุดมุ่งหมายร่วมกันเหมือนกัน นั่นคือ เพื่อปรับปรุงกระบวนการสร้างส่วนหน้า ตัวอย่างเช่น Grunt นั้นขับเคลื่อนด้วยการกำหนดค่าในขณะที่ Gulp แทบไม่มีการบังคับใช้อะไรเลย อันที่จริง Gulp อาศัยนักพัฒนาในการเขียนโค้ดเพื่อนำโฟลว์ของกระบวนการบิลด์ไปใช้ - งานบิลด์ต่างๆ
เมื่อพูดถึงการเลือกเครื่องมือเหล่านี้ สิ่งที่ฉันชอบคืออึก ทั้งหมดนี้เป็นวิธีแก้ปัญหาที่ง่าย รวดเร็ว และเชื่อถือได้ ในบทความนี้ เราจะมาดูกันว่าอึกทำงานอย่างไรภายใต้ประทุนด้วยการใช้เครื่องมือที่คล้ายกับอึกของเรา
อึก API
อึกมาพร้อมกับฟังก์ชันง่ายๆ สี่อย่าง:
- อึก.task
- gulp.src
- gulp.dest
- gulp.watch
ฟังก์ชันที่เรียบง่ายทั้งสี่นี้ ในชุดค่าผสมต่างๆ ให้พลังและความยืดหยุ่นของอึก ในเวอร์ชัน 4.0 อึกแนะนำสองฟังก์ชันใหม่: gulp.series และ gulp.parallel API เหล่านี้อนุญาตให้รันงานเป็นชุดหรือแบบคู่ขนาน
จากสี่ฟังก์ชันนี้ สามฟังก์ชันแรกจำเป็นอย่างยิ่งสำหรับไฟล์อึก อนุญาตให้กำหนดและเรียกใช้งานจากอินเทอร์เฟซบรรทัดคำสั่ง ประการที่สี่คือสิ่งที่ทำให้อึกเป็นอัตโนมัติอย่างแท้จริงโดยอนุญาตให้งานต่างๆ ทำงานเมื่อมีการเปลี่ยนแปลงไฟล์
Gulpfile
นี่คือ gulpfile เบื้องต้น:
gulp.task('test', function{ gulp.src('test.txt') .pipe(gulp.dest('out')); });
มันอธิบายงานทดสอบอย่างง่าย เมื่อเรียกใช้ ไฟล์ test.txt ในไดเร็กทอรีการทำงานปัจจุบันควรถูกคัดลอกไปยังไดเร็กทอรี ./ out ลองใช้ Gulp:
touch test.txt # Create test.txt gulp test
สังเกตว่าเมธอด .pipe
ไม่ได้เป็นส่วนหนึ่งของ Gulp แต่เป็น node-stream API มันเชื่อมต่อสตรีมที่อ่านได้ (สร้างโดย gulp.src('test.txt')
) กับสตรีมที่เขียนได้ (สร้างโดย gulp.dest('out')
). การสื่อสารทั้งหมดระหว่างอึกและปลั๊กอินจะขึ้นอยู่กับสตรีม สิ่งนี้ทำให้เราเขียนโค้ด gulpfile ได้อย่างสง่างาม
พบกับ Plug
ตอนนี้เรามีแนวคิดเกี่ยวกับวิธีการทำงานของอึกแล้ว มาสร้างเครื่องมือคล้ายอึกของเรากันดีกว่า: ปลั๊ก
เราจะเริ่มต้นด้วย plug.task API มันควรจะให้เราลงทะเบียนงาน และงานควรจะดำเนินการถ้าชื่องานถูกส่งผ่านในพารามิเตอร์คำสั่ง
var plug = { task: onTask }; module.exports = plug; var tasks = {}; function onTask(name, callback){ tasks[name] = callback; }
ซึ่งจะช่วยให้สามารถลงทะเบียนงานได้ ตอนนี้ เราต้องทำให้งานนี้ปฏิบัติการได้ เพื่อให้ง่ายขึ้น เราจะไม่สร้างตัวเรียกใช้งานแยกต่างหาก แต่เราจะรวมไว้ในการใช้งานปลั๊กของเราแทน
สิ่งที่เราต้องทำคือเรียกใช้งานที่มีชื่อในพารามิเตอร์บรรทัดคำสั่ง เราต้องแน่ใจว่าเราพยายามทำมันในลูปการดำเนินการถัดไป หลังจากที่งานทั้งหมดได้รับการลงทะเบียนแล้ว วิธีที่ง่ายที่สุดในการดำเนินการคือเรียกใช้งานในการโทรกลับไทม์เอาต์ หรือควร process.nextTick:
process.nextTick(function(){ var taskName = process.argv[2]; if (taskName && tasks[taskName]) { tasks[taskName](); } else { console.log('unknown task', taskName) } });
เขียน plugfile.js ดังนี้:
var plug = require('./plug'); plug.task('test', function(){ console.log('hello plug'); })
… และเรียกใช้
node plugfile.js test
มันจะแสดง:
hello plug
งานย่อย
อึกยังอนุญาตให้กำหนดงานย่อยที่การลงทะเบียนงาน ในกรณีนี้ plug.task ควรใช้พารามิเตอร์ 3 ตัว ได้แก่ ชื่อ อาร์เรย์ของงานย่อย และฟังก์ชันเรียกกลับ มาดำเนินการนี้กัน
เราจะต้องอัปเดตงาน API เช่น:
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); } });
ตอนนี้ถ้า plugfile.js ของเรามีลักษณะดังนี้:
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'); })
… วิ่งมัน
node plugfile.js test
… ควรแสดง:
from sub task 1 from sub task 2 hello plug
โปรดทราบว่าอึกทำงานย่อยพร้อมกัน แต่เพื่อให้ง่ายขึ้น ในการใช้งานของเรา เรากำลังรันงานย่อยตามลำดับ อึก 4.0 ช่วยให้สามารถควบคุมได้โดยใช้ฟังก์ชัน API ใหม่สองฟังก์ชัน ซึ่งเราจะนำไปใช้ในบทความนี้ในภายหลัง
ต้นทางและปลายทาง
ปลั๊กจะมีประโยชน์เพียงเล็กน้อยหากเราไม่อนุญาตให้อ่านและเขียนไฟล์ได้ ต่อไปเราจะใช้ plug.src
เมธอดนี้ในอึกคาดหวังอาร์กิวเมนต์ที่เป็นไฟล์มาสก์ ชื่อไฟล์ หรืออาร์เรย์ของไฟล์มาสก์ ส่งคืน Node stream ที่อ่านได้
สำหรับตอนนี้ ในการใช้งาน src
ของเรา เราจะอนุญาตเพียงชื่อไฟล์:
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; }
โปรดทราบว่าเราใช้ objectMode: true
ซึ่งเป็นพารามิเตอร์ทางเลือกที่นี่ เนื่องจากโหนดสตรีมทำงานกับไบนารีสตรีมโดยค่าเริ่มต้น หากเราต้องส่ง/รับวัตถุ JavaScript ผ่านสตรีม เราต้องใช้พารามิเตอร์นี้
อย่างที่คุณเห็น เราได้สร้างวัตถุเทียมขึ้น:
{ name: path, //file name buffer: data //file content }
… และส่งต่อไปยังกระแสน้ำ
ในอีกด้านหนึ่ง วิธี plug.dest ควรได้รับชื่อโฟลเดอร์เป้าหมายและส่งคืนสตรีมที่เขียนได้ซึ่งจะได้รับอ็อบเจ็กต์จากสตรีม .src ทันทีที่ได้รับไฟล์วัตถุ วัตถุนั้นจะถูกเก็บไว้ในโฟลเดอร์เป้าหมาย
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; }
ให้เราอัปเดต plugfile.js ของเรา:
var plug = require('./plug'); plug.task('test', function(){ plug.src('test.txt') .pipe(plug.dest('out')) })
… สร้าง test.txt

touch test.txt
… และเรียกใช้:
node plugfile.js test ls ./out
ควรคัดลอก test.txt ไปยังโฟลเดอร์ ./ out
อึกทำงานในลักษณะเดียวกัน แต่แทนที่จะใช้วัตถุไฟล์ประดิษฐ์ของเรามันใช้วัตถุไวนิล สะดวกกว่ามาก เนื่องจากไม่ได้มีเพียงชื่อไฟล์และเนื้อหาเท่านั้น แต่ยังมีข้อมูลเมตาเพิ่มเติมอีกด้วย เช่น ชื่อโฟลเดอร์ปัจจุบัน เส้นทางแบบเต็มไปยังไฟล์ และอื่นๆ อาจไม่มีบัฟเฟอร์เนื้อหาทั้งหมด แต่มีสตรีมเนื้อหาที่สามารถอ่านได้แทน
ไวนิล: ดีกว่าไฟล์
มี library vinyl-fs ที่ยอดเยี่ยมที่ช่วยให้เราจัดการไฟล์ที่แสดงเป็นอ็อบเจกต์ไวนิลได้ โดยพื้นฐานแล้วจะช่วยให้เราสร้างสตรีมที่อ่านได้และเขียนได้โดยใช้รูปแบบไฟล์
เราสามารถเขียนฟังก์ชันปลั๊กใหม่โดยใช้ไลบรารีไวนิล-fs แต่ก่อนอื่นเราต้องติดตั้ง vinyl-fs:
npm i vinyl-fs
เมื่อติดตั้งแล้ว การใช้งาน Plug ใหม่ของเราจะมีลักษณะดังนี้:
var vfs = require('vinyl-fs') function onSrc(fileName){ return vfs.src(fileName); } function onDest(path){ return vfs.dest(path); } // ...
… และลองใช้งาน:
rm out/test.txt node plugFile.js test ls out/test.txt
ผลลัพธ์ควรยังคงเหมือนเดิม
อึก Plugins
เนื่องจากบริการ Plug ของเราใช้หลักการสตรีม Gulp เราจึงสามารถใช้ปลั๊กอิน Gulp ดั้งเดิมร่วมกับเครื่องมือ Plug ของเราได้
มาลองดูกัน ติดตั้งอึกเปลี่ยนชื่อ:
npm i gulp-rename
… และอัปเดต plugfile.js เพื่อใช้งาน:
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')); });
การรัน plugfile.js ในตอนนี้คุณควรจะยังให้ผลลัพธ์เหมือนเดิม
node plugFile.js test ls out/renamed.txt
การติดตามการเปลี่ยนแปลง
วิธีสุดท้ายแต่ไม่ท้ายสุดคือ gulp.watch
วิธีนี้ช่วยให้เราลงทะเบียน file listener และเรียกใช้งานที่ลงทะเบียนเมื่อไฟล์เปลี่ยนแปลง มาดำเนินการกันเถอะ:
var plug = { task: onTask, src: onSrc, dest: onDest, watch: onWatch }; function onWatch(fileName, taskName){ fs.watchFile(fileName, (event, filename) => { if (filename) { tasks[taskName](); } }); }
หากต้องการลองใช้ ให้เพิ่มบรรทัดนี้ใน plugfile.js:
plug.watch('test.txt','test');
ตอนนี้ในการเปลี่ยนแปลงแต่ละครั้งของ test.txt ไฟล์จะถูกคัดลอกไปยังโฟลเดอร์ out โดยเปลี่ยนชื่อ
ซีรีส์ vs Parallel
เมื่อฟังก์ชันพื้นฐานทั้งหมดจาก API ของอึกถูกใช้งานแล้ว เรามาก้าวไปอีกขั้น Gulp เวอร์ชันต่อไปจะมีฟังก์ชัน API เพิ่มเติม API ใหม่นี้จะทำให้อึกมีประสิทธิภาพมากขึ้น:
- อึก.ขนาน
- gulp.series
เมธอดเหล่านี้อนุญาตให้ผู้ใช้ควบคุมลำดับการทำงาน ในการลงทะเบียนงานย่อยแบบขนาน gulp.parallel อาจใช้ ซึ่งเป็นพฤติกรรมอึกในปัจจุบัน ในทางกลับกัน gulp.series อาจใช้เพื่อรันงานย่อยในลักษณะที่ต่อเนื่องกัน
สมมติว่าเรามี test1.txt และ test2.txt ในโฟลเดอร์ปัจจุบัน ในการคัดลอกไฟล์เหล่านั้นไปยังโฟลเดอร์ out แบบขนาน ให้เราสร้าง 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') })
เพื่อทำให้การใช้งานง่ายขึ้น ฟังก์ชันเรียกกลับงานย่อยถูกสร้างขึ้นเพื่อส่งคืนสตรีม ซึ่งจะช่วยให้เราติดตามวงจรชีวิตของสตรีมได้
เราจะเริ่มแก้ไข API ของเรา:
var plug = { task: onTask, src: onSrc, dest: onDest, parallel: onParallel, series: onSeries };
เราจะต้องอัปเดตฟังก์ชัน onTask ด้วย เนื่องจากเราจำเป็นต้องเพิ่มข้อมูลเมตาของงานเพิ่มเติม เพื่อช่วยให้ตัวเรียกใช้งานของเราจัดการกับงานย่อยได้อย่างเหมาะสม
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 }; }
เพื่อให้ง่ายขึ้น เราจะใช้ async.js ซึ่งเป็นไลบรารียูทิลิตี้สำหรับจัดการกับฟังก์ชันอะซิงโครนัสเพื่อรันงานแบบขนานหรือแบบอนุกรม:
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(); } } }
เราอาศัย 'จุดสิ้นสุด' ของโหนดสตรีม ซึ่งปล่อยออกมาเมื่อสตรีมได้ประมวลผลข้อความทั้งหมดและถูกปิด ซึ่งเป็นการบ่งชี้ว่างานย่อยเสร็จสมบูรณ์ ด้วย async.js เราไม่ต้องจัดการกับการเรียกกลับที่ยุ่งเหยิง
ในการทดลองใช้งาน ให้เรารันงานย่อยแบบขนานกันก่อน:
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
และรันงานย่อยเดียวกันเป็นชุด:
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
บทสรุป
เพียงเท่านี้ เราก็ได้ติดตั้ง Gulp's API และสามารถใช้ปลั๊กอิน Gulp ได้แล้ว แน่นอน อย่าใช้ Plug in โปรเจ็กต์จริง เพราะอึกเป็นมากกว่าสิ่งที่เราได้นำมาใช้ที่นี่ ฉันหวังว่าแบบฝึกหัดเล็ก ๆ นี้จะช่วยให้คุณเข้าใจว่าอึกทำงานอย่างไรภายใต้ประทุน และให้เราใช้และขยายมันด้วยปลั๊กอินได้อย่างคล่องแคล่วมากขึ้น