Una de las tareas más comunes para unir dos interfaces que no se hablan de manera nativa es construir un paso intermedio que sirva de traducción entre ambas. Y uno de los formatos más comunes para dicho fin es el omnipresente archivo de Microsoft Excel.

Una de las bondades de JavaScript es su vasto ecosistema de librerías. Es muy difícil proponerse algo y no encontrar librerías / utilerías ya existentes que hagan buena parte del trabajo por nosotros. En este caso muy particular nos interesa una librería llamada SheetJS. Se trata de una librería dedicada para lectura y escritura de hojas de cálculo de forma nativa desde cualquier variante de JavaScript; desde el browser mediante JavaScript puro o bien mediante cualquier framework como Angular, React, Vue y similares y desde el servidor mediante Node.js.

Para instalarlo necesitamos que nuestro proyecto esté previamente ya inicializado con npm o yarn. En la raíz de nuestro proyecto utilizamos el siguiente comando para incluir la librería:

npm install xlsx
yarn install xlsx

El mecanismo para proveerle un archivo a la librería para que nos entregue el contenido dependerá completamente del entorno en el que estemos trabajando. Para este ejemplo puntual estoy utilizando React y he dispuesto una sencilla interfaz que le presenta al usuario un control de selección de archivos, sin embargo salvando las diferencias en interfaz de usuario se puede realizar prácticamente la misma operación desde Node.js media vez la librería pueda ser alimentada con un archivo.

Estoy utilizando el control FileInput de la colección de controles Blueprint para React en este ejemplo.

El código detrás de este control para selección de archivo es bastante básico:

<FileInput 
    hasSelection={this.state.fileLoaded}
    onInputChange={this.handleFileLoad}
    text={this.state.fileLoaded ? this.state.fileName : "Choose file..."} />

Lo más importante de este bloque de código es que estamos asociando la función handleFileLoad al evento onInputChange del control, por lo que sabemos que esta función será llamada cuando el evento se dispare en el control. Dicho de otra manera, este evento le avisará a nuestra función en el momento en el que el usuario provea un archivo mediante este control.

Para este ejemplo trabajaremos con un archivo Excel en su versión Office 365 que no es más que una sábana de datos sin nada particularmente complicado, ni siquiera fórmulas.

Nuestra hoja de cálculo para este ejemplo

Cuando seleccionamos este archivo en nuestro control entra a nuestra función handleFileLoad:

handleFileLoad = (e) => {
    if (e.target.files.length > 0) {
        try {
            this.setState({ operationInProgress: true, fileLoadProgress: 0 });
            let file = e.target.files[0];
            let reader = new FileReader();
    
            reader.onload = async function (e) {
                let data = new Uint8Array(e.target.result);
                let workbook = XLSX.read(data, { type: "array" });
                let worksheet = workbook.Sheets[workbook.SheetNames[0]];
                let sheet = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
    
                let dataset = await this.parseFileContent(sheet);
                this.setState({ 
                    dataset: dataset,
                    fileLoaded: true,
                    fileName: file.name,
                    operationInProgress: false
                });
            }.bind(this);
            reader.readAsArrayBuffer(file);
        } catch (exception) {
            this.setState({
                fileLoaded: false,
                fileName: "",
                operationInProgress: false
            });
        }
    } else {
        toast("No files found", { type: "error" });
    }
};

Para entender que hace cada parte de esta función, resaltemos algunas secciones:

let file = e.target.files[0];

Muchos controles de selección de archivos permiten seleccionar más de un archivo, por lo que usualmente vamos a tener un array con los archivos seleccionados. En este caso nuestro control únicamente permite seleccionar un archivo y por lo tanto sabemos que podemos encontrar el archivo seleccionado siempre en la posición 0.

let reader = new FileReader();
    
reader.onload = async function (e) {
    let data = new Uint8Array(e.target.result);
    let workbook = XLSX.read(data, { type: "array" });
    let worksheet = workbook.Sheets[workbook.SheetNames[0]];
    let sheet = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
    
    let dataset = await this.parseFileContent(sheet);
    this.setState({ 
        dataset: dataset,
        fileLoaded: true,
        fileName: file.name,
        operationInProgress: false
    });
}.bind(this);
reader.readAsArrayBuffer(file);

