DanLevy.net

اختبار: إتقان الإدخال/الإخراج في 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 افتراضيًا عندما لا يتم تحديد ترميز. إذا كنت تريد سلسلة نصية، فأنت بحاجة إما إلى:

  1. تحديد ترميز: fs.readFileSync('test.txt', 'utf8')
  2. تحويل Buffer: content.toString()

تعرف على المزيد حول fs.readFileSync في وثائق Node.js

ما مجموعة الأحداث الشائعة الاستخدام مع التدفقات القابلة للقراءة؟

تصدر التدفقات القابلة للقراءة عدة أحداث مهمة:

  • ‘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().

تعلم المزيد عن pipe()

ماذا يفعل الخيار recursive؟

import fs from 'fs';
fs.mkdirSync('./a/b/c', { recursive: true });

الخيار recursive: true ينشئ الأدلة الأصلية إذا لم تكن موجودة. بدون هذا الخيار، محاولة إنشاء './a/b/c' ستؤدي إلى خطأ إذا كان './a' أو './a/b' غير موجودين.

هذا مشابه لأمر الصدفة mkdir -p.

تعلم المزيد عن mkdir

ماذا سيكون الناتج؟

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 يتم:

  1. تحويله إلى string
  2. تحويله إلى uppercase
  3. تمريره إلى stdout

هذا ينشئ pipeline يحول كل الإدخال إلى uppercase.

تعلم المزيد عن Transform streams

كم مرة يتم ضمان تشغيل fs.watch() عند تعديل ملف؟

import fs from 'fs';
fs.watch('test.txt', (eventType, filename) => {
console.log(`${filename} was changed`);
});
// Then modify test.txt once

fs.watch() ليس مضمونًا للتشغيل مرة واحدة بالضبط لكل تغيير منطقي في الملف. غالبًا ما يتم تشغيله عدة مرات لأن العديد من محررات النصوص:

  1. تحفظ في ملف مؤقت
  2. تعيد تسميته إلى الملف الهدف

للحصول على مراقبة أكثر موثوقية، فكر في استخدام:

  • حزمة chokidar
  • إزالة الارتداد (debouncing) من رد الاتصال
  • استخدام fs.watchFile() (على الرغم من أنه أقل كفاءة)

تعرف على المزيد حول fs.watch()

ما هو الناتج؟

const buf1 = Buffer.from('Hello');
const buf2 = Buffer.from('Hello');
console.log(buf1 === buf2);

تتم مقارنة المخازن المؤقتة بالمرجع، وليس بالقيمة. على الرغم من أنها تحتوي على نفس البيانات، إلا أنها كائنات مختلفة.

لمقارنة محتويات المخزن المؤقت، استخدم:

buf1.equals(buf2) // true
// or
Buffer.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؟

يمكن للتدفقات أن تعمل في:

  1. الوضع الثنائي (الافتراضي): للمخازن المؤقتة والسلاسل النصية
  2. وضع الكائن: لأي قيمة جافاسكريبت

مثال على وضع الكائن:

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>

تعلم المزيد عن ترميز UTF-8

آمل أنك استمتعت باختبار معرفتك بـ NodeJS IO! هل تريد المزيد؟ اطّلع على مجموعة الاختبارات لمزيد من التحديات!