wandres.dev
DEPURAR EN MÓVIL · Dispositivo real y emulación

Port forwarding y la inspección de un WebView

Servir tu entorno de desarrollo local al teléfono por el cable, el mapeo de dominios para certificados y cookies, y cómo se depura una vista web dentro de una aplicación nativa.

⏱ 16 min

Tener el dispositivo conectado sirve de poco si no puede alcanzar tu servidor de desarrollo, que vive en la dirección local de tu ordenador y es inaccesible desde cualquier otro aparato. El reenvío de puertos resuelve exactamente eso: hace que una dirección local del teléfono se resuelva contra un puerto de tu máquina, a través del mismo cable. Y una vez conectado, la misma infraestructura permite inspeccionar algo que de otro modo es una caja negra: la vista web incrustada dentro de una aplicación nativa.

🎯 Al terminar esta lección sabrás
  • Configurar el reenvío de puertos y comprobar que funciona.
  • Usar el mapeo de dominios para servir bajo el nombre real del sitio.
  • Explicar qué problemas resuelve el mapeo que el reenvío simple no resuelve.
  • Inspeccionar una vista web incrustada y conocer sus limitaciones.

El reenvío de puertos

En la página de inspección de dispositivos hay una sección de reenvío de puertos donde se declaran pares: un puerto en el dispositivo y una dirección de destino en tu máquina.

Con una regla que asocie un puerto del dispositivo con el puerto de tu servidor de desarrollo, abrir esa dirección local en el navegador del teléfono carga tu aplicación en desarrollo. El tráfico va por el cable, así que no hace falta estar en la misma red, funciona con redes corporativas que aíslan clientes, y funciona con el teléfono en datos móviles.

Ese último detalle tiene un uso muy práctico: permite probar tu desarrollo local con la red del teléfono ralentizada, o incluso comprobar el comportamiento cuando el teléfono pierde la conectividad, mientras tu servidor sigue siendo alcanzable por el cable.

⚠️
Cuidado

Si tu servidor de desarrollo escucha solo en la interfaz local, el reenvío funciona igualmente porque el destino se resuelve en tu máquina. Pero si escucha con recarga en caliente por conexión persistente, comprueba que esa conexión también pasa: muchas configuraciones intentan conectarse a una dirección absoluta que en el teléfono no resuelve. La solución es configurar el cliente de recarga para que use la misma dirección relativa que la página.

El mapeo de dominios

El reenvío simple sirve la aplicación bajo una dirección local, y eso rompe cosas.

Las cookies con dominio declarado no se envían. Cualquier cookie configurada para tu dominio real no aplica a una dirección local.

El contexto no es seguro para algunas APIs. Aunque las direcciones locales se consideran seguras por excepción, la configuración de tu aplicación puede depender del nombre.

Las llamadas a API absolutas van al servidor real. Si tu código apunta a una dirección absoluta de producción, esas peticiones salen a producción aunque la página venga de tu máquina.

Los orígenes cruzados cambian. Lo que en producción es del mismo origen, aquí no lo es, y aparecen errores de origen cruzado que no existen realmente.

El mapeo de dominios resuelve los cuatro: permite declarar que un nombre de dominio concreto, cuando se pida desde el dispositivo, se resuelva contra un puerto de tu máquina. Con eso, el teléfono carga tu desarrollo local bajo el nombre real del sitio, con las cookies correctas, con los mismos orígenes y con el mismo contexto.

Requiere que el navegador del dispositivo tenga configurado un proxy que apunte a la máquina de desarrollo, y con esa configuración se puede además servir por conexión segura si tu servidor local tiene un certificado, aunque sea autofirmado y haya que aceptarlo una vez en el dispositivo.

Comprobar que funciona

Una vez configurado, este fragmento ejecutado en la consola remota confirma que el contexto es el que crees.

// Comprueba el contexto real que ve la pagina en el dispositivo
(() => {
  console.table([{
    origen: location.origin,
    protocolo: location.protocol,
    contextoSeguro: window.isSecureContext,
    aislamientoOrigen: window.crossOriginIsolated,
    cookiesLegibles: document.cookie ? document.cookie.split('; ').length : 0,
    serviceWorker: 'serviceWorker' in navigator,
    almacenamiento: (() => { try { localStorage.setItem('_p', '1'); localStorage.removeItem('_p'); return 'disponible'; } catch { return 'bloqueado'; } })()
  }]);

  // Comprueba a donde van realmente las llamadas de la aplicacion
  const originalFetch = window.fetch;
  const destinos = new Map();
  window.fetch = function (recurso, ...resto) {
    const url = typeof recurso === 'string' ? recurso : recurso.url;
    try {
      const o = new URL(url, location.href).origin;
      destinos.set(o, (destinos.get(o) || 0) + 1);
      if (o !== location.origin) console.warn('Peticion a otro origen:', o, url.slice(0, 70));
    } catch {}
    return originalFetch.call(this, recurso, ...resto);
  };

  window.destinosDeRed = () => {
    console.table([...destinos].map(([origen, veces]) => ({
      origen, veces, esPropio: origen === location.origin
    })));
    return destinos;
  };
  window.restaurarFetch = () => { window.fetch = originalFetch; };

  console.log('Usa la aplicacion y ejecuta destinosDeRed() para ver a donde va todo.');
})();

