كيفية جعل مخرجات الطرفية تفاعلية وممتعة في JavaScript وNode.js

دقائق القراءة: 6
مخرجات الطرفية التفاعلية في جافاسكربت وNode.js مع تأثير كتابة تدريجي

لا يجب أن تكون مخرجات الطرفية مملة دائماً. في كثير من المشاريع، نستخدم console.log لعرض الرسائل، لكن يمكن تحسين التجربة بصرياً وجعل الإخراج أكثر تفاعلاً ومتعة، خصوصاً عند بناء أدوات سطر أوامر، أو أثناء العروض التوضيحية، أو حتى عند تطوير سكربتات تعليمية.

في هذا الدليل، ستتعلم كيف تنشئ وظيفة مخصصة تعرض النص حرفاً حرفاً داخل الطرفية باستخدام JavaScript وNode.js، مع إمكانية إضافة تأخير ثابت أو عشوائي بين الأحرف للحصول على تأثير يشبه الكتابة الحية.

مثال على تأثير طباعة الأحرف تدريجياً في مخرجات الطرفية باستخدام JavaScript

لماذا قد تحتاج إلى تحسين مخرجات console؟

رغم أن الهدف الأساسي من سجل الطرفية هو عرض المعلومات، فإن تحسين طريقة الإخراج يمنحك عدة مزايا:

  • جعل الأدوات البرمجية أكثر احترافية وسهولة في التفاعل.
  • إضافة طابع بصري ممتع أثناء تشغيل السكربتات.
  • محاكاة تأثير الكتابة التدريجية في التطبيقات التعليمية أو العروض.
  • إتاحة مساحة لتخصيص الرسائل لاحقاً بالألوان أو التوقفات الذكية.

الخطوة الأولى: إنشاء دالة بسيطة لطباعة النص

لنبدأ بأبسط صورة ممكنة: دالة تستقبل سلسلة نصية ثم تعرضها باستخدام console.log.

const log = (s) => {
  console.log(s);
};

هذه الدالة مناسبة لطباعة السطر كاملاً دفعة واحدة، لكنها لا تحقق تأثير الإظهار التدريجي الذي نريده.

الخطوة الثانية: طباعة النص حرفاً حرفاً

لإنشاء تأثير كتابة متتابعة، نحتاج إلى تقسيم النص منطقياً عبر المرور على كل حرف بشكل منفصل. يمكننا تنفيذ ذلك باستخدام حلقة for...of.

const log = (s) => {
  for (const c of s) {
    console.log(c);
  }
};

المشكلة هنا أن كل استدعاء لـ console.log يضيف سطراً جديداً، وبالتالي سيظهر كل حرف في سطر مستقل، وهو سلوك غير مناسب لهذا النوع من التأثيرات.

الخطوة الثالثة: حل مشكلة الانتقال إلى سطر جديد

بدلاً من استخدام console.log، يمكن الاعتماد على process.stdout.write في بيئة Node.js. هذه الطريقة تكتب النص مباشرة إلى الطرفية من دون إضافة سطر جديد تلقائياً.

const log = (s) => {
  for (const c of s) {
    process.stdout.write(c);
  }
  process.stdout.write('\n');
};

بهذا الشكل ستظهر الأحرف على السطر نفسه، ثم نضيف في النهاية محرف السطر الجديد \n يدوياً للحفاظ على تنسيق الإخراج.

الخطوة الرابعة: إنشاء دالة sleep للتأخير الزمني

في JavaScript لا يمكن إيقاف التنفيذ المتزامن مؤقتاً بشكل مباشر. لذلك نحتاج إلى دالة تُعيد Promise وتنتهي بعد عدد معين من المللي ثانية.

const sleep = (ms) => {
  return new Promise(resolve => setTimeout(resolve, ms));
};

هذه الدالة ستكون الأساس الذي سنستخدمه لتأخير طباعة كل حرف عن الحرف الذي يسبقه.

الخطوة الخامسة: إضافة التأخير بين الأحرف

الآن أصبح بإمكاننا تحويل الدالة log إلى دالة غير متزامنة باستخدام async، ثم نضيف استدعاء await sleep(...) داخل الحلقة.

const sleep = (ms) => {
  return new Promise(resolve => setTimeout(resolve, ms));
};

const log = async (s, delay) => {
  for (const c of s) {
    process.stdout.write(c);
    await sleep(delay);
  }
  process.stdout.write('\n');
};

بهذا التعديل ستُعرض الأحرف تباعاً وفق المدة المخزنة في المتغير delay.

الخطوة السادسة: جعل التأخير عشوائياً لمظهر أكثر طبيعية

إذا كان التأخير ثابتاً دائماً، فقد يبدو الإخراج آلياً أكثر من اللازم. لذلك يمكننا إضافة معامل جديد مثل randomized لتحديد ما إذا كنا نريد زمناً ثابتاً أو عشوائياً بين كل حرف وآخر.

const sleep = (ms) => {
  return new Promise(resolve => setTimeout(resolve, ms));
};

const log = async (s, delay, randomized) => {
  for (const c of s) {
    process.stdout.write(c);
    await sleep((randomized ? Math.random() : 1) * delay);
  }
  process.stdout.write('\n');
};

في هذا المثال:

  • إذا كانت قيمة randomized هي true، فسيُحسب التأخير عشوائياً بين 0 وdelay.
  • إذا كانت false، فسيبقى التأخير ثابتاً.

