Использование Express.js, чтобы одна форма сохраняла данные как в базе данных PostgreSQL, так и в мультимедийной корзине AWS Simple Storage Solution (S3).
Форма представляет собой компонент React с отслеживанием состояния и двумя состояниями: одно используется для хранения загруженных файлов (selectedFiles), а другое используется для хранения каждого из полей ввода формы, вводимых пользователем (formState ).
const [selectedFiles, setSelectedFiles] = useState([]); const [formState, setFormState] = useState({ recipientName: '', recipientPhone: '', message: '', dueDate: '' })
С помощью обработчика событий onChange состояние каждого поля с пользовательским ключом будет обновляться по мере того, как пользователь взаимодействует с ними с помощью useState hook. и передача неглубокой копии текущего состояния вместе с новым значением для этого поля.
<input type="text" name="recipientName" value={formState.recipientName} onChange={(event) => setFormState({ ...formState, recipientName: event.target.value })} </input>
Снова используя onChange, при загрузке файла будет вызываться метод handleFileChange каждый раз, когда добавляется новый файл, в котором каждый файл будет динамически добавляться в массив в состоянии.
const handleFileChange = (event) => { setSelectedFiles([...event.target.files]); }; <input type="file" multiple name="file" onChange={handleFileChange} </input>
Созданная с помощью React, форма будет немедленно очищена после отправки путем сброса обоих состояний.
Хотя это единая форма, не все данные будут обрабатываться приложением одним способом. Поля ввода с пользовательскими ключами будут сохраняться в базе данных PostgreSQL, а загруженные файлы будут сохраняться в мультимедийной корзине AWS S3. Это означает, что данные должны будут проходить по разным маршрутам на стороне сервера; мы можем положиться на саму структуру Express.js, чтобы помочь с этим разделением задач.
На уровне формы обработчик событий onSubmit вызывает асинхронный метод (помеченный как submit), который инициирует два HTTP-запроса POST, по одному для каждого состояния.
<form onSubmit={submit}>
Здесь помогает сохранение двух уникальных состояний в форме. С помощью formState у нас есть доступ ко всем значениям с пользовательским ключом из отправки формы. Мы преобразуем этот объект в строку JSON и отправим его в теле запроса POST на одну конечную точку, а загруженные файлы будут отправлены на другую конечную точку.
await fetch('/', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(formState) })
Такое разделение обязанностей полезно для четкого распределения ролей по всей серверной части приложения. Поскольку данные отличаются друг от друга и в конечном итоге будут храниться в двух разных местах, модульность кода с помощью промежуточного программного обеспечения Express.js поможет сохранить функциональность организованной и многократно используемой.
Чтобы обработать другую часть состояния, selectedFiles, которая в настоящее время является массивом файлов, нам нужно преобразовать ее в объект перед отправкой в теле запроса POST, поскольку мы невозможно отправить массив с помощью API выборки. Для этого мы объявим новый экземпляр конструктора FormData и используем его для построения объекта FormData, содержащего каждый из загруженных файлов.
const uploadData = new FormData()
Затем мы пройдемся по массиву selectedFiles и воспользуемся методом FormData append, чтобы добавить каждый файл в объект FormData. Здесь важно использовать append, потому что мы хотим постоянно добавлять каждый файл в качестве значения к одному и тому же ключу (с именем ‘files’) на каждой итерации, а не перезаписывать его.
selectedFiles.forEach((file) => { uploadData.append('files', file); });
Теперь мы можем отправить только что созданный объект FormData, uploadData, в теле запроса POST. Этот объект имеет одно свойство с именем «files», значением которого является массив, содержащий каждый загруженный файл.
uploadData { files: [ File { /* Information about the first selected file */ }, File { /* Information about the second selected file */ }, File { /* Information about the third selected file */ } ] } fetch('/upload', { method: 'POST', body: uploadData, })
Переходя на сторону сервера, данные с пользовательским ключом, которые в конечном итоге попадут в базу данных PostgreSQL, будут проходить через цепочку методов промежуточного программного обеспечения, специфичных для конечной точки «/».
const router = express.Router(); router.post('/', controller.ensureAuthenticated, controller.saveToDatabase)
Первый из этих методов проверяет, прошел ли пользователь аутентификацию.
ensureAuthenticated: (req, res, next) => { if (req.isAuthenticated()) { return next(); } res.status(401).json({ error: "User not authenticated" }); }
Этот метод вернет next(), что означает, что поток выполнения вернется из этого метода и вернется к цепочке промежуточного программного обеспечения после завершения. Если в цепочке есть другой метод, поток выполнения впоследствии войдет в этот метод.
Это называется шаблон проектирования промежуточного программного обеспечения, и он значительно помогает при отладке и улучшении опыта разработчиков, поскольку мы можем шаг за шагом отслеживать поток выполнения через каждую часть промежуточного программного обеспечения; мы получим полезное сообщение об ошибке, если часть цепочки промежуточного программного обеспечения не переместится на next().
После аутентификации следующая часть промежуточного программного обеспечения вставит поля с пользовательским ключом в базу данных PostgreSQL и вернет next() в случае успеха. Цепочка промежуточного программного обеспечения для конечной точки «/» завершится на этом этапе, когда данные с пользовательским ключом будут сохранены в PostgreSQL. Клиенту отправляется статус ответа 200, что разрешает запрос.
Второй запрос POST — для отправки загруженных клиентом файлов на сервер и, в конечном итоге, в корзину S3 — имеет больше движущихся частей. Помните, что в исходной форме React на стороне клиента мы поддерживаем два уникальных состояния, когда пользователь взаимодействует с формой. Состояние, поддерживающее массив загруженных файлов (selectedFiles), теперь можно использовать для отправки только загруженных файлов в другую конечную точку, которая будет конечной точкой '/upload'. .
const router = express.Router(); router.post('/upload', upload.array('files')
Эта конечная точка — обработчик маршрута для «/upload» — имеет только одно промежуточное ПО: upload.array(‘files’). Если присмотреться повнимательнее, то 'upload' — это переменная, объявленная в самом верху файла, которая на самом деле является оценочным результатом вызова multer и передачи объекта { место назначения: 'uploads/' }.
const multer = require('multer') const upload = multer({ dest: 'uploads/' })
Multer — это полезная библиотека Node.js, которую можно установить через NPM. Это промежуточное программное обеспечение, которое обычно используется для приема загружаемых файлов на стороне сервера, когда они отправляются через http POST, поэтому оно идеально подходит для использования в этой ситуации. В const upload = multer({ dest: 'uploads/' }) мы вызываем промежуточное ПО multer и передаем объект конфигурации, который указывает multer на временное расположение, где должны находиться загруженные файлы. хранится на сервере. Затем мы можем получить доступ к файловому серверу на стороне объекта запроса через req.files.
router.post('/upload', upload.array('files'), (req, res) => { const files = req.files;
Метод .array() используется, чтобы сообщить multer, что файлы будут передаваться в виде массива в объекте запроса, а имя свойства будет «files». Теперь у нас есть доступ к загруженным файлам на стороне сервера приложения, каждый из которых является объектом внутри массива. Это важно помнить, так как нам потребуется доступ к свойствам каждого всплывающего файлового объекта.
Теперь мы всего в нескольких шагах от того, чтобы загруженные файлы сохранялись в корзине S3. В обработчике маршрута для '/upload' было вызвано промежуточное программное обеспечение multer, поток выполнения теперь находится в блоке кода самого обработчика маршрута, и у нас есть доступ к массиву файлов через переменная с пометкой файлы.
Мы будем использовать два разных инструмента, чтобы спрятать файлы в корзину S3: встроенный в Node модульФайловая система для создания удобного для чтения потока файлов,а также Amazon SDK(комплект для разработки программного обеспечения) для создания экземпляра S3 и загрузки каждого из потоков. Наши учетные данные AWS надежно хранятся в файле .env, который нам понадобится здесь.
const fs = require('fs'); const AWS = require('aws-sdk'); const accessKeyId = process.env.AWS_ACCESS_KEY_ID const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY const region = process.env.AWS_REGION const s3 = new AWS.S3({ region, credentials: { accessKeyId, secretAccessKey } });
Поскольку мы понятия не имеем, насколько велики могут быть загруженные файлы или даже сколько файлов в целом, использование потоков, вероятно, будет наиболее эффективным с точки зрения памяти и времени методом отправки файлов с сервера приложения в корзину S3. Они позволяют отправлять файлы удобоваримыми порциями в AWS и будут делать это асинхронно.
Перебор файлов и открытие потока для каждого из них позволяет выполнять параллельную обработку и позволяет избежать узких мест.
Помните, ранее мы сохраняли каждый файл во временной файловой системе сервера, используя dest: ‘uploads/’ .
files.forEach((file) => { const fileStream = fs.createReadStream(file.path)
Теперь file.path предоставляет это расположение для createReadStream, чтобы создать доступный для чтения поток для файлов, хранящихся в этом расположении. Этот forEach будет перебирать каждый файл, независимо от их количества, и открывать поток для каждого. Локальная переменная fileStream — это оцениваемый результат вызова createReadStream во время каждой итерации, и она временно сохраняет этот поток для использования в блоке кода.
Похоже на HTTP-запрос POST со стороны клиента, теперь мы создадим объект params для попытки загрузки в корзину S3. Самое главное, что переменная fileStream, представляющая читаемый поток для текущего файла, включена в свойство body вместе с типом и именем файла и будет динамически обновляться. на протяжении каждой итерации.
const params = { Bucket: 'S3cache', Key: file.originalname, Body: fileStream, ContentType: 'image/jpeg', };
Наконец, загрузка в корзину S3 будет происходить с использованием вновь объявленного объекта params.
s3.upload(params, (err, data) => { if (err) { return res.status(500).json({ error: 'Failed to upload file to S3' }); } console.log('File uploaded to S3:', data.Location); }); }); res.json({ message: 'Files uploaded successfully' }); });
Доступные для чтения потоки будут закрыты, как и загрузка S3, после обработки всех данных или устранения ошибок. Поскольку forEach перебирает общую длину файлов, каждый из них обрабатывается таким образом асинхронно. В конце концов ответobjectres.json({ сообщение: 'Файлы успешно загружены' }) возвращается на запрос POST в конечной точке '/upload', и оба HTTP-запроса POST были разрешены.