Se estandarizó el enrutamiento del cliente a través de una API completamente nueva que renueva por completo la compilación de aplicaciones de una sola página.
Las aplicaciones de una página, o SPA, se definen por una función principal: reescribir su contenido de forma dinámica a medida que el usuario interactúa con el sitio, en lugar del método predeterminado de cargar páginas completamente nuevas desde el servidor.
Si bien las SPA pudieron ofrecerte esta función a través de la API de History (o, en casos limitados, ajustando la parte #hash del sitio), se trata de una API torpe desarrollada mucho antes de que las SPA fueran la norma, y la Web clama por un enfoque completamente nuevo. La API de Navigation es una API propuesta que renueva por completo este espacio, en lugar de intentar simplemente corregir los bordes irregulares de la API de History. (Por ejemplo, Scroll Restoration corrigió la API de History en lugar de intentar reinventarla).
En esta publicación, se describe la API de Navigation de forma general. Para leer la propuesta técnica, consulta el Informe preliminar en el repositorio de WICG.
Ejemplo de uso
Para usar la API de Navigation, comienza por agregar un objeto de escucha "navigate" en el objeto navigation global.
Este evento es fundamentalmente centralizado: Se activará para todos los tipos de navegación, ya sea que el usuario haya realizado una acción (como hacer clic en un vínculo, enviar un formulario o ir hacia atrás y hacia adelante) o cuando la navegación se active de forma programática (es decir, a través del código de tu sitio).
En la mayoría de los casos, permite que tu código anule el comportamiento predeterminado del navegador para esa acción.
En el caso de las SPA, es probable que esto signifique mantener al usuario en la misma página y cargar o cambiar el contenido del sitio.
Se pasa un NavigateEvent al objeto de escucha "navigate", que contiene información sobre la navegación, como la URL de destino, y te permite responder a la navegación en un solo lugar centralizado.
Un objeto de escucha "navigate" básico podría verse de la siguiente manera:
navigation.addEventListener('navigate', navigateEvent => {
// Exit early if this navigation shouldn't be intercepted.
// The properties to look at are discussed later in the article.
if (shouldNotIntercept(navigateEvent)) return;
const url = new URL(navigateEvent.destination.url);
if (url.pathname === '/') {
navigateEvent.intercept({handler: loadIndexPage});
} else if (url.pathname === '/cats/') {
navigateEvent.intercept({handler: loadCatsPage});
}
});
Puedes controlar la navegación de dos maneras:
- Llama a
intercept({ handler })(como se describió anteriormente) para controlar la navegación. - Llamar a
preventDefault(), que puede cancelar la navegación por completo
En este ejemplo, se llama a intercept() en el evento.
El navegador llama a tu devolución de llamada handler, que debería configurar el siguiente estado de tu sitio.
Esto creará un objeto de transición, navigation.transition, que otro código puede usar para hacer un seguimiento del progreso de la navegación.
Por lo general, se permiten intercept() y preventDefault(), pero hay casos en los que no se pueden llamar.
No puedes controlar las navegaciones a través de intercept() si la navegación es de origen cruzado.
Además, no puedes cancelar una navegación a través de preventDefault() si el usuario presiona los botones Atrás o Adelante en su navegador. No deberías poder atrapar a los usuarios en tu sitio.
(Este tema se está debatiendo en GitHub).
Incluso si no puedes detener o interceptar la navegación en sí, el evento "navigate" se activará de todos modos.
Es informativo, por lo que tu código podría, por ejemplo, registrar un evento de Analytics para indicar que un usuario está abandonando tu sitio.
¿Por qué agregar otro evento a la plataforma?
Un objeto de escucha de eventos "navigate" centraliza el control de los cambios de URL dentro de una SPA.
Esto es difícil de lograr con las APIs anteriores.
Si alguna vez escribiste el enrutamiento de tu propia SPA con la API de History, es posible que hayas agregado código como este:
function updatePage(event) {
event.preventDefault(); // we're handling this link
window.history.pushState(null, '', event.target.href);
// TODO: set up page based on new URL
}
const links = [...document.querySelectorAll('a[href]')];
links.forEach(link => link.addEventListener('click', updatePage));
Esto está bien, pero no es exhaustivo. Los vínculos pueden aparecer y desaparecer en tu página, y no son la única forma en que los usuarios pueden navegar por las páginas. Por ejemplo, pueden enviar un formulario o incluso usar un mapa de imágenes. Es posible que tu página aborde estos temas, pero hay una gran cantidad de posibilidades que podrían simplificarse, algo que logra la nueva API de Navigation.
Además, lo anterior no controla la navegación hacia atrás o hacia adelante. Hay otro evento para eso, "popstate".
Personalmente, la API de History a menudo se siente como si pudiera ayudar de alguna manera con estas posibilidades.
Sin embargo, en realidad solo tiene dos áreas de superficie: responder si el usuario presiona Atrás o Adelante en su navegador, además de enviar y reemplazar URLs. No tiene una analogía con "navigate", excepto si configuras manualmente objetos de escucha para eventos de clic, por ejemplo, como se demostró anteriormente.
Cómo decidir cómo controlar una navegación
El objeto navigateEvent contiene mucha información sobre la navegación que puedes usar para decidir cómo abordar una navegación en particular.
Las propiedades clave son las siguientes:
canIntercept- Si es falso, no puedes interceptar la navegación. No se pueden interceptar las navegaciones entre orígenes ni los recorridos entre documentos.
destination.url- Probablemente, la información más importante que se debe tener en cuenta cuando se maneja la navegación.
hashChange- Es verdadero si la navegación es dentro del mismo documento y el hash es la única parte de la URL que es diferente de la URL actual.
En las SPA modernas, el hash debe servir para vincular diferentes partes del documento actual. Por lo tanto, si
hashChangees verdadero, probablemente no necesites interceptar esta navegación. downloadRequest- Si es verdadero, la navegación se inició a través de un vínculo con un atributo
download. En la mayoría de los casos, no es necesario interceptar esto. formData- Si no es nulo, esta navegación forma parte de un envío de formulario POST.
Asegúrate de tener esto en cuenta cuando manejes la navegación.
Si solo quieres controlar las navegaciones GET, evita interceptar las navegaciones en las que
formDatano sea nulo. Consulta el ejemplo sobre el manejo de envíos de formularios más adelante en el artículo. navigationType- Este es uno de
"reload","push","replace"o"traverse". Si es"traverse", esta navegación no se puede cancelar conpreventDefault().
Por ejemplo, la función shouldNotIntercept que se usó en el primer ejemplo podría ser similar a la siguiente:
function shouldNotIntercept(navigationEvent) {
return (
!navigationEvent.canIntercept ||
// If this is just a hashChange,
// just let the browser handle scrolling to the content.
navigationEvent.hashChange ||
// If this is a download,
// let the browser perform the download.
navigationEvent.downloadRequest ||
// If this is a form submission,
// let that go to the server.
navigationEvent.formData
);
}
Interceptación
Cuando tu código llama a intercept({ handler }) desde su objeto de escucha "navigate", informa al navegador que ahora está preparando la página para el estado nuevo y actualizado, y que la navegación puede tardar un tiempo.
El navegador comienza por capturar la posición de desplazamiento del estado actual para que se pueda restablecer más adelante de forma opcional y, luego, llama a tu devolución de llamada handler.
Si tu handler devuelve una promesa (lo que sucede automáticamente con las funciones async), esa promesa le indica al navegador cuánto tiempo lleva la navegación y si se realiza correctamente.
navigation.addEventListener('navigate', navigateEvent => {
if (shouldNotIntercept(navigateEvent)) return;
const url = new URL(navigateEvent.destination.url);
if (url.pathname.startsWith('/articles/')) {
navigateEvent.intercept({
async handler() {
const articleContent = await getArticleContent(url.pathname);
renderArticlePage(articleContent);
},
});
}
});
Por lo tanto, esta API introduce un concepto semántico que el navegador comprende: actualmente, se está produciendo una navegación de SPA que, con el tiempo, cambia el documento de una URL y un estado anteriores a uno nuevo. Esto tiene varios beneficios potenciales, incluida la accesibilidad: los navegadores pueden mostrar el inicio, el final o una posible falla de una navegación. Por ejemplo, Chrome activa su indicador de carga nativo y permite que el usuario interactúe con el botón de detener. (Actualmente, esto no sucede cuando el usuario navega con los botones Atrás o Adelante, pero se corregirá pronto).
Confirmación de la navegación
Cuando interceptes navegaciones, la nueva URL entrará en vigencia justo antes de que se llame a tu devolución de llamada handler.
Si no actualizas el DOM de inmediato, se crea un período en el que se muestra el contenido anterior junto con la URL nueva.
Esto afecta aspectos como la resolución de URLs relativas cuando se recuperan datos o se cargan nuevos recursos secundarios.
En GitHub se está debatiendo una forma de retrasar el cambio de URL, pero, en general, se recomienda actualizar la página de inmediato con algún tipo de marcador de posición para el contenido entrante:
navigation.addEventListener('navigate', navigateEvent => {
if (shouldNotIntercept(navigateEvent)) return;
const url = new URL(navigateEvent.destination.url);
if (url.pathname.startsWith('/articles/')) {
navigateEvent.intercept({
async handler() {
// The URL has already changed, so quickly show a placeholder.
renderArticlePagePlaceholder();
// Then fetch the real data.
const articleContent = await getArticleContent(url.pathname);
renderArticlePage(articleContent);
},
});
}
});
Esto no solo evita problemas de resolución de URL, sino que también se siente rápido porque respondes al usuario de inmediato.
Indicadores de anulación
Dado que puedes realizar trabajo asíncrono en un controlador de intercept(), es posible que la navegación se vuelva redundante.
Esto sucede en los siguientes casos:
- El usuario hace clic en otro vínculo o algún código realiza otra navegación. En este caso, se abandona la navegación anterior en favor de la nueva.
- El usuario hace clic en el botón "Detener" del navegador.
Para abordar cualquiera de estas posibilidades, el evento que se pasa al objeto de escucha "navigate" contiene una propiedad signal, que es un AbortSignal.
Para obtener más información, consulta Abortable fetch.
La versión corta es que básicamente proporciona un objeto que activa un evento cuando debes detener tu trabajo.
En particular, puedes pasar un AbortSignal a cualquier llamada que realices a fetch(), que cancelará las solicitudes de red en curso si se interrumpe la navegación.
Esto ahorrará ancho de banda del usuario y rechazará el Promise que devuelve fetch(), lo que evitará que el código siguiente realice acciones como actualizar el DOM para mostrar una navegación de página ahora no válida.
A continuación, se muestra el ejemplo anterior, pero con getArticleContent intercalado, en el que se muestra cómo se puede usar AbortSignal con fetch():
navigation.addEventListener('navigate', navigateEvent => {
if (shouldNotIntercept(navigateEvent)) return;
const url = new URL(navigateEvent.destination.url);
if (url.pathname.startsWith('/articles/')) {
navigateEvent.intercept({
async handler() {
// The URL has already changed, so quickly show a placeholder.
renderArticlePagePlaceholder();
// Then fetch the real data.
const articleContentURL = new URL(
'/get-article-content',
location.href
);
articleContentURL.searchParams.set('path', url.pathname);
const response = await fetch(articleContentURL, {
signal: navigateEvent.signal,
});
const articleContent = await response.json();
renderArticlePage(articleContent);
},
});
}
});
Control de desplazamiento
Cuando intercept() una navegación, el navegador intentará controlar el desplazamiento automáticamente.
En el caso de las navegaciones a una nueva entrada del historial (cuando navigationEvent.navigationType es "push" o "replace"), esto significa intentar desplazarse a la parte indicada por el fragmento de URL (la parte después del #) o restablecer el desplazamiento a la parte superior de la página.
En el caso de las recargas y los recorridos, esto significa restablecer la posición de desplazamiento a la que se encontraba la última vez que se mostró esta entrada del historial.
De forma predeterminada, esto sucede una vez que se resuelve la promesa que devuelve tu handler, pero, si tiene sentido desplazarse antes, puedes llamar a navigateEvent.scroll():
navigation.addEventListener('navigate', navigateEvent => {
if (shouldNotIntercept(navigateEvent)) return;
const url = new URL(navigateEvent.destination.url);
if (url.pathname.startsWith('/articles/')) {
navigateEvent.intercept({
async handler() {
const articleContent = await getArticleContent(url.pathname);
renderArticlePage(articleContent);
navigateEvent.scroll();
const secondaryContent = await getSecondaryContent(url.pathname);
addSecondaryContent(secondaryContent);
},
});
}
});
Como alternativa, puedes inhabilitar por completo el control de desplazamiento automático configurando la opción scroll de intercept() en "manual":
navigateEvent.intercept({
scroll: 'manual',
async handler() {
// …
},
});
Control del enfoque
Una vez que se resuelva la promesa que devolvió tu handler, el navegador enfocará el primer elemento con el atributo autofocus establecido o el elemento <body> si ningún elemento tiene ese atributo.
Para inhabilitar este comportamiento, configura la opción focusReset de intercept() en "manual":
navigateEvent.intercept({
focusReset: 'manual',
async handler() {
// …
},
});
Eventos de éxito y error
Cuando se llame a tu controlador intercept(), sucederá una de las siguientes situaciones:
- Si el
Promisedevuelto cumple con la condición (o no llamaste aintercept()), la API de Navigation activará"navigatesuccess"con unEvent. - Si el
Promisedevuelto rechaza, la API activará"navigateerror"con unErrorEvent.
Estos eventos permiten que tu código gestione el éxito o el fracaso de forma centralizada. Por ejemplo, podrías controlar el éxito ocultando un indicador de progreso que se mostró anteriormente, de la siguiente manera:
navigation.addEventListener('navigatesuccess', event => {
loadingIndicator.hidden = true;
});
O bien, puedes mostrar un mensaje de error si falla:
navigation.addEventListener('navigateerror', event => {
loadingIndicator.hidden = true; // also hide indicator
showMessage(`Failed to load page: ${event.message}`);
});
El objeto de escucha de eventos "navigateerror", que recibe un ErrorEvent, es particularmente útil, ya que garantiza que recibirá cualquier error del código que configura una página nueva.
Simplemente puedes await fetch() sabiendo que, si la red no está disponible, el error se enrutará a "navigateerror".
Entradas de navegación
navigation.currentEntry proporciona acceso a la entrada actual.
Es un objeto que describe dónde se encuentra el usuario en este momento.
Esta entrada incluye la URL actual, los metadatos que se pueden usar para identificar esta entrada a lo largo del tiempo y el estado proporcionado por el desarrollador.
Los metadatos incluyen key, una propiedad de cadena única de cada entrada que representa la entrada actual y su ranura.
Esta clave sigue siendo la misma, incluso si cambia la URL o el estado de la entrada actual.
Sigue estando en el mismo horario.
Por el contrario, si un usuario presiona Atrás y, luego, vuelve a abrir la misma página, key cambiará, ya que esta nueva entrada crea un nuevo espacio.
Para un desarrollador, key es útil porque la API de Navigation te permite dirigir al usuario directamente a una entrada con una clave coincidente.
Puedes mantenerla, incluso en los estados de otras entradas, para saltar fácilmente entre las páginas.
// On JS startup, get the key of the first loaded page
// so the user can always go back there.
const {key} = navigation.currentEntry;
backToHomeButton.onclick = () => navigation.traverseTo(key);
// Navigate away, but the button will always work.
await navigation.navigate('/another_url').finished;
Estado
La Navigation API expone una noción de "estado", que es información proporcionada por el desarrollador que se almacena de forma persistente en la entrada del historial actual, pero que no es directamente visible para el usuario.
Es muy similar a history.state en la API de History, pero mejorada.
En la API de Navigation, puedes llamar al método .getState() de la entrada actual (o de cualquier entrada) para devolver una copia de su estado:
console.log(navigation.currentEntry.getState());
De forma predeterminada, será undefined.
Estado de configuración
Si bien los objetos de estado se pueden mutar, esos cambios no se guardan con la entrada del historial, por lo que sucede lo siguiente:
const state = navigation.currentEntry.getState();
console.log(state.count); // 1
state.count++;
console.log(state.count); // 2
// But:
console.info(navigation.currentEntry.getState().count); // will still be 1
La forma correcta de establecer el estado es durante la navegación de la secuencia de comandos:
navigation.navigate(url, {state: newState});
// Or:
navigation.reload({state: newState});
Aquí, newState puede ser cualquier objeto clonable.
Si quieres actualizar el estado de la entrada actual, lo mejor es realizar una navegación que reemplace la entrada actual:
navigation.navigate(location.href, {state: newState, history: 'replace'});
Luego, tu objeto de escucha de eventos "navigate" puede detectar este cambio a través de navigateEvent.destination:
navigation.addEventListener('navigate', navigateEvent => {
console.log(navigateEvent.destination.getState());
});
Actualización del estado de forma síncrona
En general, es mejor actualizar el estado de forma asíncrona a través de navigation.reload({state: newState}). Luego, tu objeto de escucha "navigate" puede aplicar ese estado. Sin embargo, a veces, el cambio de estado ya se aplicó por completo cuando tu código se entera de él, por ejemplo, cuando el usuario activa o desactiva un elemento <details>, o cuando cambia el estado de una entrada de formulario. En estos casos, es posible que desees actualizar el estado para que estos cambios se conserven durante las recargas y los recorridos. Esto se puede hacer con updateCurrentEntry():
navigation.updateCurrentEntry({state: newState});
También hay un evento para conocer este cambio:
navigation.addEventListener('currententrychange', () => {
console.log(navigation.currentEntry.getState());
});
Sin embargo, si reaccionas a los cambios de estado en "currententrychange", es posible que estés dividiendo o incluso duplicando tu código de control de estado entre el evento "navigate" y el evento "currententrychange", mientras que navigation.reload({state: newState}) te permitiría controlarlo en un solo lugar.
Parámetros de URL vs. estado
Dado que el estado puede ser un objeto estructurado, es tentador usarlo para todo el estado de la aplicación. Sin embargo, en muchos casos, es mejor almacenar ese estado en la URL.
Si esperas que se conserve el estado cuando el usuario comparta la URL con otro usuario, almacénalo en la URL. De lo contrario, el objeto de estado es la mejor opción.
Accede a todas las entradas
Sin embargo, la "entrada actual" no es todo.
La API también proporciona una forma de acceder a la lista completa de entradas por las que navegó un usuario mientras usaba tu sitio a través de su llamada navigation.entries(), que devuelve un array de instantáneas de entradas.
Esto se podría usar, p.ej., para mostrar una IU diferente según cómo el usuario navegó a una página determinada o simplemente para volver a consultar las URLs anteriores o sus estados.
Esto es imposible con la API de History actual.
También puedes escuchar un evento "dispose" en objetos NavigationHistoryEntry individuales, que se activa cuando la entrada ya no forma parte del historial del navegador. Esto puede ocurrir como parte de una limpieza general, pero también cuando se navega. Por ejemplo, si vuelves atrás 10 lugares y, luego, navegas hacia adelante, se descartarán esas 10 entradas del historial.
Ejemplos
El evento "navigate" se activa para todos los tipos de navegación, como se mencionó anteriormente.
(De hecho, hay un apéndice extenso en la especificación de todos los tipos posibles).
Si bien, para muchos sitios, el caso más común será cuando el usuario haga clic en un <a href="...">, existen dos tipos de navegación notables y más complejos que vale la pena analizar.
Navegación programática
La primera es la navegación programática, en la que la navegación se produce por una llamada a un método dentro de tu código del cliente.
Puedes llamar a navigation.navigate('/another_page') desde cualquier parte de tu código para provocar una navegación.
Esto se controlará con el objeto de escucha de eventos centralizado registrado en el objeto de escucha "navigate", y se llamará a tu objeto de escucha centralizado de forma síncrona.
Esto se diseñó como una agregación mejorada de métodos más antiguos, como location.assign() y similares, además de los métodos pushState() y replaceState() de la API de History.
El método navigation.navigate() devuelve un objeto que contiene dos instancias de Promise en { committed, finished }.
Esto permite que el invocador espere hasta que la transición se "confirme" (cambió la URL visible y hay un nuevo NavigationHistoryEntry disponible) o "finalice" (se completaron todas las promesas que devolvió intercept({ handler }) o se rechazaron debido a una falla o a que otra navegación las adelantó).
El método navigate también tiene un objeto de opciones en el que puedes establecer lo siguiente:
state: Es el estado de la nueva entrada del historial, disponible a través del método.getState()enNavigationHistoryEntry.history: Se puede establecer en"replace"para reemplazar la entrada del historial actual.info: Es un objeto que se pasa al evento de navegación a través denavigateEvent.info.
En particular, info podría ser útil para, por ejemplo, denotar una animación específica que hace que aparezca la página siguiente.
(La alternativa podría ser establecer una variable global o incluirla como parte del #hash. Ambas opciones son un poco incómodas).
En particular, este info no se volverá a reproducir si un usuario provoca una navegación más adelante, p.ej., a través de los botones Atrás y Adelante.
De hecho, siempre será undefined en esos casos.
navigation también tiene varios otros métodos de navegación, todos los cuales devuelven un objeto que contiene { committed, finished }.
Ya mencioné traverseTo() (que acepta un key que denota una entrada específica en el historial del usuario) y navigate().
También incluye back(), forward() y reload().
Todos estos métodos son controlados, al igual que navigate(), por el objeto de escucha de eventos centralizado "navigate".
Envíos de formularios
En segundo lugar, el envío de <form> HTML a través de POST es un tipo especial de navegación, y la API de Navigation puede interceptarlo.
Si bien incluye una carga útil adicional, la navegación sigue siendo controlada de forma centralizada por el objeto de escucha "navigate".
El envío del formulario se puede detectar buscando la propiedad formData en el objeto NavigateEvent.
A continuación, se muestra un ejemplo que simplemente convierte cualquier envío de formulario en uno que permanece en la página actual a través de fetch():
navigation.addEventListener('navigate', navigateEvent => {
if (navigateEvent.formData && navigateEvent.canIntercept) {
// User submitted a POST form to a same-domain URL
// (If canIntercept is false, the event is just informative:
// you can't intercept this request, although you could
// likely still call .preventDefault() to stop it completely).
navigateEvent.intercept({
// Since we don't update the DOM in this navigation,
// don't allow focus or scrolling to reset:
focusReset: 'manual',
scroll: 'manual',
handler() {
await fetch(navigateEvent.destination.url, {
method: 'POST',
body: navigateEvent.formData,
});
// You could navigate again with {history: 'replace'} to change the URL here,
// which might indicate "done"
},
});
}
});
¿Qué información falta?
A pesar de la naturaleza centralizada del objeto de escucha de eventos "navigate", la especificación actual de la API de Navigation no activa "navigate" en la primera carga de una página.
Y para los sitios que usan la renderización del servidor (SSR) para todos los estados, esto podría estar bien: tu servidor podría devolver el estado inicial correcto, que es la forma más rápida de llevar contenido a tus usuarios.
Sin embargo, es posible que los sitios que aprovechan el código del cliente para crear sus páginas deban crear una función adicional para inicializar su página.
Otra decisión de diseño intencional de la API de Navigation es que solo funciona dentro de un solo marco, es decir, la página de nivel superior o un solo <iframe> específico.
Esto tiene varias implicaciones interesantes que se documentan más en la especificación, pero, en la práctica, reducirá la confusión de los desarrolladores.
La API de History anterior tiene varios casos extremos confusos, como la compatibilidad con los marcos, y la API de Navigation rediseñada controla estos casos extremos desde el principio.
Por último, aún no hay consenso sobre cómo modificar o reorganizar de forma programática la lista de entradas por las que navegó el usuario. Actualmente, se está debatiendo este tema, pero una opción podría ser permitir solo las eliminaciones: ya sea de entradas históricas o de "todas las entradas futuras". Este último permitiría un estado temporal. Por ejemplo, como desarrollador, podría hacer lo siguiente:
- Hacerle una pregunta al usuario navegando a una nueva URL o estado
- Permitir que el usuario complete su trabajo (o volver)
- quitar una entrada del historial cuando se complete una tarea
Esto podría ser perfecto para los elementos emergentes o los intersticiales temporales: la nueva URL es algo de lo que un usuario puede salir con el gesto de atrás, pero luego no puede ir hacia adelante accidentalmente para volver a abrirla (porque se quitó la entrada). Esto no es posible con la API de History actual.
Prueba la API de Navigation
La API de Navigation está disponible en Chrome 102 sin marcas. También puedes probar una demostración de Domenic Denicola.
Si bien la API de History clásica parece sencilla, no está muy bien definida y tiene una gran cantidad de problemas relacionados con casos extremos y la forma en que se implementó de manera diferente en los distintos navegadores. Esperamos que consideres enviar comentarios sobre la nueva API de Navigation.
Referencias
- WICG/navigation-api
- Posición de Mozilla sobre los estándares
- Intención de crear un prototipo
- Revisión del TAG
- Entrada de ChromeStatus
Agradecimientos
Gracias a Thomas Steiner, Domenic Denicola y Nate Chapin por revisar esta publicación.