Cómo cargar secuencias de comandos de terceros de forma dinámica en JavaScript

Cómo cargar dinámicamente una biblioteca de JavaScript como Google Maps escribiendo un script para inyectar automáticamente una etiqueta en su página.

Primeros pasos

Para este tutorial, vamos a utilizar el marco JavaScript de pila completa de CheatCode, Joystick. Joystick reúne un marco de interfaz de usuario de front-end con un back-end de Node.js para crear aplicaciones.

Para comenzar, querremos instalar Joystick a través de NPM. Asegúrese de estar usando Node.js 16+ antes de instalar para garantizar la compatibilidad (lea este tutorial primero si necesita aprender a instalar Node.js o ejecutar varias versiones en su computadora):

Terminal

npm i -g @joystick.js/cli

Esto instalará Joystick globalmente en su computadora. Una vez instalado, vamos a crear un nuevo proyecto:

Terminal

joystick create app

Después de unos segundos, verá un mensaje desconectado de cd en su nuevo proyecto y ejecute joystick start :

Terminal

cd app && joystick start

Después de esto, su aplicación debería estar ejecutándose y estamos listos para comenzar.

Creación de un cargador de secuencias de comandos dinámico

En JavaScript, una práctica común es cargar otros paquetes y bibliotecas en su aplicación. Tradicionalmente, esto se hace a través de un administrador de paquetes como NPM (Node Package Manager), pero a veces, necesitamos cargar JavaScript dinámicamente .

"Dinámicamente" también se puede leer como "sobre la marcha" o "desde un servidor de terceros". Generalmente, la razón por la que hacemos esto es que la secuencia de comandos en cuestión requiere una clave API o alguna otra forma de autenticación antes de que la secuencia de comandos pueda cargarse (o, la secuencia de comandos se aloja de forma remota por motivos de seguridad para evitar que se altere).

Mientras podemos agrega un <script></script> etiquete directamente en nuestro index.html principal archivo, es probable que esto sea excesivo, ya que solo necesitará ciertos scripts en ciertas páginas. Para evitar esto, podemos escribir un cargador de secuencias de comandos dinámico que se puede llamar a pedido desde páginas donde se necesita una secuencia de comandos.

/lib/loadScript.js

const urls = {
  googleMaps: `https://maps.googleapis.com/maps/api/js?key=${joystick.settings.public.googleMaps.apiKey}&libraries=places`,
};

export default (name = '', callback = null) => {
  const url = name && urls[name];

  if (!name || !url) {
    throw new Error(`Must pass the name of a supported script: ${Object.keys(urls).join(', ')}`);
  }
};

Al comenzar con nuestro script, nuestro objetivo es crear una función que podamos importar donde la necesitemos en nuestro código. Para hacerlo posible, aquí creamos un archivo donde export default una función que toma dos argumentos:

  1. name - El nombre del script que estamos tratando de cargar.
  2. callback - Una función de devolución de llamada para llamar después nuestro script se ha cargado.

Para name , esperamos que sea un nombre que hemos creado . En nuestro ejemplo aquí, vamos a cargar la API JavaScript de Google Maps. Arriba, podemos ver que se está creando un objeto urls que tiene una propiedad googleMaps definido en él, establecido en la URL que Google nos proporciona para su biblioteca de JavaScript.

En la URL aquí, hemos reemplazado el apiKey parámetro de consulta que Google Maps espera con un puntero a un valor global del archivo de configuración en nuestra aplicación:joystick.settings.public.googleMaps.apiKey .

Aquí, joystick.settings es un valor global en el navegador que se completa automáticamente con el contenido del archivo de configuración ubicado en /settings.development.json en la raíz de nuestra aplicación. Haciendo uso de esta convención aquí, estamos diciendo que esperamos que haya un valor en ese archivo de configuración ubicado en apiKey anidado en el public.googleMaps objeto, así:

/configuraciones.desarrollo.json

{
  "config": {
    "databases": [
      {
        "provider": "mongodb",
        "users": true,
        "options": {}
      }
    ],
    "i18n": {
      "defaultLanguage": "en-US"
    },
    "middleware": {},
    "email": {
      "from": "",
      "smtp": {
        "host": "",
        "port": 587,
        "username": "",
        "password": ""
      }
    }
  },
  "global": {},
  "public": {
    "googleMaps": {
      "apiKey": "apiKey1234"
    }
  },
  "private": {}
}