Acá es donde reside la mayor complejidad y a la vez la parte que más nos interesa. Empezamos por declarar una variable reader de tipo FileReader. Esta es una utilería nativa de JavaScript, no estamos haciendo uso de ninguna librería adicional. Acto seguido asignamos una función asíncrona al evento onload del reader que declaramos anteriormente. La función onload se trata de un disparador que se activará en el momento en que se le haya provisto de un archivo a la variable reader, por lo que acá el contenido de esta función anónima son las instrucciones que estamos solicitando se ejecuten al momento en que se dispare este evento.

Estas líneas son el corazón de este ejemplo y la demostración de la forma más básica de utilización de la librería SheetJS:

let data = new Uint8Array(e.target.result);
let workbook = XLSX.read(data, { type: "array" });
let worksheet = workbook.Sheets[workbook.SheetNames[0]];
let sheet = XLSX.utils.sheet_to_json(worksheet, { header: 1 });

La variable data convierte el contenido del archivo a un array y acto seguido le entregamos ese array a la variable workbook, que no es más que una instancia de SheetJS para que lea el contenido pero haciendo la salvedad que el contenido que le estamos entregando viene en un formato array. A continuación la variable worksheet toma el contenido de la variable workbook que declaramos en la línea anterior y que a estas alturas ya debiera tener el contenido que SheetJS trajo de nuestro archivo de Excel y accedemos a la propiedad Sheets de dicha variable, que literalmente son los libros que están contenidos en nuestro archivo de Excel. Acá por tratarse de un archivo estático en el cual sabemos que la estructura no va a variar podemos de manera segura acceder al primer libro (posición cero).

Por último, la hoja con el contenido estará en la variable sheet. Acá estamos utilizando la función sheet_to_json de SheetJS para que nos entregue el contenido de nuestro libro de Excel en un conveniente formato JSON. Acá también le estamos indicando a la función si nuestro archivo contiene un encabezado en la primera línea. En este caso es afirmativo, por lo que la propiedad header lleva el valor 1.

reader.readAsArrayBuffer(file);

Esta última línea del bloque es la que dispara finalmente la función onload que declaramos arriba. Ahora bien, algo que omití a propósito es el por qué declaré la función onload como asíncrona y es debido a que una vez SheetJS hace la lectura del archivo y nos entrega un objeto JSON estoy invocando una función adicional que es la que ya toma dicho objeto y lo procesa para hacer todo el trabajo adicional que necesito sobre este set de datos. Para este ejemplo puntual no nos interesa analizar qué hacer con dicho JSON pero para evitar trabajar con funciones adicionales opté por manejar la llamada a dicha función mediante el paradigma async / await de JavaScript, el cual en otra ocasión abordaremos más a detalle.

let dataset = await this.parseFileContent(sheet);

Basta con decir que para invocar una función asíncrona con await necesitamos que sea dentro de una función declarada como async, caso contrario no funcionará y tendríamos que hacerlo hipotéticamente de la manera larga:

parseFileContent(sheet)
.then(result => {
    dataset = result;
})
.catch(error => {
    // manejo de error
});

A todo esto seguramente nos estaremos preguntando cómo se ve el resultado final del trabajo de esta librería. Veamos el resultado en consola:

Este archivo en particular trae una primera línea como encabezado, por lo tanto el primer elemento del objeto no es más que todos los encabezados de columna de nuestro archivo Excel original. A partir del segundo elemento tenemos un array por cada línea en el archivo Excel:

Dependiendo del tamaño de nuestro archivo así será la longitud del array de objetos que SheetJS nos ha entregado en formato JSON:

A partir de este punto ya únicamente nos queda utilizar dicho objeto para realizar las transformaciones y operaciones necesarias. El trabajo de la librería ha concluido y hemos leído un archivo de Excel y podemos hacer uso de dicha información para utilizarla en alguna API, hacer operaciones contra una base de datos, desplegar en pantalla, etc.