اختبار: إتقان الإدخال/الإخراج في NodeJS
اختبر معرفتك بالملفات والتدفقات والمخازن المؤقتة
مستعد للغوص في عالم NodeJS IO؟ 🌊
سيختبر هذا الاختبار فهمك لعمليات الإدخال/الإخراج في Node، من عمليات نظام الملفات الأساسية إلى مفاهيم التدفق المتقدمة. سنغطي الـ buffers، الـ encoding، وأفضل الممارسات للتعامل مع البيانات بكفاءة.
دعنا نرى مدى معرفتك بالـ streams من الـ buffers! 🚀
ماذا يفعل هذا الكود؟
const buf = Buffer.alloc(5);console.log(buf);Buffer.alloc(size) ينشئ مخزنًا مؤقتًا جديدًا بالحجم المحدد مملوءًا بالأصفار.
سيكون الناتج: <Buffer 00 00 00 00 00>
إذا كنت تريد إنشاء مخزن مؤقت ببيانات عشوائية، استخدم Buffer.allocUnsafe(5).
ماذا سيطبع هذا؟
const buf = Buffer.from([65]);console.log(buf.toString());الأرقام في المصفوفة تمثل رموز ASCII:
- 65: ‘A’
toString() تحول هذه البايتات إلى تمثيلها النصي باستخدام ترميز UTF-8 بشكل افتراضي.
ما هو ترتيب الإخراج؟
import fs from 'fs';fs.readFile('test.txt', 'utf8', (err, data) => { console.log(data);});console.log('Done');بما أن readFile غير متزامن، يستمر الكود في التنفيذ أثناء قراءة الملف.
لذلك، ستتم طباعة “تم” قبل محتوى الملف.
لانتظار قراءة الملف أولاً، يمكنك استخدام النسخة المعتمدة على Promise:
import { promises as fs } from 'fs';
async function read() { const data = await fs.readFile('test.txt', 'utf8'); console.log(data); console.log('Done');}ما الذي يعيده fs.readFileSync() افتراضيًا؟
import fs from 'fs';const content = fs.readFileSync('test.txt');fs.readFileSync() يعيد كائن Buffer افتراضيًا عندما لا يتم تحديد ترميز. إذا كنت تريد سلسلة نصية، فأنت بحاجة إما إلى:
- تحديد ترميز:
fs.readFileSync('test.txt', 'utf8') - تحويل Buffer:
content.toString()
ما مجموعة الأحداث الشائعة الاستخدام مع التدفقات القابلة للقراءة؟
تصدر التدفقات القابلة للقراءة عدة أحداث مهمة:
- ‘data’: عندما تكون البيانات متاحة للقراءة
- ‘end’: عندما لا توجد بيانات أخرى للقراءة
- ‘error’: عند حدوث خطأ
- ‘close’: عند إغلاق التدفق والمورد الأساسي
const readable = fs.createReadStream('file.txt');readable.on('data', chunk => console.log(chunk));readable.on('end', () => console.log('Done!'));ماذا يفعل هذا الكود؟
import fs from 'fs';const readable = fs.createReadStream('source.txt');const writable = fs.createWriteStream('dest.txt');readable.pipe(writable);تقوم pipe() بتوصيل تدفق قابل للقراءة بتدفق قابل للكتابة، وتدير الضغط الخلفي تلقائيًا وتنسخ البيانات في أجزاء دون تحميل الملف بأكمله في الذاكرة.
هذا أكثر كفاءة في استخدام الذاكرة للملفات الكبيرة مقارنة بـ fs.readFile() متبوعًا بـ fs.writeFile().
ماذا يفعل الخيار recursive؟
import fs from 'fs';fs.mkdirSync('./a/b/c', { recursive: true });الخيار recursive: true ينشئ الأدلة الأصلية إذا لم تكن موجودة.
بدون هذا الخيار، محاولة إنشاء './a/b/c' ستؤدي إلى خطأ إذا كان './a' أو './a/b' غير موجودين.
هذا مشابه لأمر الصدفة mkdir -p.
ماذا سيكون الناتج؟
import { Transform } from 'stream';const upperCase = new Transform({ transform(chunk, encoding, callback) { callback(null, chunk.toString().toUpperCase()); }});process.stdin .pipe(upperCase) .pipe(process.stdout);// Input: "hello world"تقوم Transform streams بتعديل البيانات أثناء مرورها. هنا، كل chunk يتم:
- تحويله إلى string
- تحويله إلى uppercase
- تمريره إلى stdout
هذا ينشئ pipeline يحول كل الإدخال إلى uppercase.
كم مرة يتم ضمان تشغيل fs.watch() عند تعديل ملف؟
import fs from 'fs';fs.watch('test.txt', (eventType, filename) => { console.log(`${filename} was changed`);});// Then modify test.txt oncefs.watch() ليس مضمونًا للتشغيل مرة واحدة بالضبط لكل تغيير منطقي في الملف. غالبًا ما يتم تشغيله عدة مرات لأن العديد من محررات النصوص:
- تحفظ في ملف مؤقت
- تعيد تسميته إلى الملف الهدف
للحصول على مراقبة أكثر موثوقية، فكر في استخدام:
- حزمة
chokidar - إزالة الارتداد (debouncing) من رد الاتصال
- استخدام
fs.watchFile()(على الرغم من أنه أقل كفاءة)
ما هو الناتج؟
const buf1 = Buffer.from('Hello');const buf2 = Buffer.from('Hello');console.log(buf1 === buf2);تتم مقارنة المخازن المؤقتة بالمرجع، وليس بالقيمة. على الرغم من أنها تحتوي على نفس البيانات، إلا أنها كائنات مختلفة.
لمقارنة محتويات المخزن المؤقت، استخدم:
buf1.equals(buf2) // true// orBuffer.compare(buf1, buf2) === 0 // trueما هو الغرض الرئيسي من الضغط العكسي للتيار؟
الضغط العكسي هو آلية تمنع تجاوز الذاكرة عن طريق إيقاف القراءة مؤقتًا عندما لا تستطيع نهاية الكتابة مواكبة ذلك.
مثال على الضغط العكسي اليدوي:
readable.on('data', (chunk) => { const canContinue = writable.write(chunk); if (!canContinue) { readable.pause(); writable.once('drain', () => readable.resume()); }});pipe() يتعامل مع هذا تلقائيًا!
ماذا يفعل هذا الكود؟
import fs from 'fs';fs.symlinkSync('target.txt', 'link.txt');symlinkSync ينشئ رابطًا رمزيًا (مثل اختصار) للملف الهدف.
الاختلافات الرئيسية عن الروابط الصلبة:
- يمكن الربط بالمجلدات
- يمكن أن يمتد عبر أنظمة الملفات
- ينكسر إذا تم حذف الهدف
لإنشاء رابط صلب بدلاً من ذلك:
fs.linkSync('target.txt', 'hardlink.txt');ما هي الأوضاع التي يمكن أن تعمل بها تدفقات Node.js؟
يمكن للتدفقات أن تعمل في:
- الوضع الثنائي (الافتراضي): للمخازن المؤقتة والسلاسل النصية
- وضع الكائن: لأي قيمة جافاسكريبت
مثال على وضع الكائن:
import { Transform } from 'stream';const objectStream = new Transform({ objectMode: true, transform(chunk, encoding, callback) { callback(null, { value: chunk }); }});ما نوع المعامل fd في هذه الدالة الاستدعائية؟
import fs from 'fs';fs.open('test.txt', 'r', (err, fd) => { console.log(typeof fd);});واصفات الملفات هي أرقام تحدد بشكل فريد الملفات المفتوحة في نظام التشغيل.
أول ثلاثة واصفات ملفات محجوزة:
- 0: الإدخال القياسي (stdin)
- 1: الإخراج القياسي (stdout)
- 2: الخطأ القياسي (stderr)
تذكر دائمًا إغلاق واصفات الملفات:
fs.close(fd, (err) => { if (err) throw err;});كم عدد البايتات التي ستستهلكها هذه السلسلة في UTF-8؟
const str = "Hello 🌍";const buf = Buffer.from(str);console.log(buf.length);في UTF-8:
- الأحرف ASCII (مثل ‘Hello ’) تأخذ بايت واحد لكل حرف
- رمز الأرض 🌍 يأخذ 4 بايتات
إذن: 5 (Hello) + 1 (مسافة) + 4 (🌍) = 10 بايتات
لرؤية البايتات:
console.log(buf); // <Buffer 48 65 6c 6c 6f 20 f0 9f 8c 8d>آمل أنك استمتعت باختبار معرفتك بـ NodeJS IO! هل تريد المزيد؟ اطّلع على مجموعة الاختبارات لمزيد من التحديات!