Así que está claro, la línea https://maps.googleapis.com/maps/api/js?key=${joystick.settings.public.googleMaps.apiKey}&libraries=places arriba será leído por JavaScript como https://maps.googleapis.com/maps/api/js?key=apiKey1234&libraries=places . El remate es que la variable pasó en el ${} parte será reemplazada por el valor en nuestro archivo de configuración (esto se conoce como interpolación de cadenas de JavaScript).

/lib/loadScript.js

const urls = {
  googleMaps: `https://maps.googleapis.com/maps/api/js?key=${joystick.settings.public.googleMaps.apiKey}&libraries=places`,
};

export default (name = '', callback = null) => {
  const url = name && urls[name];

  if (!name || !url) {
    throw new Error(`Must pass the name of a supported script: ${Object.keys(urls).join(', ')}`);
  }
};

Volviendo a centrarnos en nuestro código, con nuestra clave API incrustada, asumiendo que nuestro urls el objeto tiene una propiedad que coincide con name argumento pasado a nuestro loadScript() función, justo dentro de esa función intentamos obtener la URL del script que queremos cargar con name && urls[name] . Esto dice "si name está definido y puede encontrar una propiedad en el urls objeto que coincide con este name , devuélvenos su valor."

En JavaScript, este urls[name] se conoce como "notación de paréntesis". Esto nos permite recuperar dinámicamente valores de un objeto usando alguna variable o valor. Para ser claros, si nuestro urls objeto tenía una propiedad pizza establecido en https://marcospizza.com definido en él y pasamos 'pizza' como el name para nuestro script, esperaríamos el url variable aquí para establecerse en https://marcospizza.com .

Justo debajo de esto, para estar seguros, hacemos una verificación rápida para decir "si no tenemos un name definido, o no tenemos un url 'definido' arroja un error". Esto evitará que nuestro script se cargue y nos advertirá en la consola del navegador para que podamos solucionar el problema.

/lib/loadScript.js

const urls = {
  googleMaps: `https://maps.googleapis.com/maps/api/js?key=${joystick.settings.public.googleMaps.apiKey}&libraries=places`,
};

export default (name = '', callback = null) => {
  const url = name && urls[name];

  if (!name || !url) {
    throw new Error(`Must pass the name of a supported script: ${Object.keys(urls).join(', ')}`);
  }

  const existingScript = document.getElementById(name);

  if (!existingScript) {
    const script = document.createElement('script');
    script.src = url;
    script.id = name;
    document.body.appendChild(script);

    script.onload = () => {
      if (callback) callback();
    };
  }

  if (existingScript && callback) callback();
};

Desarrollando el resto de nuestra función, ahora nos adentramos en las cosas divertidas. Suponiendo que un name se pasó y coincidió con una propiedad en nuestro urls objeto (lo que significa que obtuvimos un url ), lo siguiente que debemos hacer es asegurarnos de que no hayamos cargado el script en cuestión antes.

¡Esto es importante! Debido a que estamos cargando JavaScript dinámicamente, en términos generales, existe la posibilidad de que nuestra función se llame varias veces (ya sea intencionalmente o accidentalmente). Porque nuestro script se va a agregar o agrega un <script></script> etiqueta a nuestro HTML, queremos evitar la creación de duplicados de la misma. Aquí, buscamos un <script></script> existente etiqueta con un id atributo igual al name pasamos a loadScript .

Si lo encontramos, saltamos al final de nuestra función y, asumiendo que tenemos un callback función definida, llame a esa función (lo que indica que "sí, este script ya se cargó y se puede usar").

Si no encuentra un existingScript , queremos cargarlo dinámicamente. Para hacerlo, primero creamos un nuevo <script></script> elemento de etiqueta en memoria (lo que significa que aún no se representa en la página, solo en el almacenamiento de memoria del navegador). Esperamos que esto cree un elemento DOM (un objeto en lo que respecta a nuestro código) que almacenamos en la variable script .

En ese objeto, podemos establecer atributos en nuestro nuevo <script></script> etiqueta dinámicamente. Aquí, queremos configurar el src atributo al url obtuvimos del urls objeto de arriba y el id atributo al name pasamos a loadScript() .

