Подробное руководство

Node.js имеет довольно надежную систему управления файлами с множеством встроенных функций для работы с файлами. В дополнение к отличным встроенным функциям Node, JSZip представляет собой удобную библиотеку JavaScript для создания, чтения и редактирования zip-файлов. Я подробнее расскажу об этом позже, а пока вот некоторый контекст для создания файла.

Пользователь должен иметь возможность сгенерировать простой шаблон данных для отправки позже. Этот шаблон состоит из различных файлов CSV, которые будут использоваться для обработки данных. Всего существует шесть файлов CSV, но один файл должен создаваться динамически на основе ввода данных пользователем. Назовем это nums.csv.

ПРИМЕЧАНИЕ. Поскольку другие наши файлы являются статическими, они будут храниться в своем собственном каталоге шаблонов в проекте для справки. Подробнее об этом позже.

nums.csv должен иметь возможность иметь строки от одной до десяти, в зависимости от ввода пользователя. Количество строк играет решающую роль в том, как обрабатываются данные. Прежде чем мы займемся этим файлом, давайте посмотрим на наш пользовательский интерфейс, созданный с использованием React и Ant Design.

Здесь у нас есть кнопка, при нажатии на которую открывается наша кнопка ввода номера и отправки.

Пользователь выбирает, сколько nums (что будет соответствовать количеству строк и значений в этих строках) они хотят сгенерировать. Введенный ими номер хранится в состоянии. Когда они нажимают Загрузить, выполняется createTemplate. См. ниже.

Чтобы создать жизнеспособный контент CSV, нам нужно будет создать массивы для каждой строки. Информация в массивах будет соответствовать строкам. В первой строке мы инициализируем массив с именем rows, содержащий массив строк (Id, Name и SortOrder). Это будут заголовки столбцов в нашем nums.csv файле.

Затем createTemplate использует this.state.nums, чтобы определить, сколько строк нужно сгенерировать для нашей переменной rows. Если this.state.nums равно трем, функция трижды добавит массив (строку), содержащий значения для Id, Name и SortOrder. Первая строка будет иметь Id, равный единице, Name, равный NUM 1, и SortOrder, равный единице.

Когда у нас есть массив массивов (строк), мы объявляем переменную csvContent. csvContent будет хранить данные CSV в виде строки. Нам нужно отобразить более rows и для каждого элемента массива выполнить .join(","). Это преобразует каждый массив в строку. Почему мы соединяем наши данные запятой? Запятые - это то, как файлы CSV разделяют значения столбцов. Фактически, CSV означает значения, разделенные запятыми.

Теперь у нас есть массив строк. Последний шаг в окончательной доработке содержимого нашей строки CSV - выполнить еще один .join() на нашем массиве строк. В настоящее время наш массив строк выглядит так:

["Id,Name,SortOrder", "1,NUM 1,1", "2,NUM 2,2", "3,NUM 3,3"]

Мы объединяем наши данные с \n, потому что этот символ указывает на разрыв строки. Если бы мы не включали это, все наши данные отображались бы в одной строке. \n позволяет нам начинать новую строку всякий раз, когда мы переходим к следующему элементу. Таким образом, \n будет начинать новую строку между Id, Name, SortOrder и перед 1, NUM 1,1. В итоге csvContent будет выглядеть так:

"Id,Name,SortOrder
1,NUM 1,1
2,NUM 2,2
3,NUM 3,3"

Следующие несколько шагов приведут нас от неудобной строки выше к организованным данным ниже.

Теперь, когда у нас есть csvContent, мы можем передать его this.props.createTemplate. Эта функция передает {csv: csvContent} действию. Когда действие отправлено, на контроллер отправляется запрос. Именно в контроллере мы будем создавать наши файлы.

Прежде чем мы перейдем к функции, нам нужны следующие собственные зависимости Node:

path = require(“path”),
fs = require(“fs”) 
//don’t need npm install for either of these

Как и наш пакет npm для создания zip-файлов, JSZip.

JSZip = require(“jszip”); //npm install needed

Теперь мы можем перейти к функции, которая создаст все наши файлы, createTemplate.

В этой функции много кода, поэтому давайте посмотрим на нее построчно.

const dir = path.resolve(__dirname) + '/template/';

В этой строке объявляется каталог, в котором находятся другие пять упомянутых ранее CSV-файлов. См. ниже.

Этот путь нужен нам, чтобы читать данные других файлов и заархивировать их вместе с переданными nums.csv данными. path.resolve() преобразует имя каталога текущего модуля (__dirname) в абсолютный путь (/template/). Таким образом, нам не нужно менять путь в зависимости от среды, он всегда относительно каталога проекта.

var zip = new JSZip();

zip.file('nums.csv', req.body.body.csv) 

Эти строки создают экземпляр JSZip (наш zip-файл), а затем добавляют к нему наш nums.csv файл. .file() - это функция JSZip, которая позволяет нам добавить файл в zip и присвоить ему имя файла. req.body.body.csv - это csvContent, который мы отправили в качестве полезной нагрузки.

А теперь перейдем к fs.readdir().

fs - это модуль Node для взаимодействия с файловой системой. fs.readdir() читает указанный каталог и выполняет функцию обратного вызова. Этот обратный вызов содержит два аргумента: файлы (имена файлов и их расширения) и err, который представляет возможную ошибку, с которой могла столкнуться fs.readdir. В нашем случае мы используем эту функцию для чтения нашей переменной dir для доступа к файлам в/template/. async, который стоит перед нашей функцией обратного вызова, имеет решающее значение, потому что мы хотим читать файлы асинхронно. Это гарантирует, что мы не вернем данные до, чтобы прочитать все файлы и добавить их в наш zip-архив. Далее нам нужно посмотреть на promises.

