לדלג לתוכן

3.7 אחסון מקומי הרצאה

אחסון מקומי - Web Storage

עד עכשיו, כל הנתונים שיצרנו ב-JavaScript נעלמו כשהמשתמש סגר את הדף או רענן אותו. כל משתנה, כל רשימה, כל שינוי - הכל נמחק.

אבל מה אם רוצים לשמור נתונים בין טעינות? למשל:
- לזכור שהמשתמש בחר מצב כהה
- לשמור טופס שהמשתמש התחיל למלא
- לשמור עגלת קניות
- לשמור ציון גבוה במשחק

בשביל זה יש לנו Web Storage - מנגנון שמאפשר לשמור נתונים בדפדפן של המשתמש.


localStorage

localStorage הוא אובייקט גלובלי שזמין בכל דף. הוא מאפשר לשמור זוגות של key-value שנשמרים גם אחרי סגירת הדפדפן.

המתודות העיקריות

// save a value
localStorage.setItem("username", "Dan");

// read a value
let name = localStorage.getItem("username");  // "Dan"

// remove a specific item
localStorage.removeItem("username");

// clear everything
localStorage.clear();

// get the key at index i
let key = localStorage.key(0);  // returns the first key

// number of stored items
let count = localStorage.length;  // number of items

דוגמה בסיסית

// save
localStorage.setItem("color", "blue");
localStorage.setItem("fontSize", "16");

// read
let color = localStorage.getItem("color");     // "blue"
let size = localStorage.getItem("fontSize");   // "16"

// read non-existent key
let missing = localStorage.getItem("nothing"); // null

אם מנסים לקרוא key שלא קיים, מקבלים null.


הבעיה הגדולה - הכל הוא strings

localStorage שומר רק מחרוזות. זה אומר שכל מה ששומרים - הופך למחרוזת.

// this looks fine...
localStorage.setItem("age", 25);
let age = localStorage.getItem("age");
console.log(age);        // "25" - string, not number!
console.log(typeof age); // "string"

// this is a problem
localStorage.setItem("isAdmin", true);
let isAdmin = localStorage.getItem("isAdmin");
console.log(isAdmin);        // "true" - string!
console.log(typeof isAdmin); // "string"
// "true" as a string is truthy, but so is "false"!

// this is a bigger problem
let user = { name: "Dan", age: 25 };
localStorage.setItem("user", user);
let saved = localStorage.getItem("user");
console.log(saved);  // "[object Object]" - useless!

אובייקטים ומערכים הופכים ל-"[object Object]" - מה שלא עוזר לנו בכלל.


הפתרון - JSON.stringify ו-JSON.parse

הפתרון הוא להמיר לJSON לפני שמירה, ולהמיר חזרה אחרי קריאה:

// SAVING - convert to JSON string
let user = { name: "Dan", age: 25, isAdmin: true };
localStorage.setItem("user", JSON.stringify(user));
// stored as: '{"name":"Dan","age":25,"isAdmin":true}'

// READING - parse back to object
let saved = JSON.parse(localStorage.getItem("user"));
console.log(saved);        // { name: "Dan", age: 25, isAdmin: true }
console.log(saved.name);   // "Dan"
console.log(saved.age);    // 25 (number!)
console.log(saved.isAdmin); // true (boolean!)

אותו דבר עם מערכים:

// save array
let tasks = ["buy milk", "learn JS", "clean house"];
localStorage.setItem("tasks", JSON.stringify(tasks));

// read array
let savedTasks = JSON.parse(localStorage.getItem("tasks"));
console.log(savedTasks);    // ["buy milk", "learn JS", "clean house"]
console.log(savedTasks[0]); // "buy milk"

טיפול ב-null

כשקוראים key שלא קיים, getItem מחזיר null. אם נעביר null ל-JSON.parse, נקבל שגיאה. לכן חשוב לטפל בזה:

// safe reading pattern
let data = localStorage.getItem("tasks");
let tasks = data ? JSON.parse(data) : [];

// or in one line
let tasks = JSON.parse(localStorage.getItem("tasks")) || [];

הדפוס JSON.parse(localStorage.getItem(key)) || defaultValue הוא מאוד נפוץ.


sessionStorage

sessionStorage עובד בדיוק כמו localStorage - אותו API, אותן מתודות. ההבדל היחיד: הנתונים נמחקים כשהטאב נסגר.

// same API as localStorage
sessionStorage.setItem("temp", "this will disappear when tab closes");
let temp = sessionStorage.getItem("temp");
sessionStorage.removeItem("temp");
sessionStorage.clear();

מתי להשתמש במה?

  • localStorage - נתונים שצריכים להישמר לאורך זמן: הגדרות, נתוני משתמש, עגלת קניות
  • sessionStorage - נתונים זמניים של סשן: טופס שעוד לא נשלח, מצב זמני של דף, נתוני ניווט

מגבלות ואילוצים

גודל אחסון

  • localStorage ו-sessionStorage מוגבלים ל-5MB בערך (לכל domain)
  • זה יותר מספיק לנתונים קטנים, אבל לא מתאים לשמירת תמונות או קבצים גדולים