Con esos atributos establecidos, nuestro script está listo para ser agregado o "renderizado" al HTML de nuestro navegador. Para hacerlo llamamos al document.body.appendChild() pasando nuestro script variable (JavaScript reconocerá el formato del objeto como un elemento DOM válido y lo agregará según lo solicitado). Porque estamos diciendo document.body aquí, podemos esperar este <script></script> etiqueta para que se agregue literalmente como el último elemento dentro de nuestro <body></body> de HTML etiqueta:

Finalmente, después de agregar nuestro script, asignamos un onload función que es la función que llamará nuestro navegador una vez que el archivo se encuentre en el url establecemos en src está cargado. Dentro, si nuestro callback está definido, lo llamamos.

Eso lo hace para la definición de nuestro cargador. A continuación, echemos un vistazo a ponerlo en uso y veamos cómo funciona.

Llamar al cargador de secuencias de comandos dinámicas

Para poner en uso nuestro cargador, vamos a hacer uso de la función de componentes integrada en el marco de Joystick con el que comenzamos al principio del tutorial. Cuando ejecutamos joystick create app , se nos dio automáticamente un componente en /ui/pages/index/index.js en nuestro proyecto. Abramos ese archivo y extraigamos nuestro loadScript() función.

/ui/pages/index/index.js

import ui, { get } from "@joystick.js/ui";
import Quote from "../../components/quote";
import loadScript from "../../../lib/loadScript";

const Index = ui.component({
  lifecycle: {
    onMount: (component) => {
      loadScript('googleMaps', () => {
        new google.maps.Map(document.getElementById("map"), {
          center: { lat: -34.397, lng: 150.644 },
          zoom: 8,
        });
      });
    },
  },
  methods: { ... },
  events: { ... },
  css: `
    div p {
      font-size: 18px;
      background: #eee;
      padding: 20px;
    }

    #map {
      width: 100%;
      height: 300px;
    }
  `,
  render: ({ component, i18n }) => {
    return `
      <div>
        <p>${i18n("quote")}</p>
        ${component(Quote, {
          quote: "Light up the darkness.",
          attribution: "Bob Marley",
        })}
        <div id="map"></div>
      </div>
    `;
  },
});

export default Index;

Arriba, nosotros import loadScript del /lib/loadScript.js ruta donde lo creamos (omitiendo el .js al final está bien aquí ya que nuestra herramienta de compilación intentará cargar automáticamente un .js archivo en esta URL como parte del proceso de importación).

La parte a la que queremos prestar atención es el lifecycle.onMount función que se define cerca de la parte superior de nuestro componente. Si miramos dentro de esa función, estamos llamando a nuestro loadScript() función pasando primero el name del script que queremos cargar, seguido de nuestro callback función. Mire de cerca la devolución de llamada. Recuerde:nuestro objetivo es cargar la biblioteca de Google Maps para que podamos usarla inmediatamente después de cargarla. Aquí, porque nuestro callback se activa después de cargar nuestro script, podemos suponer que Google Maps está disponible.

Siguiendo esa suposición, hacemos una llamada al new google.maps.Map() método, pasando el nodo DOM donde queremos cargar nuestro mapa (si miramos hacia abajo en el render() función de nuestro componente, podemos ver un <div id="map"></div> siendo renderizado como un marcador de posición donde nuestro mapa debería ser renderizado. Aquí decimos document.getElementById() para obtener ese <div></div> nodo DOM del elemento en el navegador.

Eso es todo. Si echamos un vistazo a nuestra aplicación en el navegador en http://localhost:2600 después de unos milisegundos, deberíamos ver la carga de nuestro mapa de Google (si no es así, verifique dos veces su clave API y que los bloqueadores de anuncios estén desactivados).

Terminando

En este tutorial, aprendimos cómo escribir una función para ayudarnos a crear e inyectar dinámicamente un <script></script> etiqueta en nuestro HTML. Para hacerlo, tomamos el nombre de un script y lo asignamos a una URL donde ese script vive en un objeto y luego usamos el document.createElement() función de JavaScript para crear una etiqueta de script antes de agregarla al <body></body> etiqueta en nuestro DOM. Finalmente, aprendimos cómo llamar a nuestro loadScript() función para representar un mapa de Google Maps en la página.