Gulp Under the Hood: إنشاء أداة لأتمتة المهام قائمة على التدفق
نشرت: 2022-03-11يستخدم مطورو الواجهة الأمامية في الوقت الحاضر أدوات متعددة لأتمتة العمليات الروتينية. ثلاثة من أشهر الحلول هي Grunt و Gulp و Webpack. كل من هذه الأدوات مبنية على فلسفات مختلفة ، لكنها تشترك في نفس الهدف المشترك: تبسيط عملية بناء الواجهة الأمامية. على سبيل المثال ، يعتمد Grunt على التكوين بينما لا يفرض Gulp أي شيء تقريبًا. في الواقع ، يعتمد Gulp على المطور كتابة التعليمات البرمجية لتنفيذ تدفق عمليات البناء - مهام البناء المختلفة.
عندما يتعلق الأمر باختيار إحدى هذه الأدوات ، فإن المفضل لدي هو Gulp. الكل في الكل هو حل بسيط وسريع وموثوق. في هذه المقالة سوف نرى كيف يعمل Gulp تحت غطاء المحرك عن طريق أخذ طعنة في تنفيذ أداة تشبه Gulp الخاصة بنا.
واجهة برمجة تطبيقات Gulp
يأتي Gulp بأربع وظائف بسيطة فقط:
- مهمة
- gulp.src
- بلع
- راقب
توفر هذه الوظائف الأربع البسيطة ، في مجموعات مختلفة ، كل قوة ومرونة Gulp. في الإصدار 4.0 ، قدم Gulp وظيفتين جديدتين: gulp.series و gulp.parallel. تسمح واجهات برمجة التطبيقات (API) بتشغيل المهام بالتسلسل أو بالتوازي.
من بين هذه الوظائف الأربع ، تعتبر الوظائف الثلاثة الأولى ضرورية للغاية لأي ملف Gulp. السماح بتحديد المهام واستدعائها من واجهة سطر الأوامر. العامل الرابع هو ما يجعل Gulp آليًا حقًا من خلال السماح بتشغيل المهام عند تغيير الملفات.
ملف
هذا ملف غالب أولي:
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 ، إنها واجهة برمجة تطبيقات دفق العقدة ، فهي تربط دفقًا قابلًا للقراءة (تم إنشاؤه بواسطة gulp.src('test.txt') ) مع دفق قابل للكتابة (تم إنشاؤه بواسطة gulp.dest('out') ). تستند جميع الاتصالات بين Gulp والمكونات الإضافية إلى التدفقات. هذا يتيح لنا كتابة كود gulpfile بطريقة أنيقة.
لقاء مع بلج
الآن بعد أن أصبح لدينا فكرة عن كيفية عمل Gulp ، فلنقم ببناء أداة تشبه Gulp الخاصة بنا: Plug.
سنبدأ مع plug.task API. يجب أن يسمح لنا بتسجيل المهام ، ويجب تنفيذ المهام إذا تم تمرير اسم المهمة في معلمات الأمر.
var plug = { task: onTask }; module.exports = plug; var tasks = {}; function onTask(name, callback){ tasks[name] = callback; }سيسمح ذلك بتسجيل المهام. الآن نحن بحاجة إلى جعل هذه المهمة قابلة للتنفيذ. لإبقاء الأمور بسيطة ، لن نقوم بإنشاء مشغل مهام منفصل. بدلاً من ذلك ، سنقوم بتضمينه في تنفيذ المكونات الخاصة بنا.
كل ما نحتاجه هو تشغيل المهام المسماة في معلمات سطر الأوامر. نحتاج أيضًا إلى التأكد من أننا نحاول القيام بذلك في حلقة التنفيذ التالية ، بعد تسجيل جميع المهام. أسهل طريقة للقيام بذلك هي تشغيل المهام في رد اتصال مهلة ، أو يفضل العملية.
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المهام الفرعية
يسمح Gulp أيضًا بتحديد المهام الفرعية عند تسجيل المهمة. في هذه الحالة ، يجب أن يأخذ plug.task 3 معاملات ، الاسم ، مصفوفة المهام الفرعية ، ووظيفة رد الاتصال. دعونا ننفذ هذا.
سنحتاج إلى تحديث واجهة برمجة تطبيقات المهام على هذا النحو:
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لاحظ أن Gulp يدير المهام الفرعية بالتوازي. ولكن لتبسيط الأمور ، فإننا في تنفيذنا نقوم بتشغيل المهام الفرعية بالتتابع. يسمح Gulp 4.0 بالتحكم في ذلك باستخدام وظيفتي API الجديدتين ، والتي سننفذها لاحقًا في هذه المقالة.
المصدر والوجهة
لن يكون للمكونات فائدة تذكر إذا لم نسمح بقراءة الملفات والكتابة إليها. بعد ذلك plug.src . تتوقع هذه الطريقة في Gulp وسيطة تكون إما قناع ملف أو اسم ملف أو مجموعة من أقنعة الملفات. تقوم بإرجاع دفق عقدة قابل للقراءة.
في الوقت الحالي ، في تطبيقنا لـ 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 .
يعمل Gulp نفسه بنفس الطريقة تقريبًا ، ولكن بدلاً من كائنات الملفات الاصطناعية الخاصة بنا ، فإنه يستخدم كائنات الفينيل. إنه أكثر ملاءمة ، لأنه لا يحتوي فقط على اسم الملف والمحتوى ولكن أيضًا معلومات تعريفية إضافية ، مثل اسم المجلد الحالي والمسار الكامل للملف وما إلى ذلك. قد لا يحتوي على المخزن المؤقت للمحتوى بالكامل ، ولكنه يحتوي على دفق يمكن قراءته من المحتوى بدلاً من ذلك.
الفينيل: أفضل من الملفات
هناك مكتبة فينيل مكتبة ممتازة تتيح لنا معالجة الملفات الممثلة ككائنات من الفينيل. يتيح لنا بشكل أساسي إنشاء تدفقات قابلة للقراءة وقابلة للكتابة بناءً على قناع الملف.
يمكننا إعادة كتابة وظائف التوصيل باستخدام مكتبة vinyl-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يجب أن تظل النتائج هي نفسها.
الإضافات Gulp
نظرًا لأن خدمة Plug الخاصة بنا تستخدم اصطلاح Gulp Stream ، يمكننا استخدام مكونات Gulp الإضافية مع أداة Plug الخاصة بنا.
دعونا نجرب واحدة. تثبيت gulp-rename:
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 تسمح لنا هذه الطريقة بتسجيل مستمع الملف واستدعاء المهام المسجلة عندما تتغير الملفات. دعونا ننفذها:
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 ، سيتم نسخ الملف إلى مجلد الإخراج مع تغيير اسمه.
السلسلة مقابل الموازي
الآن بعد أن تم تنفيذ جميع الوظائف الأساسية من واجهة برمجة تطبيقات Gulp ، فلنأخذ الأمور خطوة أخرى إلى الأمام. سيحتوي الإصدار القادم من Gulp على المزيد من وظائف API. ستجعل واجهة برمجة التطبيقات الجديدة Gulp أكثر قوة:
- جرعة موازية
- سلسلة بلع
تسمح هذه الطرق للمستخدم بالتحكم في التسلسل الذي يتم تشغيل المهام فيه. لتسجيل المهام الفرعية بالتوازي يمكن استخدام gulp.parallel وهو سلوك Gulp الحالي. من ناحية أخرى ، يمكن استخدام gulp.series لتشغيل المهام الفرعية بطريقة متسلسلة ، واحدة تلو الأخرى.
افترض أن لدينا test1.txt و test2.txt في المجلد الحالي. من أجل نسخ هذه الملفات إلى مجلد خارجي بالتوازي ، دعنا نصنع ملفًا إضافيًا:
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(); } } }نحن نعتمد على دفق العقدة "end" الذي ينبعث عندما يعالج الدفق جميع الرسائل ويغلق ، مما يشير إلى أن المهمة الفرعية قد اكتملت. مع 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 ويمكننا استخدام ملحقات Gulp الآن. بالطبع ، لا تستخدم مشاريع Plug in الحقيقية ، لأن Gulp أكثر من مجرد ما قمنا بتنفيذه هنا. آمل أن يساعدك هذا التمرين الصغير في فهم كيفية عمل Gulp تحت الغطاء والسماح لنا باستخدامه بطلاقة وتوسيعه باستخدام المكونات الإضافية.
