Cómo importar un CSV usando Next.js y Node.js

Cómo analizar un CSV en una matriz de JavaScript y cargarlo en un servidor a través de búsqueda e insertarlo en una base de datos MongoDB.

Primeros pasos

Para este tutorial, vamos a utilizar CheatCode Node.js Boilerplate en el servidor y CheatCode Next.js Boilerplate en el cliente.

Comenzando con el modelo estándar de Node.js...

Terminal

git clone https://github.com/cheatcode/nodejs-server-boilerplate server

A continuación, instale las dependencias repetitivas:

Terminal

cd server && npm install

A continuación, inicie el modelo estándar de Node.js:

Terminal

npm run dev

Después de que el servidor se esté ejecutando, a continuación, queremos configurar el modelo estándar de Next.js. En otra pestaña o ventana de terminal, clona una copia:

Terminal

git clone https://github.com/cheatcode/nextjs-boilerplate client

A continuación, instale las dependencias repetitivas:

Terminal

cd client && npm install

Antes de comenzar con el repetitivo, necesitamos instalar una dependencia adicional, papaparse que usaremos para ayudarnos a analizar nuestro archivo CSV:

Terminal

npm i papaparse

Finalmente, con eso, continúe y ponga en marcha el repetitivo:

Terminal

npm run dev

Con eso, ¡estamos listos para comenzar!

Construyendo una ruta Express para manejar cargas

Para comenzar, configuraremos una ruta usando Express (ya implementado en el Boilerplate de Node.js que acabamos de configurar) donde cargaremos nuestro CSV:

/servidor/api/index.js

import Documents from "./documents";
import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  app.use("/uploads/csv", (req, res) => {
    // We'll handle our uploaded CSV here...
    res.send("CSV uploaded!");
  });
};

Dentro del repetitivo, un Express app la instancia se crea y se pasa a una serie de funciones en /server/index.js . Más específicamente, por defecto tenemos dos funciones que consumen el app instancia:middleware() y api() . El primero—definido en /middleware/index.js —es responsable de adjuntar nuestras funciones de middleware Express (código que se ejecuta antes de que cada solicitud recibida por nuestro servidor Express se transfiera a nuestras rutas). Este último, definido en /api/index.js —se encarga de adjuntar nuestras API relacionadas con los datos (de forma predeterminada, un servidor GraphQL).

En ese archivo, arriba, debajo de la llamada para configurar nuestro graphql() servidor (no usaremos GraphQL en este tutorial, así que podemos ignorar esto), estamos agregando una ruta a nuestro app instancia a través del .use() método en esa instancia. Como primer argumento, pasamos la URL en nuestra aplicación donde enviaremos un POST solicitud del navegador que contiene nuestros datos CSV.

De forma predeterminada, el modelo estándar comienza en el puerto 5001, por lo que podemos esperar que esta ruta esté disponible en http://localhost:5001/uploads/csv . Dentro de la devolución de llamada de la ruta, aunque no lo haremos esperar algo a cambio del cliente, para asegurarnos de que la solicitud no se cuelgue, respondemos con res.send() y un breve mensaje reconociendo una carga exitosa.

/servidor/api/index.js

import Documents from "./documents";
import graphql from "./graphql/server";
import generateId from "../lib/generateId";

export default (app) => {
  graphql(app);

  app.use("/uploads/csv", (req, res) => {
    const documentsFromCSV = req?.body?.csv;

    for (let i = 0; i < documentsFromCSV.length; i += 1) {
      Documents.insertOne({
        _id: generateId(),
        ...(documentsFromCSV[i] || {}),
      });
    }

    res.send("CSV uploaded!");
  });
};

Al agregar la funcionalidad que realmente buscamos, arriba, hemos agregado dos cosas importantes:

  1. Una expectativa de algún documentsFromCSV se nos pasa a través del csv campo en el req.body (POST cuerpo de la solicitud).
  2. Un bucle sobre esos documentsFromCSV , agregando cada uno a una colección de MongoDB que hemos importado arriba llamada Documents (La definición de esto se incluye en el modelo de Node.js para nosotros como ejemplo).

Para cada iteración del ciclo, esto se ejecutará cinco veces como nuestra prueba .csv el archivo tendrá cinco filas; llamamos a Documents.insertOne() , pasando un _id establecer igual a una llamada al generateId() incluido función de /server/lib/generateId.js (esto genera una cadena hexadecimal aleatoria única de 16 caracteres de longitud).

