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