Dynamic import o importación dinámica se realiza a partir del método import()
que devuelve una promesa del módulo solicitado. Eso significa que podemos solicitar código en caso de que sea necesario. Lo cual mejora increíblemente el rendimiento de una aplicación.
// hi.js
export function hi() {
console.log("¡Hola 👋!");
}
// index.js
button.addEventListener("click", async function() {
const module = await import("./hi.js");
module.hi(); // "¡Hola 👋!"
});
Sintaxis
import("./module.js")
.then((module) => {
// Code ...
});
Ejemplo práctico
Supongamos que tenemos tres botones para un ecommerce y que cada botón necesita de un script diferente para mostrar la información. La clave es pasar por atributo el módulo se va a solicitar (data-module
).
<!DOCTYPE html>
<html lang="es">
<head>
<title>Dynamic imports</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<!-- Botones -->
<button class="btn" data-module="clients">Ver Clientes</button>
<button class="btn" data-module="products">Ver Productos</button>
<button class="btn" data-module="orders">Ver Pedidos</button>
<!-- Para usar los módulos se necesita el atributo type="module"-->
<script type="module" src="app.js"></script>
</body>
</html>
Los siguientes pasos son:
- Crear los eventos para capturar el click
- Tomar lo que contiene el atributo
- Asegurar la importación acorde al tipo de botón
- Hacer uso del script en caso de click
// app.js
const buttons = document.querySelectorAll(".btn");
for (const button of buttons) {
button.addEventListener("click", async function (event) {
event.preventDefault();
const moduleName = button.dataset.module;
try {
const module = await import(`./modules/${moduleName}.js`);
module.showInfo();
} catch (error) {
console.log(`Ocurrió un error: ${error}`);
}
});
}
El resultado es código que carga dinámicamente a solicitud del usuario.
Viendo los datos de las peticiones que se hicieron, se puede notar que el iniciador de app.js
es index.html
, pero, el iniciador de clients.js
, products.js
y orders.js
es el app.js
😯 .
Cargando módulos basado en rutas
Otro uso podría ser el cargar módulos dependiendo la ruta actual. Un ejemplo básico de lo que podría ser un router es lo siguiente:
const currentUrl = "home";
async function loadPage() {
try {
const module = await import(`./pages/${currentUrl}.js`);
module.start();
} catch(error){
console.log(`Ocurrión un error: ${error}`)
}
}
Cargando módulos con React
Una versión más completa de las rutas es la carga de módulos usando React Router y React.lazy
.
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</Suspense>
</Router>
);
Polyfills dinámicos
Un gran problema que hay con los pollyfills es que al agregarlos a un proyecto para dar soporte a ciertos navegadores, este se vuelve más pesado.
Bien por los navegadores que no soportan la característica que queremos usar, pero ¿qué pasa con los navegadores que si la soportan? Se estaría cargado código totalmente innecesario. ¡Ya no más! La solución vuelve a ser los dynamic imports.
Para este ejemplo voy a usar el método Object.values
, un método introducido en ES2016 y que devuelve un array con los valores de un objeto. Para más información te dejo mi BitSnack.
async function importPolyfill() {
if (!Object.values) {
const polyfill = await import("./modules/polyfill.js");
return polyfill;
}
}
importPolyfill().then(() => {
const obj = {
name: "Fili",
age: 24,
job: "Frontend Developer",
};
console.log(Object.values(obj)); // ["Fili", 24, "Frontend Developer"]
});
Incluso podrías tener un array de polyfills para activarlos dependiendo si se requiere el soporte o no.
Para ello voy a usar el método Promise.allSettled()
que devuelve una promesa que es resuelta después de que todas las promesas dadas hayan sido concluidas, sin importar si fueron resueltas o rechazadas. Te dejo mi BitCode para más información.
async function importPolyfill() {
const promises = [];
if (!Object.values) {
const objectValues = await import("./modules/object.values.js");
promises.push(objectValues);
}
if (!Object.entries) {
const objectEntries = await import("./modules/object.entries.js");
promises.push(objectEntries);
}
if (!Object.keys) {
const objectKeys = await import("./modules/object.keys.js");
promises.push(objectKeys);
}
return Promise.allSettled(promises);
}
importPolyfill().then(() => {
const obj = {
name: "Fili",
age: 24,
job: "Frontend Developer",
};
console.log(Object.keys(obj)); // ["name", "age", "job"]
console.log(Object.values(obj)); // ["Fili", 24, "Frontend Developer"]
console.log(Object.entries(obj)); // ["name", "Fili"] ["age", 24] ["job", "Frontend Developer"]
});
De esta manera, solo los usuarios que usan un navegador que no admita la característica descargarán el polyfill. También puede ser útil para medir cuántas personas realmente están usando el polyfill y verificar si es necesario.
Finalmente quisiera recomendarte no usar los dynamic imports para los datos iniciales de tu app, especialmente para la carga de lo primero que llega a ver un usuario.