A continuación, usamos JavaScript ... operador de propagación para decir "si hay un objeto en el documentsFromCSV matriz en la misma posición (índice) que el valor actual de i , devuélvalo y 'descomprima' su contenido en el objeto junto con nuestro _id (el documento que finalmente insertaremos en la base de datos)." Si por alguna razón no tenemos un documento, recurrimos a un objeto vacío con || {} para evitar un error de tiempo de ejecución. Alternativamente (y preferiblemente, si sus datos pueden o no ser consistentes), podríamos ajustar la llamada a Documents.insertOne() en un if declaración que verifica esto incluso antes de que lo llamemos.

Eso es todo para el servidor. A continuación, pasemos al cliente y veamos cómo analizar nuestro archivo CSV y subirlo.

Conectando un componente React para analizar y cargar nuestro CSV

Ahora, en el cliente, configuraremos un componente React con una entrada de archivo que nos permitirá seleccionar un CSV, analizarlo en un objeto JavaScript y luego cargarlo en el punto final que acabamos de definir en el servidor.

/cliente/páginas/subir/index.js

import React, { useState } from "react";

const Upload = () => {
  const [uploading, setUploading] = useState(false);

  const handleUploadCSV = () => {
    // We'll handle our CSV parsing and upload here...
  };

  return (
    <div>
      <h4 className="page-header mb-4">Upload a CSV</h4>
      <div className="mb-4">
        <input disabled={uploading} type="file" className="form-control" />
      </div>
      <button
        onClick={handleUploadCSV}
        disabled={uploading}
        className="btn btn-primary"
      >
        {uploading ? "Uploading..." : "Upload"}
      </button>
    </div>
  );
};

Upload.propTypes = {};

export default Upload;

