Lección 13 de 1515 min
Navegación e integración con el sistema
History y Navigation API, portapapeles, notificaciones y push, archivos, geolocalización, Wake Lock, pagos y medición del rendimiento.
Abres la web de una tienda en el móvil: filtras por talla y la URL cambia sin recargar, pulsas «Copiar enlace» para mandárselo a alguien, aceptas que te avise cuando el pedido salga y pagas con la tarjeta que ya tenías guardada. Nada de eso es magia del framework: son APIs del navegador, funciones que el navegador pone a disposición de tu JavaScript.
En esta lección repasamos las que conectan tu web con la navegación y con el sistema: historial, portapapeles, archivos, ubicación, pantalla, pagos, notificaciones y medición del rendimiento. Muchas tienen algo en común: como tocan cosas del usuario, el navegador las protege con permisos y solo las ofrece en páginas servidas por HTTPS.
Navegar sin recargar la página
Una SPA (single-page application) carga una sola página y cambia el contenido con JavaScript. El problema es que, si no haces nada, la URL no cambia, el botón «Atrás» saca al usuario de tu web y no se puede compartir un enlace a una vista concreta. Estas dos APIs lo resuelven.
History API
Cambia la URL y añade entradas al historial sin recargar la página.
- Soporte:
- Todos los navegadores
- Evento clave:
- popstate
history.pushState(estado, "", url) cambia la URL de la barra de direcciones y añade una entrada al historial, pero el navegador no carga nada: eres tú quien actualiza el contenido. history.replaceState() hace lo mismo sin añadir entrada, útil para filtros que no deben llenar el historial.
Cuando el usuario pulsa «Atrás» o «Adelante», el navegador dispara el evento popstate con el estado que guardaste, y tu código vuelve a pintar la vista correspondiente. Es la base de los routers de React, Vue o Angular desde hace más de una década.
Úsalo cuando
Cuando cambias de vista con JavaScript y quieres que la URL refleje dónde está el usuario, que «Atrás» funcione y que el enlace se pueda compartir.
Ten en cuenta
pushState no dispara popstate: solo lo hacen atrás y adelante. Y los clics en enlaces normales los tienes que interceptar tú. Además, si el usuario recarga una URL que solo existe en tu SPA, el servidor tiene que saber devolver la página; lo verás en la lección de Nginx.
// Al elegir un filtro, cambia la URL sin recargar
function aplicarFiltro(talla) {
pintarProductos(talla);
history.pushState({ talla }, "", `?talla=${talla}`);
}
// Atrás y adelante disparan popstate (pushState no)
window.addEventListener("popstate", (evento) => {
pintarProductos(evento.state?.talla ?? "todas");
});Navigation API
La sucesora de History: un único evento para todas las navegaciones.
- Objeto:
- navigation
- Evento clave:
- navigate
Con History tienes que vigilar por separado los enlaces, los formularios y el botón «Atrás». La Navigation API lo unifica: el objeto global navigation dispara un evento navigate para cualquier navegación, y con evento.intercept() le dices al navegador «esta la gestiono yo».
Además ofrece cosas que History no tiene: navigation.entries() para ver las entradas del historial de tu web, navigation.currentEntry para saber dónde estás y promesas que te avisan de cuándo ha terminado una navegación.
Úsalo cuando
Para escribir un router propio o una SPA sencilla sin librería, con una lógica de navegación centralizada en un solo sitio.
Ten en cuenta
Llegó primero a los navegadores basados en Chromium y los demás la han incorporado en sus versiones recientes. Antes de depender de ella, comprueba su estado Baseline en MDN y ten un plan B con History.
navigation.addEventListener("navigate", (evento) => {
const url = new URL(evento.destination.url);
if (!evento.canIntercept || !url.pathname.startsWith("/app/")) return;
evento.intercept({
async handler() {
const html = await cargarVista(url.pathname);
document.querySelector("main").innerHTML = html;
},
});
});El sistema y los archivos
Estas APIs dejan que tu web haga cosas que antes eran exclusivas de las apps instaladas: copiar al portapapeles, abrir y guardar archivos, saber dónde está el usuario, mantener la pantalla encendida o cobrar con un toque.
Clipboard API
Copia y pega texto o imágenes desde JavaScript, de forma asíncrona.
- Requiere:
- HTTPS y un gesto
- Sustituye a:
- execCommand
navigator.clipboard.writeText(texto) copia texto al portapapeles y readText() lo lee. Para otros formatos, como imágenes, están write() y read() con objetos ClipboardItem. Todo devuelve promesas, así que no bloquea la página.
Sustituye al antiguo document.execCommand("copy"), que está obsoleto y obligaba a seleccionar un campo de texto oculto para poder copiar.
Úsalo cuando
Para botones de «Copiar enlace», «Copiar código» o «Copiar cupón», y para pegar imágenes en un editor.
Ten en cuenta
Solo funciona con HTTPS y, en general, en respuesta a una acción del usuario como un clic. Leer el portapapeles es más delicado que escribir: el navegador puede pedir permiso o mostrar un aviso, porque ahí podría haber una contraseña.
botonCopiar.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(location.href);
botonCopiar.textContent = "¡Copiado!";
} catch {
botonCopiar.textContent = "No se pudo copiar";
}
});File API
Lee los archivos que el usuario elige, sin subirlos al servidor.
- Soporte:
- Todos los navegadores
- Objetos:
- File, Blob, FileReader
Cuando el usuario elige archivos con <input type="file"> o los arrastra a la página, recibes objetos File: un tipo de Blob (un bloque de datos binarios) con nombre, tipo y tamaño. Puedes leer su contenido con await archivo.text() o arrayBuffer(), o crear una URL temporal con URL.createObjectURL() para mostrarlo.
Así puedes validar el tamaño antes de subir, enseñar una vista previa de una foto o procesar un CSV completamente en el navegador.
Úsalo cuando
Siempre que el usuario aporte un archivo: subir un avatar, importar un CSV, adjuntar un documento.
Ten en cuenta
Solo puedes leer lo que el usuario elige, y no conoces la ruta real del archivo en su disco. Las URL de createObjectURL ocupan memoria hasta que llamas a URL.revokeObjectURL(). Y valida siempre también en el servidor: el tipo que declara el archivo se puede falsear.
// <input type="file" id="foto" accept="image/*">
const campo = document.querySelector("#foto");
campo.addEventListener("change", () => {
const archivo = campo.files[0];
if (!archivo) return;
console.log(archivo.name, archivo.type, `${archivo.size} bytes`);
// Vista previa sin subir nada al servidor
const vista = document.querySelector("#vista-previa");
vista.src = URL.createObjectURL(archivo);
});File System Access API
Abre un archivo del disco, edítalo y guarda los cambios en el mismo archivo.
- Soporte:
- Chromium de escritorio
- Requiere:
- HTTPS y un gesto
Con la File API solo lees; para «guardar» tienes que descargar una copia. La File System Access API va más allá: showOpenFilePicker() te da un manejador (handle) del archivo, y con él puedes volver a escribir en el mismo archivo, como haría un editor de escritorio. También existe showDirectoryPicker() para trabajar con una carpeta entera.
Es lo que permite que editores como VS Code para la web abran una carpeta de tu equipo y guarden los cambios directamente en ella.
Úsalo cuando
En herramientas de productividad (editores de código, de imagen o de texto) que trabajan sobre archivos locales y deben guardarlos en su sitio.
Ten en cuenta
Los selectores de archivos solo están disponibles en navegadores basados en Chromium de escritorio, como Chrome y Edge; Firefox y Safari no los ofrecen. Detecta la función y recurre a la File API como plan B. Lo que sí funciona en todos es el Origin Private File System, un sistema de archivos privado de tu web que el usuario no ve.
let manejador;
botonAbrir.addEventListener("click", async () => {
[manejador] = await window.showOpenFilePicker({
types: [{ description: "Markdown", accept: { "text/markdown": [".md"] } }],
});
const archivo = await manejador.getFile();
editor.value = await archivo.text();
});
botonGuardar.addEventListener("click", async () => {
const escritura = await manejador.createWritable(); // pide permiso para escribir
await escritura.write(editor.value);
await escritura.close();
});Geolocation API
Pide al dispositivo su ubicación, siempre con permiso del usuario.
- Soporte:
- Todos los navegadores
- Requiere:
- HTTPS y permiso
navigator.geolocation.getCurrentPosition() pide la posición una vez y watchPosition() te avisa cada vez que cambia. Recibes la latitud, la longitud y la precisión estimada en metros. El navegador decide cómo obtenerla: GPS, redes wifi cercanas o la dirección IP.
La primera vez, el navegador muestra un aviso para que el usuario acepte o rechace. Tu código tiene que estar preparado para ambas respuestas.
Úsalo cuando
Para mostrar tiendas o servicios cercanos, rellenar la ciudad en un formulario o seguir un recorrido en una app de deporte.
Ten en cuenta
Pídela en el momento en que el usuario entiende para qué (al pulsar «Buscar cerca de mí»), no nada más cargar la página: si la rechaza, no podrás volver a preguntar. Ofrece siempre una alternativa, como escribir el código postal. Y la precisión puede ser de kilómetros si no hay GPS.
navigator.geolocation.getCurrentPosition(
(posicion) => {
const { latitude, longitude, accuracy } = posicion.coords;
buscarTiendasCerca(latitude, longitude);
console.log(`Precisión: ±${Math.round(accuracy)} m`);
},
(error) => mostrarBuscadorPorCodigoPostal(error.message),
{ enableHighAccuracy: false, timeout: 10_000, maximumAge: 60_000 },
);Screen Wake Lock API
Evita que la pantalla se apague mientras el usuario la necesita.
- Requiere:
- HTTPS
- Objeto:
- WakeLockSentinel
Los dispositivos apagan la pantalla tras un rato sin tocarla para ahorrar batería. Con navigator.wakeLock.request("screen") le pides al sistema que no lo haga mientras tu página esté visible. Recibes un objeto «centinela» (WakeLockSentinel) que puedes liberar con release() cuando ya no haga falta.
El navegador lo libera solo cuando el usuario cambia de pestaña o minimiza la ventana, así que lo habitual es volver a pedirlo al regresar.
Úsalo cuando
En recetas paso a paso, tarjetas de embarque, partituras, presentaciones o rutas de navegación: pantallas que el usuario mira sin tocar.
Ten en cuenta
La petición puede fallar, por ejemplo con la batería baja, así que envuélvela en try/catch. Ya está disponible en los navegadores principales en sus versiones recientes, pero detecta la función antes de usarla. Y libéralo en cuanto deje de hacer falta.
let bloqueo = null;
async function mantenerPantalla() {
try {
bloqueo = await navigator.wakeLock.request("screen");
} catch (error) {
console.warn("No se pudo mantener la pantalla encendida", error);
}
}
// Se libera al cambiar de pestaña: pídelo de nuevo al volver
document.addEventListener("visibilitychange", () => {
if (bloqueo?.released && document.visibilityState === "visible") mantenerPantalla();
});Payment Request API
Muestra la hoja de pago nativa del navegador en lugar de un formulario.
- Requiere:
- HTTPS y un gesto
- Soporte:
- Chromium y Safari
En vez de pedir al usuario que teclee los 16 dígitos de su tarjeta, creas un new PaymentRequest(metodos, detalles) con los métodos de pago que aceptas y el importe, y llamas a show(). El navegador muestra su propia hoja de pago, con los métodos que el usuario ya tiene configurados, como Apple Pay o Google Pay.
Cuando el usuario confirma, recibes los datos del pago, los envías a tu procesador de pagos y cierras la hoja con complete("success") o complete("fail").
Úsalo cuando
En tiendas online que quieren un pago más rápido en el móvil, sobre todo si ya trabajan con un proveedor que admite Apple Pay o Google Pay.
Ten en cuenta
No es un procesador de pagos: sigues necesitando un proveedor que cobre de verdad, y cada método de pago exige su propia configuración. Chromium y Safari la soportan, pero Firefox no, así que mantén siempre el formulario normal como alternativa.
const pago = new PaymentRequest(
[{ supportedMethods: "https://apple.com/apple-pay", data: configApplePay }],
{ total: { label: "Total", amount: { currency: "EUR", value: "24.90" } } },
);
if (await pago.canMakePayment()) {
const respuesta = await pago.show();
const ok = await enviarAlProcesadorDePagos(respuesta.details);
await respuesta.complete(ok ? "success" : "fail");
} else {
mostrarFormularioDeTarjeta();
}Contexto seguro y permisos
Casi todas estas APIs solo existen en un contexto seguro: una página servida por HTTPS o desde localhost. Si abres tu web con http:// y una IP, navigator.clipboard o navigator.geolocation pueden no funcionar, aunque el código sea correcto. Y las que acceden a datos del usuario, además, piden permiso.
Notificaciones
Avisar al usuario cuando algo pasa es útil, pero hay dos problemas distintos: mostrar el aviso y recibirlo aunque la web esté cerrada. Cada uno lo resuelve una API.
Notifications API
Muestra avisos del sistema operativo, fuera de la pestaña del navegador.
- Requiere:
- HTTPS y permiso
- Estados:
- granted, denied, default
Primero pides permiso con Notification.requestPermission(), que devuelve "granted", "denied" o "default" (sin decidir). Si te lo conceden, new Notification(titulo, opciones) muestra un aviso como el de cualquier app, con título, texto e icono.
Desde un service worker (un script que el navegador ejecuta en segundo plano, separado de la página) se usa registration.showNotification(), que además admite botones de acción.
Úsalo cuando
Para avisar de algo que ocurre mientras el usuario tiene la web abierta pero está en otra pestaña: un mensaje nuevo, una exportación que ha terminado.
Ten en cuenta
Pide el permiso tras un gesto del usuario y explicando para qué; algunos navegadores ignoran o silencian las peticiones al cargar la página. Si el usuario dice que no, no podrás volver a preguntar. En iPhone y iPad solo funcionan en webs añadidas a la pantalla de inicio.
botonAvisos.addEventListener("click", async () => {
const permiso = await Notification.requestPermission();
if (permiso !== "granted") return;
new Notification("Exportación lista", {
body: "Tu informe de ventas ya se puede descargar.",
icon: "/icono.png",
});
});Push API
Recibe mensajes de tu servidor aunque la web esté cerrada.
- Requiere:
- Service worker
- Claves:
- VAPID
La página se suscribe con pushManager.subscribe() y obtiene una URL única del servicio de push del navegador (el de Google, Mozilla o Apple). La envías a tu servidor, que guarda la suscripción. Cuando quiera avisar, tu servidor manda un mensaje a esa URL, firmado con unas claves llamadas VAPID.
El navegador despierta a tu service worker con un evento push, aunque no haya ninguna pestaña de tu web abierta, y este muestra la notificación con showNotification(). Push trae el mensaje; Notifications lo enseña.
Úsalo cuando
Para avisos que llegan desde el servidor en cualquier momento: «tu pedido ha salido», «tienes un mensaje nuevo», «ha bajado el precio».
Ten en cuenta
Necesita un service worker, permiso de notificaciones y algo de trabajo en el servidor; lo normal es usar una librería de web push. Navegadores como Chrome y Safari exigen que cada mensaje muestre un aviso visible (userVisibleOnly: true). Y no abuses: es la forma más rápida de que el usuario te bloquee.
// En la página: suscribirse (tras conceder el permiso)
const registro = await navigator.serviceWorker.ready;
const suscripcion = await registro.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: CLAVE_PUBLICA_VAPID,
});
await fetch("/api/suscripciones", { method: "POST", body: JSON.stringify(suscripcion) });
// En el service worker (sw.js): llega un mensaje del servidor
self.addEventListener("push", (evento) => {
const { titulo, texto } = evento.data.json();
evento.waitUntil(self.registration.showNotification(titulo, { body: texto }));
});Medir el rendimiento
Performance API
Mide con precisión cuánto tarda tu código y cómo carga la página.
- Soporte:
- Todos los navegadores
- Unidad:
- Milisegundos
performance.now() devuelve los milisegundos, con decimales, desde que empezó a cargarse la página. A diferencia de Date.now(), no salta si alguien cambia la hora del equipo, así que es la forma correcta de medir duraciones. Con performance.mark() y performance.measure() pones marcas con nombre y mides lo que hay entre ellas.
El navegador también registra por su cuenta lo que pasa al cargar: cuánto tardó cada archivo, cuándo se pintó el contenido principal… Lo lees con PerformanceObserver. De ahí salen las Core Web Vitals, las métricas de experiencia que verás en el módulo de rendimiento.
Úsalo cuando
Para medir una operación concreta (cuánto tarda en filtrarse una tabla) o para enviar a tu analítica cómo carga la web en los dispositivos de tus usuarios reales.
Ten en cuenta
Medir en tu equipo de desarrollo no dice mucho de un móvil de gama baja: mide en producción. No todos los navegadores exponen todas las métricas, y para las Core Web Vitals suele ser más cómodo usar la librería web-vitals.
performance.mark("inicio-filtro");
filtrarProductos(catalogo);
performance.mark("fin-filtro");
const medida = performance.measure("filtrar", "inicio-filtro", "fin-filtro");
console.log(`Filtrar tardó ${medida.duration.toFixed(1)} ms`);
// Métrica del navegador: cuándo se pintó el elemento más grande (LCP)
new PerformanceObserver((lista) => {
const ultima = lista.getEntries().at(-1);
enviarAnalitica("LCP", ultima.startTime);
}).observe({ type: "largest-contentful-paint", buffered: true });Pruébalo
Abre la consola del navegador en esta página y escribe performance.getEntriesByType("navigation")[0]. Verás cuánto tardó en llegar la respuesta del servidor (responseStart) y cuándo terminó de cargar todo (loadEventEnd).
Interactivo
History API frente a Navigation API
Activa o desactiva opciones para compararlas cara a cara.
| Característica | History API | Navigation API |
|---|---|---|
| Cambiar la URL sin recargar | pushState() y replaceState() | navigation.navigate() o interceptando con intercept() |
| Enterarte de las navegaciones | Solo atrás y adelante (popstate); los enlaces los capturas tú | Todas, con un único evento navigate |
| Ver el historial | Solo cuántas entradas hay (history.length) | Las entradas de tu web, con navigation.entries() |
| Saber cuándo termina | No hay forma directa | Promesas que se cumplen al terminar |
| Soporte | Todos los navegadores, desde hace años | Chromium primero; el resto, en versiones recientes |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
En la página de un artículo quieres un botón que copie la URL para que el lector la pegue en un chat.
Para recordar
- History y Navigation cambian la URL sin recargar; el contenido lo actualizas tú.
- Las APIs que tocan datos o dispositivos del usuario exigen HTTPS y, muchas, un permiso explícito que conviene pedir en el momento oportuno.
- Push recibe mensajes del servidor con la web cerrada; Notifications es la que muestra el aviso.
- No todas las APIs funcionan en todos los navegadores: detecta la función y ofrece siempre un plan B, sobre todo con File System Access y Payment Request.
- Mide duraciones con
performance.now(), no conDate.now(), y mide en los dispositivos reales de tus usuarios.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.Llamas a
history.pushState({}, "", "/perfil"). ¿Qué ocurre?2.¿Qué diferencia hay entre la Push API y la Notifications API?
3.Pruebas tu web en el móvil abriendo
http://192.168.1.20:3000ynavigator.geolocationno funciona, aunque en tu equipo conlocalhostsí. ¿Por qué?4.Tu web de recetas pide el wake lock, pero cuando el usuario vuelve de otra pestaña la pantalla vuelve a apagarse. ¿Qué falta?
5.¿Por qué es mejor
performance.now()queDate.now()para medir cuánto tarda una función?