وإذا كنت تفضّل أسلوباً أوضح من العامل الثلاثي، يمكنك استخدام if التقليدية:

if (randomized) {
  sleep(Math.random() * delay);
} else {
  sleep(delay);
}

لكن تذكّر أن الاستدعاء الفعلي داخل الحلقة يحتاج إلى await حتى يعمل التأخير كما هو متوقع.

الخطوة السابعة: تحويل الوظيفة إلى أداة قابلة للتهيئة

حتى الآن، كل مرة نريد فيها طباعة نص يجب علينا تمرير delay وrandomized يدوياً. هذا الأسلوب يعمل، لكنه ليس عملياً عندما تتكرر الاستدعاءات كثيراً.

log('Hello, world!', 100, true);
log('What\'s up?', 100, true);
log('How are you?', 100, true);

الأفضل هو إنشاء مُسجل قابل للتهيئة مرة واحدة، ثم استخدامه لاحقاً لتمرير النص فقط. لهذا سنبني دالة باسم funkylog تستقبل كائناً إعدادياً يحوي القيم المطلوبة، ثم تُعيد دالة جديدة مسؤولة عن الطباعة.

const funkylog = ({ delay, randomized }) => {
  const sleep = (ms) => {
    return new Promise(resolve => setTimeout(resolve, ms));
  };

  return async (s) => {
    for (const c of s) {
      process.stdout.write(c);
      await sleep((randomized ? Math.random() : 1) * delay);
    }
    process.stdout.write('\n');
  };
};

هذا التصميم يمنحك مرونة أعلى، لأنك تنشئ وظيفة طباعة بإعدادات ثابتة، ثم تعيد استخدامها في أي موضع داخل التطبيق.

الخطوة الثامنة: اللمسة الأخيرة وإضافة قيمة افتراضية

بعد بناء funkylog، يمكن استعمالها بهذه الصورة:

const log = funkylog({ delay: 100, randomized: true });

log('Hello, world!');
log('What\'s up?');
log('How are you?');

أصبح لدينا الآن:

  • مُسجل قابل لإعادة الاستخدام.
  • إمكانية تحديد التأخير مرة واحدة فقط.
  • طريقة أبسط لاستدعاء الطباعة.
  • قابلية عالية للتطوير لاحقاً.

ولزيادة السهولة، يمكن تعيين قيمة افتراضية للمتغير delay:

const funkylog = ({ delay = 100, randomized } = {}) => {
  const sleep = (ms) => {
    return new Promise(resolve => setTimeout(resolve, ms));
  };

  return async (s) => {
    for (const c of s) {
      process.stdout.write(c);
      await sleep((randomized ? Math.random() : 1) * delay);
    }
    process.stdout.write('\n');
  };
};

لاحظ هنا أننا أضفنا أيضاً القيمة الافتراضية = {} للوسيط كله، حتى تتمكن من استدعاء funkylog() بدون أخطاء.

وبذلك يمكنك إنشاء المُسجل حتى لو لم تمرر أي إعدادات:

const log = funkylog();
log('Hello, world!');

أفكار عملية لتطوير funkylog

ما أن تنتهي من النسخة الأساسية، يمكنك توسيعها بطرق مفيدة تجعل الإخراج أكثر أناقة واحترافية:

1. إضافة الألوان إلى النص

يمكنك استخدام مكتبة chalk من خلال npm لتلوين الرسائل وتمييز التحذيرات أو النجاح أو الأخطاء.

2. إضافة تأخير بين الكلمات

بدلاً من التعامل مع الأحرف فقط، يمكنك إدخال توقفات أطول عند المسافات كي يبدو الإخراج أقرب إلى الكتابة البشرية.

3. بناء أوضاع إخراج متعددة

  • وضع سريع للرسائل القصيرة.
  • وضع عشوائي للعروض التفاعلية.
  • وضع ملون للواجهات النصية.

4. دعم سلاسل مخصصة للرسائل

يمكنك لاحقاً ربط funkylog بمستويات تسجيل مثل info وwarn وerror مع أنماط إخراج مختلفة لكل نوع.

متى يكون هذا الأسلوب مناسباً؟

رغم أن تأثير الطباعة التدريجية ممتع، من الأفضل استخدامه في السياقات الصحيحة فقط، مثل:

  • أدوات سطر الأوامر التعليمية.
  • العروض التوضيحية والمنتجات التفاعلية.
  • المشاريع الشخصية التي تهتم بالتجربة البصرية.

أما في التطبيقات التي تتطلب سرعة عالية في الإخراج أو سجلات تشغيل كثيفة، فقد يكون التأخير غير مناسب ويؤثر في تجربة الاستخدام.

الخلاصة التقنية

إنشاء مُخرج طرفية تفاعلي في JavaScript وNode.js ليس معقداً، لكنه يوضح فكرة مهمة: حتى الأدوات البسيطة مثل console يمكن تطويرها لتصبح أكثر مرونة وجاذبية. استخدام process.stdout.write مع async/await ودالة sleep يمنحك تحكماً دقيقاً في طريقة ظهور النص، بينما يضيف التغليف داخل funkylog مستوى ممتازاً من التنظيم وقابلية إعادة الاستخدام. تقنياً، هذا النمط مفيد جداً عند بناء أدوات سطر أوامر مخصصة أو تجارب عرض تفاعلية خفيفة داخل الطرفية.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *