JavaScript
Tutorial JavaScript: LocalStorage y SessionStorage
Descubre los límites y seguridad en el almacenamiento web para aplicaciones modernas. Aprende a usar APIs eficientemente con medidas de protección.
Aprende JavaScript y certifícateLímites y consideraciones de seguridad
Al trabajar con las APIs de almacenamiento web (localStorage y sessionStorage), es fundamental comprender sus limitaciones técnicas y las implicaciones de seguridad que conllevan. Estos mecanismos, aunque útiles, no son soluciones universales para todos los escenarios de persistencia de datos.
Capacidad de almacenamiento
La capacidad de almacenamiento disponible en localStorage y sessionStorage está limitada por los navegadores. Aunque esta limitación varía según el navegador, generalmente se establece alrededor de 5-10 MB por dominio. Esta restricción es importante cuando planificamos qué datos almacenar:
// Función para comprobar el espacio utilizado en localStorage
function checkStorageUsage() {
let totalSize = 0;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
totalSize += localStorage[key].length * 2; // Caracteres * 2 bytes (UTF-16)
}
}
return (totalSize / 1024).toFixed(2) + " KB";
}
console.log(`Espacio utilizado: ${checkStorageUsage()}`);
Cuando intentamos exceder este límite, el navegador lanzará una excepción. Es buena práctica implementar manejo de errores para estas situaciones:
try {
// Intentamos almacenar un objeto grande
const largeObject = { /* datos extensos */ };
localStorage.setItem('largeData', JSON.stringify(largeObject));
} catch (e) {
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
console.error('Error: Límite de almacenamiento excedido');
// Implementar estrategia de gestión (eliminar datos antiguos, etc.)
}
}
Políticas de mismo origen (Same-Origin Policy)
Una de las restricciones de seguridad más importantes es la política de mismo origen. Tanto localStorage como sessionStorage están vinculados al origen del documento, que se define por la combinación de:
- Protocolo (http, https)
- Dominio (ejemplo.com)
- Puerto (80, 443, etc.)
// Este almacenamiento solo es accesible desde https://miapp.com:443
// No puede ser accedido desde http://miapp.com o https://otrositio.com
localStorage.setItem('userData', JSON.stringify({name: 'Ana'}));
Esta restricción tiene importantes implicaciones:
- Los datos almacenados en
https://app.ejemplo.com
no son accesibles desdehttps://ejemplo.com
- Subdominios diferentes se consideran orígenes distintos
- El cambio de protocolo (HTTP a HTTPS) crea un origen diferente
Vulnerabilidades de seguridad
El almacenamiento web no está cifrado por defecto, lo que presenta varios riesgos de seguridad:
- Ataques XSS: Un script malicioso inyectado en la página puede acceder a todo el almacenamiento del dominio:
// Código potencialmente vulnerable
function displayUserComment(comment) {
// Peligroso: insertar contenido sin sanitizar
document.getElementById('comments').innerHTML += comment;
// Mejor enfoque: sanitizar el contenido
const sanitizedComment = DOMPurify.sanitize(comment);
document.getElementById('comments').innerHTML += sanitizedComment;
}
- Persistencia de datos sensibles: Nunca debemos almacenar información confidencial sin cifrado:
// Enfoque incorrecto
localStorage.setItem('authToken', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
// Mejor enfoque (aunque no perfecto)
function encryptData(data, key) {
// Implementación de cifrado (usando SubtleCrypto API)
return encryptedData;
}
const encryptedToken = encryptData(token, secretKey);
localStorage.setItem('secureToken', encryptedToken);
Modo incógnito y limpieza de navegador
Es importante considerar que los datos en localStorage persisten incluso después de cerrar el navegador, pero pueden ser eliminados en estas situaciones:
- Cuando el usuario borra los datos de navegación
- En modo incógnito/privado, donde los datos se eliminan al cerrar la ventana
- Si el usuario tiene configurado su navegador para rechazar cookies o almacenamiento local
// Detectar si localStorage está disponible
function isStorageAvailable() {
try {
const testKey = '__storage_test__';
localStorage.setItem(testKey, testKey);
localStorage.removeItem(testKey);
return true;
} catch (e) {
return false;
}
}
// Implementar alternativa si no está disponible
if (!isStorageAvailable()) {
console.warn('localStorage no disponible, usando alternativa en memoria');
// Implementar solución alternativa
}
Consideraciones de rendimiento
Aunque el acceso al almacenamiento web es síncrono, operaciones frecuentes o con grandes volúmenes de datos pueden afectar el rendimiento:
- Las operaciones de lectura/escritura bloquean el hilo principal
- La serialización/deserialización de objetos complejos puede ser costosa
// Enfoque ineficiente: múltiples operaciones individuales
for (let i = 0; i < 1000; i++) {
localStorage.setItem(`item_${i}`, `value_${i}`);
}
// Enfoque más eficiente: operación única con estructura de datos
const batchData = {};
for (let i = 0; i < 1000; i++) {
batchData[`item_${i}`] = `value_${i}`;
}
localStorage.setItem('batchItems', JSON.stringify(batchData));
Comprender estas limitaciones y consideraciones de seguridad es esencial para implementar soluciones de almacenamiento web robustas y seguras en aplicaciones JavaScript modernas.
Patrones de diseño: Implementación de wrapper classes y sistemas de caché
El uso directo de localStorage y sessionStorage puede volverse complejo cuando las aplicaciones crecen. Para mejorar la mantenibilidad, testabilidad y agregar funcionalidades avanzadas, es recomendable implementar patrones de diseño que encapsulen estas APIs nativas. Veremos cómo crear abstracciones elegantes que simplifiquen el trabajo con el almacenamiento web.
Wrapper classes para almacenamiento
Una clase wrapper proporciona una interfaz más amigable sobre las APIs nativas, añadiendo funcionalidades como manejo automático de JSON, expiración de datos y validación:
class StorageManager {
constructor(storageType = 'local') {
this.storage = storageType === 'local' ? localStorage : sessionStorage;
}
set(key, value, expiresIn = null) {
const item = {
value,
timestamp: Date.now()
};
if (expiresIn) {
item.expiresAt = Date.now() + expiresIn;
}
this.storage.setItem(key, JSON.stringify(item));
return true;
}
get(key) {
const item = this.storage.getItem(key);
if (!item) return null;
const parsedItem = JSON.parse(item);
// Verificar expiración
if (parsedItem.expiresAt && parsedItem.expiresAt < Date.now()) {
this.remove(key);
return null;
}
return parsedItem.value;
}
remove(key) {
this.storage.removeItem(key);
}
clear() {
this.storage.clear();
}
}
Esta implementación ofrece varias ventajas:
- Manejo automático de serialización/deserialización JSON
- Expiración de datos configurable
- Interfaz unificada para localStorage y sessionStorage
- Encapsulación que facilita cambios futuros
Podemos utilizar esta clase de manera sencilla:
const userStorage = new StorageManager('local');
// Guardar datos con expiración de 1 hora
userStorage.set('userProfile', { name: 'Carlos', role: 'admin' }, 3600000);
// Recuperar datos
const profile = userStorage.get('userProfile');
console.log(profile); // { name: 'Carlos', role: 'admin' }
Sistemas de caché con almacenamiento web
Implementar un sistema de caché es otro patrón común que mejora el rendimiento de aplicaciones que realizan operaciones costosas o peticiones a APIs:
class CacheService {
constructor(defaultExpiration = 3600000) { // 1 hora por defecto
this.storage = new StorageManager('local');
this.defaultExpiration = defaultExpiration;
}
async fetch(url, options = {}) {
const cacheKey = `cache_${this.hashUrl(url)}`;
const cachedResponse = this.storage.get(cacheKey);
if (cachedResponse) {
return cachedResponse;
}
try {
const response = await fetch(url, options);
const data = await response.json();
// Guardar en caché
this.storage.set(
cacheKey,
data,
options.expiration || this.defaultExpiration
);
return data;
} catch (error) {
console.error('Error fetching data:', error);
throw error;
}
}
invalidate(url) {
const cacheKey = `cache_${this.hashUrl(url)}`;
this.storage.remove(cacheKey);
}
hashUrl(url) {
// Implementación simple de hash para URLs
let hash = 0;
for (let i = 0; i < url.length; i++) {
hash = ((hash << 5) - hash) + url.charCodeAt(i);
hash |= 0; // Convertir a entero de 32 bits
}
return hash.toString(16);
}
}
Este sistema de caché ofrece beneficios significativos:
- Reducción de peticiones a servidores externos
- Mejora de rendimiento para operaciones repetitivas
- Funcionamiento offline para datos previamente cargados
- Control granular sobre la invalidación de caché
Ejemplo de uso del sistema de caché:
const apiCache = new CacheService();
// Componente que muestra productos
async function loadProducts() {
try {
// Los datos se cargarán desde la caché si están disponibles
const products = await apiCache.fetch('https://api.example.com/products');
renderProductList(products);
} catch (error) {
showErrorMessage('No se pudieron cargar los productos');
}
}
// Invalidar caché cuando se actualiza un producto
function updateProduct(productId, data) {
// Actualizar en el servidor
fetch(`https://api.example.com/products/${productId}`, {
method: 'PUT',
body: JSON.stringify(data)
}).then(() => {
// Invalidar la caché para forzar una recarga fresca
apiCache.invalidate('https://api.example.com/products');
});
}
Patrón Repository con almacenamiento local
El patrón Repository proporciona una abstracción sobre el mecanismo de almacenamiento, permitiendo operaciones CRUD consistentes independientemente del origen de datos:
class UserRepository {
constructor() {
this.storage = new StorageManager('local');
this.collectionKey = 'users';
}
getAll() {
return this.storage.get(this.collectionKey) || [];
}
getById(id) {
const users = this.getAll();
return users.find(user => user.id === id) || null;
}
save(user) {
const users = this.getAll();
const existingIndex = users.findIndex(u => u.id === user.id);
if (existingIndex >= 0) {
// Actualizar usuario existente
users[existingIndex] = { ...users[existingIndex], ...user };
} else {
// Crear nuevo usuario con ID generado
users.push({
...user,
id: this.generateId(),
createdAt: Date.now()
});
}
this.storage.set(this.collectionKey, users);
return user;
}
delete(id) {
const users = this.getAll();
const filteredUsers = users.filter(user => user.id !== id);
if (filteredUsers.length !== users.length) {
this.storage.set(this.collectionKey, filteredUsers);
return true;
}
return false;
}
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
}
Este patrón facilita la gestión de colecciones de datos y proporciona una interfaz uniforme para operaciones CRUD:
const userRepo = new UserRepository();
// Crear usuario
const newUser = userRepo.save({ name: 'Elena', email: 'elena@example.com' });
// Obtener todos los usuarios
const allUsers = userRepo.getAll();
// Actualizar usuario
userRepo.save({ id: newUser.id, role: 'editor' });
// Eliminar usuario
userRepo.delete(newUser.id);
La implementación de estos patrones de diseño mejora significativamente la arquitectura de aplicaciones que utilizan almacenamiento web, facilitando el mantenimiento, las pruebas y la escalabilidad del código. Además, proporcionan una capa de abstracción que permite cambiar la implementación subyacente sin afectar al código cliente.
Sincronización entre pestañas: Detección de cambios con el evento storage
Cuando trabajamos con aplicaciones web modernas, es común que los usuarios tengan múltiples pestañas abiertas de la misma aplicación. En estos escenarios, mantener la sincronización de datos entre estas pestañas se vuelve crucial para garantizar una experiencia coherente. JavaScript proporciona un mecanismo elegante para detectar cambios en el almacenamiento web a través del evento storage.
El evento storage
se dispara automáticamente en todas las pestañas o ventanas (excepto en la que realizó el cambio) cuando se modifica el localStorage. Esta característica permite implementar sistemas de comunicación entre diferentes instancias de nuestra aplicación sin necesidad de servidores intermedios.
// Escuchar cambios en localStorage desde otras pestañas
window.addEventListener('storage', (event) => {
console.log('Cambio detectado en otra pestaña:');
console.log('Clave modificada:', event.key);
console.log('Valor anterior:', event.oldValue);
console.log('Nuevo valor:', event.oldValue);
console.log('URL de la pestaña que realizó el cambio:', event.url);
});
El objeto event
proporciona información detallada sobre el cambio realizado:
key
: La clave que se modificóoldValue
: El valor anterior (antes del cambio)newValue
: El nuevo valor establecidourl
: La URL de la página donde se realizó el cambiostorageArea
: Referencia al objeto de almacenamiento (localStorage o sessionStorage)
Implementación de un sistema de notificaciones entre pestañas
Podemos aprovechar este mecanismo para crear un sistema de notificaciones que mantenga sincronizadas todas las instancias de nuestra aplicación:
class TabSynchronizer {
constructor(channelName = 'app-sync') {
this.channel = channelName;
this.listeners = new Map();
// Escuchar eventos de otras pestañas
window.addEventListener('storage', (event) => {
if (event.key === this.channel) {
try {
const data = JSON.parse(event.newValue);
if (data && data.type && this.listeners.has(data.type)) {
this.listeners.get(data.type).forEach(callback => {
callback(data.payload);
});
}
} catch (error) {
console.error('Error al procesar mensaje entre pestañas:', error);
}
}
});
}
// Enviar mensaje a otras pestañas
broadcast(type, payload) {
const message = JSON.stringify({
type,
payload,
timestamp: Date.now()
});
localStorage.setItem(this.channel, message);
// Necesario para que otras pestañas detecten cambios consecutivos
// con el mismo tipo de mensaje
setTimeout(() => localStorage.removeItem(this.channel), 100);
}
// Suscribirse a un tipo de mensaje
subscribe(type, callback) {
if (!this.listeners.has(type)) {
this.listeners.set(type, new Set());
}
this.listeners.get(type).add(callback);
// Devolver función para cancelar suscripción
return () => {
const callbacks = this.listeners.get(type);
callbacks.delete(callback);
if (callbacks.size === 0) {
this.listeners.delete(type);
}
};
}
}
Este sistema permite implementar comunicación bidireccional entre pestañas de forma sencilla:
// Crear instancia del sincronizador
const tabSync = new TabSynchronizer();
// Suscribirse a actualizaciones de usuario
const unsubscribe = tabSync.subscribe('user-updated', (userData) => {
console.log('Datos de usuario actualizados en otra pestaña:', userData);
updateUserInterface(userData);
});
// Cuando el usuario actualiza su perfil
function updateUserProfile(newData) {
// Actualizar en el servidor
api.updateProfile(newData)
.then(response => {
// Guardar localmente
localStorage.setItem('user', JSON.stringify(response.data));
// Notificar a otras pestañas
tabSync.broadcast('user-updated', response.data);
showSuccessMessage('Perfil actualizado correctamente');
});
}
Sincronización de estado de autenticación
Un caso de uso común es mantener sincronizado el estado de autenticación entre pestañas. Cuando un usuario cierra sesión en una pestaña, probablemente desee que todas las demás pestañas también reflejen este cambio:
class AuthSynchronizer {
constructor() {
this.tabSync = new TabSynchronizer('auth-channel');
// Escuchar eventos de autenticación
this.tabSync.subscribe('auth-changed', (authState) => {
if (authState.status === 'logged-out') {
// Limpiar datos locales
localStorage.removeItem('authToken');
localStorage.removeItem('userData');
// Redirigir a página de login
window.location.href = '/login?reason=session_ended';
}
});
}
// Llamar cuando el usuario cierra sesión manualmente
logout() {
// Limpiar datos de autenticación
localStorage.removeItem('authToken');
localStorage.removeItem('userData');
// Notificar a otras pestañas
this.tabSync.broadcast('auth-changed', {
status: 'logged-out',
timestamp: Date.now()
});
// Redirigir a login
window.location.href = '/login';
}
}
// Uso
const authSync = new AuthSynchronizer();
document.getElementById('logout-button').addEventListener('click', () => {
authSync.logout();
});
Sincronización de carrito de compras
Otro escenario común es la sincronización de un carrito de compras entre diferentes pestañas de una tienda online:
class CartSynchronizer {
constructor() {
this.tabSync = new TabSynchronizer('cart-channel');
// Inicializar carrito desde localStorage
this.cart = JSON.parse(localStorage.getItem('cart')) || { items: [], total: 0 };
// Escuchar cambios en el carrito desde otras pestañas
this.tabSync.subscribe('cart-updated', (newCart) => {
this.cart = newCart;
this.updateCartUI();
});
}
// Añadir producto al carrito
addToCart(product, quantity = 1) {
// Buscar si el producto ya existe en el carrito
const existingItem = this.cart.items.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.cart.items.push({
id: product.id,
name: product.name,
price: product.price,
quantity
});
}
// Recalcular total
this.cart.total = this.cart.items.reduce(
(sum, item) => sum + (item.price * item.quantity), 0
);
// Guardar en localStorage
localStorage.setItem('cart', JSON.stringify(this.cart));
// Notificar a otras pestañas
this.tabSync.broadcast('cart-updated', this.cart);
// Actualizar interfaz
this.updateCartUI();
}
updateCartUI() {
// Actualizar contador de items
document.querySelector('.cart-count').textContent =
this.cart.items.reduce((sum, item) => sum + item.quantity, 0);
// Actualizar total
document.querySelector('.cart-total').textContent =
`$${this.cart.total.toFixed(2)}`;
// Actualizar lista de productos si está visible
const cartList = document.querySelector('.cart-items');
if (cartList) {
// Implementación de actualización del DOM
}
}
}
Consideraciones y limitaciones
Al implementar sistemas de sincronización entre pestañas, es importante tener en cuenta algunas limitaciones:
- El evento
storage
no se dispara en la misma pestaña que realizó el cambio - Existe un límite de tamaño para los datos que podemos almacenar
- La comunicación es asíncrona pero no garantiza entrega inmediata
- No funciona entre dominios diferentes debido a la política de mismo origen
Para casos más complejos donde estas limitaciones sean problemáticas, podemos considerar alternativas como WebSockets o la API BroadcastChannel:
// Alternativa usando BroadcastChannel (no soportada en todos los navegadores)
class ModernTabSynchronizer {
constructor(channelName) {
this.channel = new BroadcastChannel(channelName);
this.listeners = new Map();
this.channel.addEventListener('message', (event) => {
const { type, payload } = event.data;
if (this.listeners.has(type)) {
this.listeners.get(type).forEach(callback => callback(payload));
}
});
}
broadcast(type, payload) {
this.channel.postMessage({ type, payload, timestamp: Date.now() });
}
subscribe(type, callback) {
if (!this.listeners.has(type)) {
this.listeners.set(type, new Set());
}
this.listeners.get(type).add(callback);
return () => {
const callbacks = this.listeners.get(type);
callbacks.delete(callback);
if (callbacks.size === 0) {
this.listeners.delete(type);
}
};
}
}
La implementación de sistemas de sincronización entre pestañas mejora significativamente la experiencia de usuario en aplicaciones web modernas, garantizando coherencia de datos y reduciendo la confusión que puede surgir cuando diferentes instancias de la misma aplicación muestran estados diferentes.
Ejercicios de esta lección LocalStorage y SessionStorage
Evalúa tus conocimientos de esta lección LocalStorage y SessionStorage con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Array
Modificación de elementos DOM
Encapsulación
Manipulación DOM
Clases y objetos
Uso de operadores
Uso de operadores
Estructuras de control
Funciones
Excepciones
Transformación con map()
Arrays y Métodos
Transformación con map()
Funciones flecha
Async / Await
Polimorfismo
Variables
Selección de elementos DOM
API Fetch
Encapsulación
Mapas con Map
Creación y uso de variables
Polimorfismo
Tipos de datos
Promises
Estructuras de control
Pruebas unitarias
Encapsulación
Inmutabilidad y programación funcional pura
Destructuring de objetos y arrays
Mapas con Map
Funciones flecha
Polimorfismo
Herencia
Array
Transformación con map()
Gestor de tareas con JavaScript
Manipulación DOM
Funciones
Operadores avanzados
Conjuntos con Set
Funciones flecha
Async / Await
Clases y objetos
Métodos de Strings
Creación y uso de variables
Excepciones
Promises
Funciones cierre (closure)
Funciones cierre (closure)
Herencia
Prototipos y cadena de prototipos
Herencia
Estructuras de control
Selección de elementos DOM
Modificación de elementos DOM
Funciones flecha
Filtrado con filter() y find()
Funciones cierre (closure)
Callbacks
Funciones
Mapas con Map
Reducción con reduce()
Callbacks
Manipulación DOM
Introducción al DOM
Expresiones regulares
Promises
Async / Await
Eventos del DOM
Introducción a JavaScript
Async / Await
Excepciones
Promises
Selección de elementos DOM
Filtrado con filter() y find()
Callbacks
Eventos del DOM
Creación de clases y objetos Restaurante
Reducción con reduce()
Filtrado con filter() y find()
Reducción con reduce()
Conjuntos con Set
Herencia de clases
Eventos del DOM
Clases y objetos
Modificación de elementos DOM
Mapas con Map
Proyecto carrito compra agoodshop
Introducción a JavaScript
Filtrado con filter() y find()
Estructuras de control
Funciones
Reducción con reduce()
Proyecto administrador de contactos
Tipos de datos
Clases y objetos
Array
Conjuntos con Set
Todas las lecciones de JavaScript
Accede a todas las lecciones de JavaScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Javascript
Introducción Y Entorno
Introducción A Javascript
Introducción Y Entorno
Introducción A Javascript
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Funciones Cierre (Closure)
Sintaxis
Métodos De Strings
Sintaxis
Funciones Cierre (Closure)
Sintaxis
Operadores Avanzados
Sintaxis
Funciones
Sintaxis
Expresiones Regulares
Sintaxis
Estructuras De Control
Sintaxis
Variables
Sintaxis
Arrays Y Métodos
Estructuras De Datos
Conjuntos Con Set
Estructuras De Datos
Mapas Con Map
Estructuras De Datos
Conjuntos Con Set
Estructuras De Datos
Funciones Flecha
Programación Funcional
Filtrado Con Filter() Y Find()
Programación Funcional
Transformación Con Map()
Programación Funcional
Reducción Con Reduce()
Programación Funcional
Funciones Flecha
Programación Funcional
Reducción Con Reduce()
Programación Funcional
Filtrado Con Filter() Y Find()
Programación Funcional
Transformación Con Map()
Programación Funcional
Inmutabilidad Y Programación Funcional Pura
Programación Funcional
Clases Y Objetos
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
This Y Contexto
Programación Orientada A Objetos
Patrón De Módulos Y Namespace
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Prototipos Y Cadena De Prototipos
Programación Orientada A Objetos
Destructuring De Objetos Y Arrays
Programación Orientada A Objetos
Manipulación Dom
Dom
Selección De Elementos Dom
Dom
Modificación De Elementos Dom
Dom
Eventos Del Dom
Dom
Modificación De Elementos Dom
Dom
Eventos Del Dom
Dom
Localstorage Y Sessionstorage
Dom
Bom (Browser Object Model)
Dom
Modificación De Elementos Dom
Dom
Selección De Elementos Dom
Dom
Callbacks
Programación Asíncrona
Promises
Programación Asíncrona
Async / Await
Programación Asíncrona
Promises
Programación Asíncrona
Async / Await
Programación Asíncrona
Naturaleza De Js Y Event Loop
Programación Asíncrona
Callbacks
Programación Asíncrona
Websockets
Programación Asíncrona
Módulos En Es6
Construcción
Configuración De Bundlers Como Vite
Construcción
Eslint Y Calidad De Código
Construcción
Npm Y Dependencias
Construcción
Introducción A Pruebas En Js
Testing
Pruebas Unitarias
Testing
En esta lección
Objetivos de aprendizaje de esta lección
- Entender las limitaciones técnicas de localStorage y sessionStorage.
- Implementar medidas de seguridad para proteger datos almacenados.
- Desarrollar patrones de diseño para encapsular almacenamiento web.
- Crear sistemas de sincronización entre pestañas utilizando el evento 'storage'.
- Mejorar la experiencia de usuario con sistemas de caché y sincronización de estados.