El interceptor de peticiones es la comprobación decisiva: revela si tu desarrollo local está hablando con tu servidor local o con producción, que es un error muy fácil de cometer y que puede tener consecuencias si el código escribe.

Inspeccionar una vista web

Una aplicación nativa que incrusta contenido web contiene un navegador completo, y ese navegador se puede inspeccionar con las mismas herramientas, con dos condiciones.

La aplicación tiene que habilitarlo explícitamente. Hay una llamada en el código nativo que activa la depuración de las vistas web de esa aplicación. Sin ella, la vista no aparece en la lista de destinos.

Solo funciona en compilaciones que lo permitan. Una aplicación de tienda normalmente lo tiene desactivado. Se depura la compilación de desarrollo.

Con eso, la vista web aparece en la lista de destinos junto a las pestañas del navegador, y al inspeccionarla se abren las DevTools completas.

Las tres limitaciones que hay que conocer al depurar en ese contexto.

El motor puede ser distinto o más antiguo que el del navegador instalado. Una vista web es un componente del sistema y su versión depende de la actualización del sistema o del propio componente, no del navegador que el usuario tenga.

El puente con el código nativo es invisible. Las llamadas entre el contenido web y la aplicación nativa pasan por un canal propio y no aparecen en el panel de red. Para depurarlas hay que instrumentar el puente en el lado web, envolviéndolo y registrando lo que pasa.

El contexto de navegación puede estar restringido. Algunas capacidades están limitadas o configuradas de forma distinta dentro de una vista web, y comprobar cuáles con el fragmento anterior es el primer paso ante cualquier comportamiento raro.

// Instrumentar el puente nativo, que no aparece en el panel de red
(() => {
  // Los puentes suelen exponerse como un objeto global inyectado por la app
  const posiblesPuentes = Object.keys(window).filter(k =>
    /bridge|native|android|webkit|flutter|reactnative|cordova/i.test(k));
  console.log('Posibles puentes nativos detectados:', posiblesPuentes);

  const envolver = (nombre) => {
    const puente = window[nombre];
    if (!puente || typeof puente !== 'object') return;
    for (const metodo of Object.keys(puente)) {
      if (typeof puente[metodo] !== 'function') continue;
      const original = puente[metodo].bind(puente);
      puente[metodo] = (...args) => {
        console.log('[puente]', nombre + '.' + metodo, args);
        const r = original(...args);
        console.log('[puente] ->', r);
        return r;
      };
    }
    console.log('Instrumentado el puente:', nombre);
  };

  posiblesPuentes.forEach(envolver);
  console.log('Interactua con la aplicacion y observa las llamadas al puente.');
})();

Esa instrumentación convierte una caja negra en un registro legible, y es prácticamente la única forma de depurar la frontera entre el contenido web y la aplicación que lo aloja.

Una vista web dentro de una aplicación es un navegador que no elegiste, con reglas que no controlas

Merece la pena cerrar el nivel con una advertencia sobre este entorno, porque una fracción enorme del tráfico web actual no ocurre en un navegador sino dentro de una aplicación: enlaces abiertos desde redes sociales, desde clientes de correo, desde mensajería. Esas vistas incrustadas se comportan como navegadores y no lo son del todo, y las diferencias importan. No comparten el almacenamiento con el navegador del sistema, así que el usuario que ya tenía sesión iniciada aparece sin sesión, y las cookies que pusiste ayer no están. Muchas no soportan service workers ni instalación, así que toda tu estrategia sin red no existe ahí. Algunas inyectan su propio código en la página, lo que puede interferir con el tuyo y añadir trabajo que no controlas. Su versión del motor puede ir por detrás de la del navegador, con lo que una función que compruebas como disponible puede no estarlo. Y su gestión de la memoria suele ser más agresiva, porque la aplicación anfitriona tiene sus propias prioridades y la vista web es prescindible. La consecuencia práctica es que hay una parte de tu público, probablemente grande y probablemente invisible en tu analítica porque se agrupa mal, cuya experiencia es sistemáticamente peor y que tú nunca has probado. Las dos medidas que lo corrigen son sencillas. La primera es medirlo: segmenta tus métricas de campo por si el contexto es una vista web incrustada, cosa que se puede detectar razonablemente por la cadena de identificación, y mira si esa cohorte tiene métricas peores o tasas de abandono mayores. La segunda es probarlo, que cuesta un minuto: envíate el enlace de tu sitio por la aplicación de mensajería que usen tus usuarios, ábrelo desde ahí, y comprueba si funciona. Es una de esas pruebas que casi nadie hace y que descubre, con una frecuencia sorprendente, que algo importante está roto para una parte considerable del tráfico.