מדיניות מקור זהה - Same-Origin Policy

  • כל domain מקבל localStorage משלו
  • example.com לא יכול לגשת לנתונים של other.com
  • אפילו http://example.com ו-https://example.com הם domains שונים
// this data is only visible to pages on the same domain
localStorage.setItem("myData", "only this site can see this");

סינכרוני

  • כל הפעולות של localStorage הן סינכרוניות (חוסמות)
  • בשביל כמויות קטנות של נתונים זה לא בעיה
  • עבור נתונים גדולים, זה יכול להאט את הדף

עוגיות - Cookies (סקירה קצרה)

עוגיות הן מנגנון אחסון ישן יותר שעדיין בשימוש נרחב. בניגוד ל-localStorage, עוגיות נשלחות לשרת עם כל בקשת HTTP.

// set a cookie
document.cookie = "username=Dan; expires=Fri, 31 Dec 2027 23:59:59 GMT; path=/";

// read all cookies
console.log(document.cookie); // "username=Dan; theme=dark"

ההבדלים העיקריים מ-localStorage

  • עוגיות נשלחות לשרת אוטומטית; localStorage לא
  • עוגיות מוגבלות ל-4KB; localStorage ל-5MB
  • ה-API של עוגיות הוא לא נוח (מחרוזת אחת ארוכה)
  • עוגיות יכולות לפוג (expires); localStorage נשאר לנצח

בדרך כלל, עוגיות משמשות לדברים שהשרת צריך (כמו authentication tokens), ו-localStorage לדברים שרק הצד של הלקוח צריך.


IndexedDB (הכרות קצרה)

IndexedDB הוא מסד נתונים מלא שרץ בדפדפן. הוא מיועד לאחסון כמויות גדולות של נתונים מובנים.

  • יכול לשמור מגה-בייטים של נתונים
  • עובד עם אובייקטים ישירות (לא רק strings)
  • תומך באינדקסים, transactions, ושאילתות
  • ה-API שלו מורכב יותר מ-localStorage

לא נלמד IndexedDB עכשיו, אבל טוב לדעת שהוא קיים כשצריכים אחסון מקומי רציני.


אירוע storage - תקשורת בין טאבים

כשמשנים נתונים ב-localStorage, הדפדפן מפעיל אירוע storage בכל הטאבים האחרים (של אותו domain) שפתוחים. זה מאפשר לטאבים לתקשר ביניהם.

// listen for changes from OTHER tabs
window.addEventListener("storage", function (event) {
  console.log("key changed:", event.key);
  console.log("old value:", event.oldValue);
  console.log("new value:", event.newValue);
});

האירוע לא מופעל בטאב שביצע את השינוי - רק בטאבים אחרים.

דוגמה - סנכרון מצב כהה בין טאבים

// in tab 1 - user toggles dark mode
function toggleDarkMode() {
  let isDark = document.body.classList.toggle("dark");
  localStorage.setItem("darkMode", isDark);
}

// in tab 2 - automatically sync
window.addEventListener("storage", function (event) {
  if (event.key === "darkMode") {
    if (event.newValue === "true") {
      document.body.classList.add("dark");
    } else {
      document.body.classList.remove("dark");
    }
  }
});

// on page load - check saved preference
let isDark = localStorage.getItem("darkMode") === "true";
if (isDark) {
  document.body.classList.add("dark");
}

דפוסים מעשיים

דפוס 1 - שמירת העדפות משתמש

// save preferences
function savePreferences(prefs) {
  localStorage.setItem("preferences", JSON.stringify(prefs));
}

// load preferences with defaults
function loadPreferences() {
  let saved = localStorage.getItem("preferences");
  let defaults = {
    theme: "light",
    language: "he",
    fontSize: 16,
    notifications: true
  };

  if (saved) {
    let parsed = JSON.parse(saved);
    // merge saved with defaults (in case new preferences were added)
    return Object.assign({}, defaults, parsed);
  }
  return defaults;
}

// usage
let prefs = loadPreferences();
console.log(prefs.theme); // "light" or whatever was saved

prefs.theme = "dark";
savePreferences(prefs);

Object.assign({}, defaults, parsed) ממזג את ברירות המחדל עם מה שנשמר. אם נוסיפה העדפה חדשה בעתיד, היא תקבל את ערך ברירת המחדל.

דפוס 2 - שמירת טופס תוך כדי מילוי

let form = document.getElementById("my-form");

// save form data on every keystroke
form.addEventListener("input", function () {
  let data = Object.fromEntries(new FormData(form));
  localStorage.setItem("formDraft", JSON.stringify(data));
});

// restore form data on page load
function restoreForm() {
  let saved = localStorage.getItem("formDraft");
  if (!saved) return;

  let data = JSON.parse(saved);
  Object.keys(data).forEach(function (key) {
    let input = form.elements[key];
    if (input) {
      input.value = data[key];
    }
  });
}