Aquí, estamos usando el patrón de componente de función en React para definir un componente llamado Upload . Debido a que estamos usando Next.js (un marco construido alrededor de React), estamos definiendo nuestro componente en el /pages carpeta, anidada en su propia carpeta en /pages/upload/index.js . Al hacer esto, Next.js mostrará automáticamente el componente que estamos definiendo arriba en el navegador cuando visitemos el /upload ruta (el texto estándar comienza en el puerto 5000 de forma predeterminada, estará disponible en http://localhost:5000/upload ).

Centrándose en el return valor dentro del Upload función—de nuevo, esta es una función componente, así que nada más que una función de JavaScript:estamos devolviendo un marcado que representará nuestro componente. Debido a que el repetitivo usa el marco CSS de Bootstrap, aquí hemos renderizado algunas marcas básicas para darnos un título, una entrada de archivo y un botón en el que podemos hacer clic para iniciar una carga con estilo usando el CSS de ese marco.

Centrándose en el useState() llamada a la función en la parte superior de nuestro componente, aquí, estamos configurando un valor de estado que se usará para controlar la visualización de nuestra entrada y el botón cuando estemos cargando un archivo.

Al llamar al useState() , le pasamos un valor predeterminado de false y luego esperar que nos devuelva una matriz de JavaScript con dos valores:el valor actual y un método para establecer el valor actual. Aquí, usamos la desestructuración de matrices de JavaScript para permitirnos asignar variables a estos elementos en la matriz. Esperamos nuestro valor actual en la posición 0 (el primer elemento de la matriz), y lo hemos asignado a la variable uploading aquí. En la posición 1 (el segundo elemento de la matriz), hemos asignado la variable setUploading (esperamos que esta sea una función que establecerá nuestro uploading valor).

Abajo en el return valor, podemos ver uploading siendo asignado al disabled atributo en nuestro <input /> así como nuestro <button /> . Cuando uploading es true , queremos deshabilitar la capacidad de seleccionar otro archivo o hacer clic en el botón de carga. Además de esto, para agregar contexto a nuestros usuarios, cuando uploading es cierto, queremos cambiar el texto de nuestro botón a "Cargando..." y cuando no subiendo a "Subir".

Con todo eso en su lugar, ahora veamos el handleUploadCSV función que hemos eliminado cerca de la mitad de nuestro componente. Cabe destacar que estamos llamando a esta función cada vez que nuestro <button /> se hace clic.

Analizando y subiendo nuestro archivo CSV

Ahora viene la parte divertida. Desarrollemos ese handleUploadCSV funciona un poco y haz que esto funcione.

/cliente/páginas/subir/index.js

import React, { useState, useRef } from "react";
import Papa from "papaparse";

const Upload = () => {
  const [uploading, setUploading] = useState(false);
  const inputRef = useRef();

  const handleUploadCSV = () => {
    setUploading(true);

    const input = inputRef?.current;
    const reader = new FileReader();
    const [file] = input.files;

    reader.onloadend = ({ target }) => {
      const csv = Papa.parse(target.result, { header: true });
    };

    reader.readAsText(file);
  };

  return (
    <div>
      <h4 className="page-header mb-4">Upload a CSV</h4>
      <div className="mb-4">
        <input ref={inputRef} disabled={uploading} type="file" className="form-control" />
      </div>
      <button
        onClick={handleUploadCSV}
        disabled={uploading}
        className="btn btn-primary"
      >
        {uploading ? "Uploading..." : "Upload"}
      </button>
    </div>
  );
};

Upload.propTypes = {};

export default Upload;

Hemos agregado un poco de detalle; vamos a caminar a través de él. Primero, cuando llamamos para subir nuestro CSV, lo primero que queremos hacer es deshabilitar temporalmente nuestro <input /> y <button /> , entonces llamamos a setUploading() pasando true (esto activará una nueva representación en React automáticamente, haciendo que nuestra entrada y el botón sean temporalmente inaccesibles).

Luego, para obtener acceso al archivo seleccionado por nuestro usuario, agregamos algo especial a nuestro componente. En React, mientras podemos acceder técnicamente a los elementos representados en el DOM utilizando métodos tradicionales como document.querySelector() , es mejor si usamos una convención llamada refs.

Las referencias, abreviatura de referencias, son una forma de darnos acceso a un elemento DOM en particular tal como lo representa React a través de una variable. Aquí, hemos agregado la función useRef() a nuestro react importar arriba y justo debajo de nuestra llamada a useState() han definido una nueva variable inputRef configurado para una llamada a useRef() .

Con ese inputRef , abajo en nuestro return valor, le asignamos un ref atributo a nuestro <input /> elemento, pasando el inputRef variable. Ahora, automáticamente, cuando React renderice este componente, verá este ref valor y asignar inputRef de vuelta al nodo DOM que representa.

De vuelta en handleUploadCSV , usamos esto llamando a inputRef?.current . Aquí, current representa el nodo DOM representado actualmente (literalmente, el elemento tal como se representa en el navegador). El inputRef? parte solo dice "if inputRef está definido, danos su current valor (abreviatura de inputRef && inputRef.current )".

Con eso almacenado en una variable, a continuación, creamos una instancia del FileReader() nativo clase (significa nativo que está integrado en el navegador y no hay nada que instalar). Al igual que las sugerencias de nombres, esto nos ayudará a manejar la lectura real del archivo que nuestro usuario selecciona a través de nuestro <input /> en la memoria.

Con nuestro reader instancia, a continuación, necesitamos obtener acceso a la representación DOM de nuestro archivo, por lo que llamamos a input (que contiene nuestro nodo DOM) y acceda a su files propiedad. Esto contiene el archivo seleccionado por el usuario en una matriz, por lo que aquí, usamos la desestructuración de la matriz de JavaScript nuevamente para "arrancar" el primer elemento de esa matriz y asignarlo a la variable file .

A continuación, en la parte inferior de nuestra función, observe que estamos haciendo una llamada a reader.readAsText(file) . Aquí le decimos a nuestro FileReader() instancia para cargar el file nuestro usuario seleccionó en la memoria como texto sin formato. Justo encima de esto, agregamos una función de devolución de llamada .onloadend que es llamado automáticamente por reader una vez que haya "leído" el archivo en la memoria.

Dentro de esa devolución de llamada, esperamos obtener acceso al evento de JavaScript que representa el onloadend evento como el primer argumento pasado a la función de devolución de llamada. En ese objeto de evento, esperamos un target atributo que en sí mismo contendrá un result atributo. Porque le preguntamos al reader para leer nuestro archivo como texto sin formato, esperamos target.result para contener el contenido de nuestro archivo como una cadena de texto sin formato.

Finalmente, utilizando el Papa objeto que importamos a través del papaparse paquete que instalamos anteriormente, llamamos al .parse() función que pasa dos argumentos:

  1. Nuestro target.result (la cadena de texto sin formato que contiene nuestro .csv contenido del archivo).
  2. Un objeto de opciones para papaparse que establece el header opción a true lo cual es interpretado por la biblioteca como esperando que la primera fila en nuestro CSV sean los títulos de columna que queremos usar como propiedades de objeto en los objetos generados por papaparse (uno por fila en nuestro CSV).

Ya casi hemos terminado. Ahora, con nuestro csv analizado , estamos listos para llamar a nuestro servidor y cargar esto.

Subiendo nuestro CSV al servidor

Ultima parte. Escupamos todo el código y analicemos:

/cliente/páginas/subir/index.js

import React, { useState, useRef } from "react";
import Papa from "papaparse";
import pong from "../../lib/pong";

const Upload = () => {
  const [uploading, setUploading] = useState(false);
  const inputRef = useRef();

  const handleUploadCSV = () => {
    setUploading(true);

    ...

    reader.onloadend = ({ target }) => {
      const csv = Papa.parse(target.result, { header: true });

      fetch("http://localhost:5001/uploads/csv", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          csv: csv?.data,
        }),
      })
        .then(() => {
          setUploading(false);
          pong.success("CSV uploaded!");
        })
        .catch((error) => {
          setUploading(false);
          console.warn(error);
        });
    };

    reader.readAsText(file);
  };

  return (...);
};

