Categoría: JavaScript

  • Capturas de pantalla con Cloudflare Browser Rendering

    Capturas de pantalla con Cloudflare Browser Rendering

    Con el nuevo servicio Cloudflare Browser Rendering se abren más posibilidades de usar Puppeteer en los servidores de Cloudflare y sin mucha configuración de por medio.

    Cloudflare Browser Rendering usa Puppeteer. Esta es una librería en JavaScript que ejecuta una instancia de Google Chrome permitiendo realizar varias interacciones, por ejemplo, de un sitio web podrás generar PDFs, tomar capturas de pantalla, obtener datos e incluso modificar el DOM.

    Uno de los proyectos de la empresa donde trabajo es tomar captura de pantallas de los resultados de Google (SERP), específicamente donde los resultados de la web de la empresa aparezcan. Así que, ese proyecto cayó en buen momento para probar Cloudflare Browser Rendering.

    En este post te enseñaré a cómo sacar una captura de pantalla a la primera página de resultados de Google usando Cloudflare Browser Rendering.

    Cloudflare Browser Rendering

    Este es un producto de Cloudflare que permite ejecutar Puppeteer en los servidores de Cloudflare.

    Funciona bajo la plataforma de Cloudflare Workers y está desarrollado para que los desarrolladores solo se enfoquen en desarrollar sus aplicaciones, así que no necesitas configurar nada relacionado con Puppeteer.

    Cloudflare Browser Rendering está disponible para usuarios que pagen el servicio de Workers y el precio que pagarás será de acuerdo al consumo de memoria y CPU de tu aplicación.

    Proyecto: Captura de pantalla a la primera página de los resultados de Google

    Aunque, por este medio, no puedo compartir el código desarrollado en la empresa donde trabajo, si puedo mostrarte cómo implementar Cloudflare Browser Rendering usando nuestro propio proyecto.

    Vamos a desarrollar una aplicación de Cloudflare Workers que tome captura de pantalla a la primera página de los resultados de Google usando Cloudflare Browser Rendering.

    La lógica del proyecto es la siguiente:

    1. La aplicación se ejecuta haciendo un HTTP GET.
    2. Agregamos el parámetro keyword al enlace que nos dará nuestro Worker.
    3. Aplicar validaciones necesarias.
    4. Si hay algún error, enviar una respuesta JSON.
    5. Iniciar Puppeteer, entrar a la página de Google con la palabra clave indicada.
    6. Tomar la captura de pantalla.
    7. Mostrar la captura de pantalla al navegador del usuario.

    Requisitos

    Los siguientes requisitos nos ayudarán a desarrollar nuestra aplicación:

    Desarrollo del proyecto

    1. Crea un Cloudflare Worker

    En si, todo el proyecto es un Cloudflare Worker. Un Worker es un sistema serverless, es decir, ejecutas tu código en un servidor en la nube con cero configuración en la infraestructura.

    Ejecuta el siguiente comando y contesta las preguntas que aparecerán en la terminal. El proyecto puedes llamarlo browser-rendering:

    npm create cloudflare@latest

    Si estás en Windows, ejecuta el comando usando la Windows PowerShell en modo administrador para responder las preguntas en la terminal.

    Si te pregunta qué tipo de aplicación deseas crear, escoge la opción “Hello World” Worker. Además, te preguntará si usarás JavaScript o TypeScript. En nuestro caso, usaremos JavaScript.

    2. Instala Puppeteer

    Dentro de tu proyecto browser-rendering, instala el Puppeteer de Cloudflare. Así es, Cloudflare ha hecho un fork de la librería Puppeteer y le aplicaron algunos cambios para hacerlo funcionar con Workers.

    npm install @cloudflare/puppeteer --save-dev

    3. Configura tu archivo Wrangler

    Un proyecto de Cloudflare Worker siempre tiene su archivo wrangler.toml, este archivo sirve para configurar tu proyecto con los productos de Cloudflare.

    El archivo en tu proyecto debe lucir así:

    name = "browser-rendering"
    main = "src/worker.js"
    compatibility_date = "2023-03-14"
    compatibility_flags = [ "nodejs_compat" ]
    account_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    
    browser = { binding = "MYBROWSER" }

    El archivo wrangler.toml está compuesto por el nombre de variables y sus valores. En este archivo todo es importante porque declaramos valores para que nuestro proyecto sea compatible con Node.js, desde donde se ejecuta nuestro proyecto y más.

    Ahora, la variable browser contiene el binding con su valor MYBROWSER. Este binding establecerá la conexión entre nuestro Worker con el Chrome ejecutado desde Cloudflare.

    El valor del binding puede ser cualquiera de tu preferencia. En este proyecto usaremos MYBROWSER.

    Algo que no mencionan al configurar el wrangler.toml, es la variable account_id. Aunque esta variable es opcional, por alguna razón cuando ejecutaba el Worker me pedía que ingresara el account_id. Esta nos permite interactuar directamente con la zona de Cloudflare donde ejecutaremos nuestro Worker.

    Puedes ver más configuraciones del archivo wrangler.toml en este enlace.

    4. El código

    Una vez configurado el archivo wrangler.toml ya podemos empezar a desarrollar nuestra aplicación que tomará capturas de pantalla a un sitio web. El código JavaScript es el siguiente:

    import puppeteer from "@cloudflare/puppeteer";
    
    /**
     * Send error response.
     *
     * @since 1.0.0
     *
     * @param {string} message The error message.
     * @param {int} status The status code.
     * @returns {Response} The response.
     */
    function sendError(message, status = 400) {
        const json = {
            message,
            success: false,
        };
    
        return new Response(JSON.stringify(json), {
            status,
            headers: { 'content-type': 'application/json;charset=UTF-8' },
        });
    }
    
    /**
     * Get the Google URL.
     *
     * @since 1.0.0
     *
     * @param {string} keyword The keyword.
     * @returns {string} The Google URL.
     */
    function getGoogleUrl(keyword) {
        const params = {
            q: keyword,
            gl: 'us',
            hl: 'en',
        };
    
        const urlSearchParams = new URLSearchParams(params);
    
        return `https://www.google.com/search?${ urlSearchParams }`;
    }
    
    export default {
        /**
         * Fetch the SERP screenshot.
         *
         * @since 1.0.0
         *
         * @param {object} request The request.
         * @param {object} env The environment variables.
         * @returns {Promise<Response>} The response.
         */
        async fetch(request, env) {
            if (request.method !== 'GET') {
                return sendError('Method not allowed.', 405);
            }
    
            const { searchParams } = new URL(request.url);
            let keyword = searchParams.get('keyword');
    
            if (!keyword) {
                return sendError('You need to specify a keyword.');
            }
    
            try {
                const browser = await puppeteer.launch(env.MYBROWSER);
                const page = await browser.newPage();
    
                await page.goto(getGoogleUrl(keyword), { waitUntil: 'load' });
    
                const screenshot = await page.screenshot();
    
                await browser.close();
    
                return new Response(screenshot, {
                    headers: {
                        'content-type': 'image/png',
                    }
                });
            } catch (e) {
                return sendError(e.message);
            }
        }
    }

    La función sendError la ejecutamos cuando hay algún tipo de error en nuestras validaciones o cuando Puppeteer arrojó algún otro tipo de error. Por otra parte, obtendremos el enlace de Google para buscar nuestra palabra clave con la función getGoogleUrl.

    Y dentro de la función fetch se ejecutará la funcionalidad de Puppeteer. Dentro de esta función ejecutamos validaciones al principio:

    1. Solo debemos aceptar peticiones HTTP GET.
    2. La variable keyword debe existir.

    Ahora, la parte que ejecuta la funcionalidad de Puppeteer, preferentemente, debe estar encerrado dentro de un try/catch, para que podamos detectar cualquier error lanzado por Puppeteer.

    Y acá viene lo más importante, la constante browser contiene la instancia de Chrome, la cual usaremos para abrir nuevas ventanas y cerrar la conexión. Pero, ¿ya notaste que la función puppeteer.launch() tiene un parámetro?

    const browser = await puppeteer.launch(env.MYBROWSER);

    Dentro de la función puppeteer.launch() ponemos el binding que hemos configurado en nuestro archivo wrangler.toml.

    browser = { binding = "MYBROWSER" }

    Esto permitirá la comunicación entre nuestro Worker y Chrome. Después de establecer la comunicación, ahora podemos tomar capturas de pantalla y más.

    ¡No olvides cerrar la conexión del navegador! Aunque si tu instancia (Chrome) no recibe algún comando durante 60 segundos entonces se cierra automáticamente.

    Más información en los límites de la plataforma en este enlace.

    5. Prueba la aplicación

    Cloudflare Wrangler nos provee de un comando para ejecutar y probar nuestra aplicación:

    wrangler dev --remote

    El comando tiene que ejecutarse dentro de tu proyecto, desde la consola de comandos, con la opción —-remote para que se conecte con los servidores de Cloudflare y ejecute la instancia de Chrome.

    Abre el localhost junto con el puerto que te ha dado el output del comando y abrelo en el navegador, por ejemplo:

    http://localhost:8787/?keyword=roel%20magdaleno

    Verás la imagen en tu navegador en aproximadamente 6 segundos:

    Lo que he notado al usar el Cloudflare Browser Rendering es que tarda alrededor de 2 o 3 segundos en abrir la instancia de Chrome y otros 3 segundos para abrir una ventana y realizar la búsqueda.

    Si tu objetivo es buscar múltiples palabras claves, entonces agrega un bucle en tu código para que busque en una sola instancia de Chrome.

    6. Sube la aplicación a Cloudflare Workers

    Ahora, si deseas subir la aplicación a tu cuenta Cloudflare, solo tienes que ejecutar el siguiente comando:

    wrangler deploy

    En la terminal verás el enlace generado por el comando y luce así:

    https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/?keyword=roel%20magdaleno

    Este enlace será el que usaremos para tomar las capturas de pantalla en producción.

    ¡Y listo! Has desarrollado un Cloudflare Worker que toma capturas de pantallas de un sitio web, sin embargo, como ya sabes, también puedes generar archivos PDF. ¿Cómo usarás esta herramienta?

    ¿Problemas con el Buffer de Node.js?

    Este problema ya ha sido solucionado en nuevas versiones del Cloudflare Browser Rendering pero si por alguna razón te sale un error con el Buffer, quiere decir que la librería de Puppeteer no está detectando esa funcionalidad de Node.js.

    Por suerte, alguien en el Discord de Cloudflare sugirió aplicar la siguiente solución:

    import { Buffer } from 'node:buffer';
    import puppeteer from "@cloudflare/puppeteer";
    
    globalThis.Buffer = Buffer;

    Declaramos el Buffer justo antes de puppeteer y lo registramos en el globalThis de nuestra aplicación. Reiniciamos la aplicación y debe funcionar perfecto.

    Referencias

  • Agrega un evento en múltiples elementos con event delegation

    Agrega un evento en múltiples elementos con event delegation

    En este post hablaremos sobre event delegation, en JavaScript. La solución a tus problemas de performance y escalabilidad en tu proyecto actual, donde tal vez tengas una tabla de usuarios.

    Cada usuario tiene los botones de editar o eliminar. Sin embargo, estás haciendo un loop para asignarles un evento click para hacer sus respectivas funciones.

    Esto soluciona tu requerimiento, pero, si tienes más de 1000 usuarios en esa tabla, ¿será bueno en performance? y mejor aún, ¿será escalable?

    ¿Qué es event delegation?

    El event delegation es un patrón en JavaScript que sugiere registrar un evento, por ejemplo, click, en nuestro document para interceptar todas las interacciones de nuestro documento.

    Es decir, si haces click en un botón, lo estás escuchando, y si haces click fuera del botón, también lo estás escuchando.

    ¿Suena ilógico no? ¿Por qué queremos escuchar todos los clicks de todo nuestro sitio web si solo podemos asignarle el evento a los elementos a través de un loop?

    Lo mismo pensé cuando escuché sobre esto la primera vez, pero cuando piensas en el performance y escalabilidad, empieza a cobrar sentido.

    ¿Cómo funciona event delegation?

    El truco está en que dentro de la función callback de nuestro evento, hay que usar el objeto event.target.

    document.addEventListener('click', () => {
       const clickedElement = event.target;
    });

    En nuestro ejemplo, event.target es equivalente al elemento que hemos hecho click, el botón, en este caso.

    Por otra parte, si haces click afuera del botón, el evento también se ejecutará. Es ahí donde tienes que agregar las condiciones necesarias para que el evento solo responda a los botones y no a todo el contenido del sitio.

    Funciones como matches() o closest() se usan mucho cuando se está trabajando con event delegation.

    Antes de event delegation, se usaba un loop

    Retomemos el ejemplo del primer párrafo. Tenemos la tabla de 1000 usuarios, y cada uno de ellos tiene un botón para editar; digamos que al hacer click a ese botón, se abre un modal con los datos del usuario para editar.

    Lo que yo solía hacer para abrir ese modal, es agregar un evento click a cada uno de los botones con la siguiente lógica:

    1. Obtener los botones.
    2. Pasar por un loop los botones.
    3. Asignar un evento a cada uno de los botones pasados en el loop.

    Observa el siguiente demo. Utilizo un forEach para hacer un loop de los elementos y asignarles el evento; también puedes usar for y for ... in. La funcionalidad es la misma.

    Si bien el código anterior funciona. ¿Qué es lo que lo hace incorrecto?

    Problema #1: Performance

    El performance es algo que debes incluir en tus desarrollos web. Es muy medido por Google y sus Core Web Vitals, además, un buen performance asegura una mejor experiencia para el usuario.

    Si tenemos 1000 usuarios y por cada uno podría haber dos botones, editar y eliminar, eso hace que nuestro sistema tenga 2 mil botones en total. Imagina agregar otro botón más.

    Ahora, con nuestro sistema actual tendría que hacer loop y agregar los eventos a esos 2 mil botones; y eso, no es para nada saludable para el sitio y navegador web. ¿Diría que incluso podría saturar el navegador?

    Otra técnica para un buen performance es ejecutar tu código solo en donde sea necesario.

    Problema #2: Escalabilidad

    Tu cliente quiere agregar usuarios en esa misma tabla usando AJAX. Se puede, sí; sin embargo, ¿nuestros botones de editar funcionarán después de agregar al usuario?

    Seguramente no funcionará porque, previamente, has agregado los eventos a los botones existentes, no a los nuevos.

    Uno piensa, ¡ah! pues después de crear el usuario, busco los nuevos botones y les asigno el evento. Si y no. Esto solo genera complejidad y duplicidad de código. No es escalable.

    Conclusión

    Debemos acostumbrarnos al uso del event delegation, pero siempre analiza si este tipo de patrón es el adecuado para los requerimientos de tu proyecto.

    Ya no podemos usar las viejas formas en las que programábamos con JavaScript. Tenemos que actualizar nuestro código para hacer el uso de la web más satisfactoria para los usuarios.

  • Envía peticiones AJAX antes de cerrar la página web

    Envía peticiones AJAX antes de cerrar la página web

    Actualmente desarrollo plugins para WordPress, y recién me topé con algo que nunca había pensado: “Enviar peticiones AJAX antes de que el usuario cierre nuestra página web“.

    Antes de empezar este post, dejemos claro dos cosas:

    1. Lo que veremos a continuación será completamente JavaScript.
    2. El problema anterior puede ser enfocado a cualquier tipo de aplicación web, no solo para WordPress.

    Indagué por la web

    La mayoría de las respuestas a mi problema provenían de StackOverflow, y mi sorpresa fue que, no soy el único buscando lo mismo.

    Hasta hace unos años, varias personas recomendaban usar el siguiente método:

    • Realizar peticiones síncronas.

    El problema es que esto provoca que el navegador o la pestaña tarden un poco en cerrar, debido a que la petición aún se sigue ejecutando.

    Sin embargo, en años más recientes, hubo una persona que comentó dos palabras:

    Beacon API

    ¡Ésta es la solución! Me dije.

    ¿Por qué? Pues, se veía sencillo de usar y su documentación decía lo que yo quería hacer en ese momento. Y lo logré.

    Un resumen de lo que dice su documentación:

    La Beacon API es usada para realizar peticiones asíncronas y no bloqueables a un servidor web. Aborda las necesidades de ejecutar código que intenta enviar datos al servidor web antes de que el documento se cierre.

    La documentación lo explica sencillo, y el código mucho más; pero antes de seguir, me gustaría explicar que es una petición AJAX.

    ¿Qué es una petición AJAX?

    Al principio de mi carrera, el hacer uso de algo llamado AJAX en mis desarrollos, era algo tenebroso. Tal vez para ti, mi querido lector, también lo fue.

    ¡No le tengas miedo! De hecho, creo que fue una de las mejores cosas que pude haber conocido, y lo mejor que le pasó a la web.

    AJAX significa JavaScript Asíncrono y XML, y nos sirve prácticamente para:

    • Obtener datos de un servidor web sin recargar la página.
    • Enviar datos a nuestro servidor sin que el usuario se vea interrumpido.

    Hacer uso de AJAX hoy en día es totalmente normal, y lo puedes implementar con varias tecnologías, ya sea usando VanillaJS, jQuery u otros.

    A continuación, te dejo un video de Platzi para una mejor comprensión del tema:

    ¿Qué es AJAX y cómo funciona? – Por Platzi

    Compatibilidad

    Antes de empezar a trabajar con la Beacon API, hay que tener en cuenta que, en este año 2018, esta API es compatible en la mayoría de los navegadores, a excepción de Internet Explorer y Opera Mini:

    Compatibilidad de la Beacon API
    Imagen 1. Compatibilidad de la Beacon API entre navegadores.

    Usando la Beacon API

    Pero volvemos al principio del problema:

    Enviar peticiones AJAX antes de que el usuario cierre nuestra página web.

    La Beacon API nos provee la siguiente función:

    navigator.sendBeacon( url, data );

    Esta función nos permite enviar los datos que queremos transmitir a nuestro servidor antes de cerrar nuestra página.

    Acepta dos parámetros:

    ParámetroDescripción
    urlÉsta será la URL a donde queremos transmitir los datos.
    dataEste es una variable que contendrá los datos a enviar en la petición.

    Cabe destacar que, el tipo de la variable a enviar debe ser:

    Cualquiera de éstos tipos es aceptable, escoge el que más te convenga. Sin embargo, en los siguientes ejemplos usaremos FormData.

    Ahora, la pregunta del millón, ¿cómo lo usamos para enviar datos antes de cerrar nuestra página web?

    Bien, tenemos que usar un evento de JavaScript llamado unload, éste se ejecuta justo antes de que la página sea cerrada o recargada:

    window.addEventListener( 'unload', sendData, false );

    Cuando la página sea cerrada, se ejecutará la función sendData:

    function sendData() {
       var formData     = new FormData();
       var currentTasks = getSomeTasks();
    
       if ( Object.keys( currentTasks ).length <= 1 ) {
          return;
       }
    
       formData.append( 'action', 'cronjob_task_system' );
       formData.append( 'currentTasks', JSON.stringify( currentTasks ) );
    
       navigator.sendBeacon( myObject.url, formData );
    }

    Primero, tenemos que hacer una instancia de FormData, ó cualquier tipo de variable que mencionamos anteriormente, y agregar los datos que queremos enviar a nuestro servidor.

    Puedes hacer cualquier operación antes de enviar los datos, por ejemplo, yo en el código digo que si mi variable currentTasks está vacío, no envíe nada al servidor.

    ¿Por qué mi código está en inglés? Chécalo en mi post sobre ¿por qué es necesario nombrar bien las cosas en programación?

    Ya que la lógica esté lista, pasamos los respectivos parámetros a navigator.sendBeacon( url, data ) y manejamos esos datos en nuestro servidor.

    Inconvenientes

    El uso de la Beacon API es impresionante, cumple con nuestros objetivos al querer enviar datos al servidor cuando la página se haya cerrado; sin embargo, existe un inconveniente.

    Si queremos implementar la Beacon API, dijimos que tenemos que ejecutarlo en el evento unload, ¿verdad?

    Sin embargo, este evento se ejecuta no sólo cuando la página es cerrada, si no que también se ejecuta cuando el usuario recarga la página, o cuando el usuario cambia de página dentro del mismo dominio.

    Tal vez sea un inconveniente o no, depende del tipo de proyecto que estés desarrollando.

    Posible solución a los inconvenientes

    Como dije, la Beacon API tendrá sus inconvenientes o no, dependiendo del tipo de proyecto que estés desarrollando, por ejemplo, si quieres enviar datos estadísticos de las partes donde navegan tus usuarios, éste método es perfecto.

    Pero si andas buscando algo que únicamente se ejecute al cerrar la página, tal vez tengas que usar algo parecido al Heartbeat API de WordPress, para que tu servidor se comunique si el usuario sigue en tu página o no.

    Conclusión

    La verdad es que, el uso de la Beacon API me ha salvado de implementar demasiado código en el proyecto que estaba desarrollando, y espero que te pueda servir a ti también.

    Sin embargo, el uso del evento unload, es algo que debe mejorarse para solucionar los inconvenientes que previamente mencionamos.

    ¿No te ha funcionado la implementación del código? Deja un comentario y con gusto te apoyo.