עבודה עם קבצים (fs ו-path)

אחד הדברים הראשונים ש-Node.js נותן ושדפדפן לא: גישה למערכת הקבצים. קריאת קובץ הגדרות, כתיבת לוג, יצירת תיקיות - כל אלה עוברים דרך שני מודולים מובנים: fs (File System) ו-path.

שלוש דרכים לאותה פעולה

למודול fs יש שלושה סגנונות, ובקוד ישן תפגשו את כולם:

סגנוןדוגמהמתי
Promises (מודרני)await readFile(path, 'utf8')ברירת המחדל לכל קוד חדש
סינכרוניreadFileSync(path, 'utf8')רק בסקריפטים קצרים או בעליית התוכנית
Callbacks (ישן)readFile(path, 'utf8', (err, data) => {})בקוד ישן בלבד

הגרסה הסינכרונית חוסמת את ה-Event Loop: בזמן שהקובץ נקרא, השרת לא עונה לאף משתמש. בשרת משתמשים תמיד בגרסת ה-Promises.

קריאת קובץ

import { readFile } from 'node:fs/promises';

try {
    const text = await readFile('notes.txt', 'utf8');
    console.log(text);
} catch (error) {
    if (error.code === 'ENOENT') {
        console.log('הקובץ לא קיים');
    } else {
        throw error;
    }
}
  • הקידומת node: מסמנת שזה מודול מובנה ולא חבילה מ-npm. היא לא חובה, אבל מומלצת.
  • בלי 'utf8' מקבלים Buffer (בייטים גולמיים) במקום מחרוזת.
  • ב-ES Module אפשר לכתוב await ישירות ברמה העליונה של הקובץ (Top-level await).

כתיבה והוספה

import { writeFile, appendFile } from 'node:fs/promises';

// יוצר את הקובץ, או דורס אותו אם הוא קיים
await writeFile('report.txt', 'דוח יומי\n', 'utf8');

// מוסיף לסוף הקובץ בלי למחוק את מה שהיה
await appendFile('app.log', `${new Date().toISOString()} השרת עלה\n`);

קריאה וכתיבה של JSON

זה הצירוף הכי נפוץ בפועל: קובץ הגדרות או "מסד נתונים" קטן.

import { readFile, writeFile } from 'node:fs/promises';

async function loadSettings() {
    const text = await readFile('settings.json', 'utf8');
    return JSON.parse(text);
}

async function saveSettings(settings) {
    // הפרמטר השלישי מוסיף הזחה, כדי שהקובץ יהיה קריא
    await writeFile('settings.json', JSON.stringify(settings, null, 2));
}

const settings = await loadSettings();
settings.theme = 'dark';
await saveSettings(settings);

תיקיות

import { mkdir, readdir, rm, stat } from 'node:fs/promises';

// recursive: יוצר גם תיקיות ביניים, ולא נכשל אם התיקייה קיימת
await mkdir('output/2026/reports', { recursive: true });

// רשימת קבצים, עם סוג כל פריט
const entries = await readdir('output', { withFileTypes: true });
for (const entry of entries) {
    console.log(entry.name, entry.isDirectory() ? '(תיקייה)' : '(קובץ)');
}

// גודל ותאריך שינוי
const info = await stat('settings.json');
console.log(info.size, 'בייטים, עודכן', info.mtime);

// מחיקת תיקייה עם כל התוכן שלה - בזהירות!
await rm('output/temp', { recursive: true, force: true });

path: לבנות נתיבים נכון

Windows משתמש ב-\ ו-Mac/Linux ב-/. חיבור נתיבים עם + שובר קוד בין מערכות. path מטפל בזה:

import path from 'node:path';

path.join('data', 'users', 'list.json');   // data/users/list.json (או \ ב-Windows)
path.resolve('data');                       // נתיב מוחלט מהתיקייה הנוכחית
path.basename('/tmp/photo.png');            // 'photo.png'
path.extname('photo.png');                  // '.png'
path.dirname('/tmp/photo.png');             // '/tmp'

נתיב ביחס לקובץ עצמו

נתיב יחסי כמו 'data.json' נמדד מהתיקייה שממנה הרצתם את node, לא מהתיקייה של הקובץ. כדי לקרוא קובץ שיושב ליד הקוד, בונים נתיב מהתיקייה של הקובץ:

import path from 'node:path';

// ES Modules, Node.js 20.11 ומעלה
const dataFile = path.join(import.meta.dirname, 'data.json');

// בקוד CommonJS ישן: path.join(__dirname, 'data.json')

קבצים גדולים: Streams

readFile טוען את כל הקובץ לזיכרון. לקובץ של כמה גיגה-בייט זה לא יעבוד. Stream קורא את הקובץ בחתיכות:

import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';

// ספירת שורות בקובץ לוג ענק, בלי לטעון אותו כולו
const lines = createInterface({ input: createReadStream('huge.log') });
let count = 0;
for await (const line of lines) {
    if (line.includes('ERROR')) count++;
}
console.log(`נמצאו ${count} שגיאות`);

דוגמאות מלאות להרצה

  • read-file.js - קריאה בשלושת הסגנונות, כולל JSON
  • write-file.js - כתיבה, הוספה וקבצים זמניים
  • directories.js - יצירה, סריקה ומחיקה של תיקיות

סיכום

  • בקוד חדש: node:fs/promises עם await, ו-try/catch סביב כל פעולה.
  • שגיאת ENOENT פירושה "הקובץ לא קיים".
  • בונים נתיבים עם path.join, ונתיב ליד הקוד עם import.meta.dirname.
  • לקבצים גדולים: Streams.

בדקו את עצמכם

נסו לענות לבד לפני שאתם פותחים את התשובה.

  1. מה אומרת שגיאה עם code === 'ENOENT'?

    1. הקובץ או התיקייה לא קיימים
    2. הקובץ ריק
    3. אין הרשאה
    4. הדיסק מלא
    הצגת התשובה

    תשובה א. Error NO ENTry: הנתיב לא נמצא.

  2. איך בונים נתיב לקובץ שיושב ליד הקוד, בקובץ ES Module?

    1. __dirname + 'data.json'
    2. C:/data.json
    3. path.join(import.meta.dirname, 'data.json')
    4. './data.json'
    הצגת התשובה

    תשובה ג. נתיב יחסי נמדד מהתיקייה שממנה הריצו את node, לא מהקובץ.

  3. מתי כדאי Stream במקום readFile?

    1. אף פעם
    2. תמיד
    3. בקבצים גדולים, כדי לא לטעון את כולם לזיכרון
    4. בקבצים קטנים
    הצגת התשובה

    תשובה ג. Stream קורא בחתיכות.