Upload.propTypes = {};

export default Upload;

Para hacer nuestra carga, vamos a usar el navegador incorporado fetch() función. Recuerde que anteriormente en el tutorial, configuramos nuestra ruta en el servidor en /uploads/csv y sugirió que estará disponible en http://localhost:5001/uploads/csv . Aquí, continuamos con esa suposición, pasándola como la URL de nuestro fetch() solicitud.

A continuación, como segundo argumento de fetch() , pasamos un objeto de opciones que describe la solicitud. Porque queremos enviar nuestros datos en el body de nuestra solicitud, configuramos el HTTP method campo a POST . A continuación, configuramos el Content-Type encabezado a application/json para que nuestro servidor sepa que nuestra solicitud body contiene datos en formato JSON (si tiene curiosidad, esto le dice a nuestro bodyParser software intermedio en /server/middleware/bodyParser.js cómo convertir los datos del cuerpo sin procesar antes de entregarlos a nuestras rutas).

Ahora, para la parte importante, al body propiedad pasamos un objeto a JSON.stringify()fetch() espera que pasemos el cuerpo de nuestra solicitud como una cadena, y en ese objeto, establecemos el csv propiedad que hemos anticipado en el servidor, igual al csv.data propiedad. Aquí, csv representa la respuesta que recibimos de Papa.parse() y data contiene la matriz de filas en nuestro CSV analizado como objetos JavaScript (recuerde que en el servidor, recorremos esta matriz).

Finalmente, porque esperamos fetch() para devolvernos una promesa de JavaScript, agregamos dos funciones de devolución de llamada .then() y .catch() . El primero maneja el estado de "éxito" si nuestra carga es exitosa y el segundo maneja cualquier error que pueda ocurrir. Dentro de .then() , nos aseguramos de setUploading() a false para hacer nuestro <input /> y <button /> accesible de nuevo y use el pong biblioteca incluida en el repetitivo para mostrar un mensaje de alerta cuando nuestra carga sea exitosa. En el .catch() , también setUploading() a false y luego cierre la sesión del error en la consola del navegador.

¡Hecho! Ahora, cuando seleccionemos nuestro archivo CSV (tome un archivo de prueba aquí en Github si no tiene uno) y haga clic en "Cargar", nuestro archivo se analizará, se cargará en el servidor y luego se insertará en la base de datos.

Terminando

En este tutorial, aprendimos cómo crear un componente React con una entrada de archivo que nos permitió seleccionar un .csv archivo y súbalo al servidor. Para hacerlo, usamos la API FileReader de HTML5 junto con el papaparse biblioteca para leer y analizar nuestro CSV en un objeto JavaScript.

Finalmente, usamos el navegador fetch() para entregar ese CSV analizado al servidor donde definimos una ruta Express que copió nuestros datos CSV en una colección de base de datos MongoDB.