var promises = files.map(function(_path){...} 

Мы сопоставляем файлы и возвращаем Promise для каждого имени файла, потому что мы хотим выполнить другое асинхронное действие, fs.readFile(). Мы не хотим возвращать какие-либо данные, пока не будут прочитаны данные файла (а не только имя и расширение файла).

Без обещаний эта функция вернула бы пустой объект в качестве ответа, потому что функция продолжила бы работу, не дожидаясь fs.readFile(). Мы вернемся к обещаниям через мгновение, а пока продолжим с fs.readFile().

fs.readFile(dir + _path, 'utf8', function(err, data){
  if(err){
    console.log(err);
    resolve("");
  } else {
    resolve({filename: _path, content: data});
  }
});

Мы объединяем dir + _path, чтобы создать абсолютный путь, необходимый для доступа к файлу в нашем каталоге. Если dir === 'ROOT_PROJECT_DIRECTORY/template/' и _path === file.csv, то объединение обоих даст нам 'ROOT_PROJECT_DIRECTORY/template/file.csv', который является абсолютным путем к этому файлу. 'utf8' определяет формат кодирования, а наша функция обратного вызова, которая принимает err и data, либо разрешит, либо отклонит текущий Promise.

data - это прочитанные данные файла (фактическое содержимое файла). Когда у нас есть эти данные, мы resolve это как объект. Объект будет содержать имя файла, а также его содержимое. Это важно, когда мы хотим добавить его в наш почтовый индекс. Перед этим у нас есть еще один шаг. Я ОБЕЩАЮ еще одно.

Взгляните на эту часть блока:

}.bind(this, _path));

Эта строка позволяет нам поддерживать лексическое значение this, а также _path, которое в противном случае было бы потеряно в нашей асинхронной функции. Наш _path будет undefined, а наш Promise будет отклонен. После этой строки у нас должен быть массив Promises, который мы можем передать в Promise.all().

Promise.all() получает массив и возвращает единственное разрешенное Promise, когда все Promises в массиве были разрешены. Теперь мы можем, наконец, начать добавлять данные файла в наш экземпляр JSZip. Это инициируется .then() после Promise.all().

.then() позволяет нам выполнять функцию обратного вызова для обработки нашего Promise, независимо от того, разрешено оно или отклонено. Здесь мы можем начать добавлять файлы в zip-архив.

results.forEach((obj)=>{
   zip.file(obj.filename,obj.content);
})

Здесь мы перебираем наш массив и используем .file() JSZip. Эта функция принимает два параметра: имя файла и содержимое файла. Это будет использоваться позже, когда пользователь извлечет zip-файл. obj.filename будет указывать на obj.content (подобно тому, как имена файлов соответствуют реальному файлу в Finder Mac или проводнике Windows). Наконец, мы переходим к try и catch части обратного вызова.

Этот оператор попробуй и поймай будет попробовать оператор (в нашем случае, Promise) и, если эта попытка не удастся, поймает ошибку и обработает ее соответствующим образом.

В нашей попытке мы будем использовать .generateAsync() для асинхронной установки типа файла zip-архива. base64 - это стандартный тип кодирования / декодирования двоичных данных, предназначенных для хранения и передачи через носители. Мы добавляем еще один .then(), в котором мы отправим обратно наш сгенерированный zip-файл в виде ответа JSON. В нашем объекте мы передаем title (то, что мы хотим, чтобы наш почтовый индекс был назван) и content (данные zip).

Вернувшись в действия, получен ответ и скачан zip-файл.

См. ниже.

var file_path = "data:application/zip;base64," + payload.content;
var a = document.createElement("A");
a.href = file_path;
a.download = payload.title;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);

Здесь мы создаем тег привязки с атрибутом download и href, равным file_path.

file_path использует URL данных. В этот URL-адрес мы включаем application/zip, чтобы указать тип MIME, вместе с base64 токеном (чтобы указать, что файл не является текстовым), а затем добавляем фактические данные файла из ответа. Атрибуту download присваивается title (также из ответа), который будет применен к данным файла после их загрузки.

Затем мы добавляем тег привязки в тело нашего документа, выполняем .click() для привязки, имитируя событие щелчка, которое запускает загрузку. После завершения загрузки элемент удаляется из тела нашего документа, чтобы никто другой не использовал ту же загрузку.

Резюме

Вот этапы разработки.

  • Полученный пользовательский ввод для создания данных файла CSV, сохраненных в виде строки.
  • Отправил эту строку в запрос к createTemplate в нашем контроллере.
  • Создан экземпляр JSZip.
  • В экземпляр JSZip добавлена ​​строка CSV.
  • Асинхронно просматривает каталог проекта, сохраняя данные каждого файла в Promise.
  • Разложил все эти Promises в один.
  • Мы перебрали файлы в Promise и добавили их к нашему экземпляру JSZip, используя .file().
  • Асинхронно сгенерированный zip-контент из нашего экземпляра JSZip с использованием .generateAsync().
  • Отправил ответ JSON, содержащий данные заархивированного файла и имя файла.
  • Создан тег привязки с атрибутом download.
  • Данные файла добавлены из ответа на URL данных в href нашего тега привязки.
  • Используется .click(), чтобы инициировать загрузку, а затем удалить тег привязки из DOM, чтобы предотвратить несанкционированное использование.

Если вам понравился материал или вы нуждаетесь в разъяснении чего-либо упомянутого, дайте мне знать в комментариях!

Обновите бесплатную подписку на Medium до платной здесь, и всего за 5 долларов в месяц вы получите неограниченное количество рассказов без рекламы от тысяч авторов из самых разных публикаций. Это партнерская ссылка, и часть вашего членства помогает мне получать вознаграждение за контент, который я создаю. Спасибо!

использованная литература