// clear draft on successful submit
form.addEventListener("submit", function (event) {
  event.preventDefault();
  // ... send data to server ...
  localStorage.removeItem("formDraft");
});

// restore on load
restoreForm();

המשתמש יכול לסגור את הדף, לחזור אחרי שעה, ולמצוא את הטופס מלא עם מה שהוא כתב.

דפוס 3 - עגלת קניות

function getCart() {
  return JSON.parse(localStorage.getItem("cart")) || [];
}

function saveCart(cart) {
  localStorage.setItem("cart", JSON.stringify(cart));
}

function addToCart(product) {
  let cart = getCart();

  // check if product already in cart
  let existing = cart.find(function (item) {
    return item.id === product.id;
  });

  if (existing) {
    existing.quantity++;
  } else {
    cart.push({
      id: product.id,
      name: product.name,
      price: product.price,
      quantity: 1
    });
  }

  saveCart(cart);
}

function removeFromCart(productId) {
  let cart = getCart();
  cart = cart.filter(function (item) {
    return item.id !== productId;
  });
  saveCart(cart);
}

function getCartTotal() {
  let cart = getCart();
  return cart.reduce(function (total, item) {
    return total + (item.price * item.quantity);
  }, 0);
}

// usage
addToCart({ id: 1, name: "T-Shirt", price: 50 });
addToCart({ id: 2, name: "Jeans", price: 120 });
addToCart({ id: 1, name: "T-Shirt", price: 50 }); // quantity becomes 2

console.log(getCart());
console.log("total:", getCartTotal()); // 220

דפוס 4 - שמירת נתוני cache

function getCachedData(key, maxAgeMs) {
  let cached = localStorage.getItem(key);
  if (!cached) return null;

  let data = JSON.parse(cached);
  let age = Date.now() - data.timestamp;

  // check if cache is still valid
  if (age > maxAgeMs) {
    localStorage.removeItem(key);
    return null;
  }

  return data.value;
}

function setCachedData(key, value) {
  let cacheEntry = {
    value: value,
    timestamp: Date.now()
  };
  localStorage.setItem(key, JSON.stringify(cacheEntry));
}

// usage - cache for 5 minutes (300000ms)
let data = getCachedData("api-data", 300000);
if (!data) {
  // fetch from server (we'll learn this later)
  data = { users: ["Dan", "Sara"] }; // pretend we fetched this
  setCachedData("api-data", data);
}

הנתונים נשמרים עם timestamp, וכשקוראים אותם בודקים אם עבר יותר מדי זמן. אם כן, מוחקים ומביאים מחדש.


בניית עטיפה - Storage Wrapper

כדי לא לחזור על JSON.stringify ו-JSON.parse בכל פעם, אפשר לבנות עטיפה שמטפלת בזה אוטומטית:

let storage = {
  get: function (key, defaultValue) {
    let data = localStorage.getItem(key);
    if (data === null) return defaultValue;
    try {
      return JSON.parse(data);
    } catch (e) {
      return data; // return raw string if not valid JSON
    }
  },

  set: function (key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  },

  remove: function (key) {
    localStorage.removeItem(key);
  },

  clear: function () {
    localStorage.clear();
  },

  has: function (key) {
    return localStorage.getItem(key) !== null;
  }
};

// usage - much cleaner!
storage.set("user", { name: "Dan", age: 25 });
let user = storage.get("user");            // { name: "Dan", age: 25 }
let theme = storage.get("theme", "light"); // "light" (default value)
storage.remove("user");

try/catch מגן מפני מצב שבו ה-data ב-localStorage הוא לא JSON תקין (למשל אם מישהו ערך אותו ידנית ב-DevTools).


DevTools וניפוי שגיאות

אפשר לראות ולערוך את ה-localStorage ישירות ב-DevTools:
1. פתחו DevTools (F12)
2. לכו ללשונית Application (בChrome) או Storage (בFirefox)
3. בצד שמאל, פתחו Local Storage ובחרו את ה-domain
4. תראו טבלה של כל הkey-value pairs
5. אפשר לערוך, למחוק, ולהוסיף ערכים ישירות

זה מאוד שימושי לניפוי שגיאות ולבדיקות.


סיכום

  • localStorage - שומר נתונים שנשארים גם אחרי סגירת הדפדפן
  • sessionStorage - אותו API, אבל נמחק כשהטאב נסגר
  • חשוב - storage שומר רק strings. חובה JSON.stringify() בשמירה ו-JSON.parse() בקריאה
  • מוגבל ל-5MB לכל domain
  • same-origin policy - כל domain מקבל אחסון משלו
  • עוגיות - מנגנון ישן יותר שנשלח לשרת; מתאים ל-authentication
  • IndexedDB - מסד נתונים מלא בדפדפן, לנתונים גדולים
  • אירוע storage - מאפשר לטאבים לתקשר כשנתונים משתנים
  • דפוסים נפוצים: שמירת העדפות, טיוטת טופס, עגלת קניות, cache
  • בנו wrapper עם JSON handling כדי להימנע מחזרה על stringify/parse