使用 MERN 堆栈构建具有图像预览的文件上传/下载功能
介绍
在本文中,我们将使用 MERN 堆栈创建具有图像预览功能的文件上传和下载功能。
通过创建此应用程序,您将学习
- 如何使用拖放功能上传文件
- 如何上传和下载任何类型的文件
- 如何在上传时限制文件类型
- 如何在上传时限制文件的大小
- 如何在选择图像后显示图像预览
- 如何使用MongoDB存储和获取文件的详细信息
等等。
我们不会将文件以 base64 编码的形式存储在 MongoDB 数据库中,而是将文件存储在服务器上,并且只将文件的路径存储在数据库内,以保持数据库大小较小,并根据需要轻松访问和移动文件。
我们正在使用非常流行的react-dropzone npm 库来实现拖放功能。
对于实际的文件上传,我们使用multer npm 库,该库对于文件上传也非常流行。
我们将使用 React Hooks 来构建此应用程序,因此如果您不熟悉它,请查看我之前的文章以了解 Hooks 的介绍。
我们将使用MongoDB
数据库,因此请确保按照我之前的文章在本地安装它
初始设置
使用以下方式创建新项目create-react-app
create-react-app react-upload-download-files
项目创建完成后,删除src
文件夹中的所有文件,并在文件夹中创建index.js
和styles.scss
文件src
。同时,在文件夹中创建components
、router
和utils
文件夹src
。
安装必要的依赖项:
yarn add axios@0.20.0 bootstrap@4.5.2 downloadjs@1.4.7 node-sass@4.14.1 react-bootstrap@1.3.0 react-dropzone@11.2.0 react-router-dom@5.2.0
打开并从这里styles.scss
添加内容。
创建初始页面
Header.js
在文件夹内创建一个名为的新文件,components
其内容如下:
import React from 'react';
import { NavLink } from 'react-router-dom';
const Header = () => {
return (
<div className="header">
<h1>File Upload And Download</h1>
<nav>
<NavLink activeClassName="active" to="/" exact={true}>
Home
</NavLink>
<NavLink activeClassName="active" to="/list">
Files List
</NavLink>
</nav>
</div>
);
};
export default Header;
App.js
在文件夹内创建一个名为的新文件,components
其内容如下:
import React, { useState, useRef } from 'react';
import { Form, Row, Col, Button } from 'react-bootstrap';
const App = (props) => {
const [file, setFile] = useState(null); // state for storing actual image
const [previewSrc, setPreviewSrc] = useState(''); // state for storing previewImage
const [state, setState] = useState({
title: '',
description: ''
});
const [errorMsg, setErrorMsg] = useState('');
const [isPreviewAvailable, setIsPreviewAvailable] = useState(false); // state to show preview only for images
const dropRef = useRef(); // React ref for managing the hover state of droppable area
const handleInputChange = (event) => {
setState({
...state,
[event.target.name]: event.target.value
});
};
const handleOnSubmit = async (event) => {
event.preventDefault();
};
return (
<React.Fragment>
<Form className="search-form" onSubmit={handleOnSubmit}>
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<Row>
<Col>
<Form.Group controlId="title">
<Form.Control
type="text"
name="title"
value={state.title || ''}
placeholder="Enter title"
onChange={handleInputChange}
/>
</Form.Group>
</Col>
</Row>
<Row>
<Col>
<Form.Group controlId="description">
<Form.Control
type="text"
name="description"
value={state.description || ''}
placeholder="Enter description"
onChange={handleInputChange}
/>
</Form.Group>
</Col>
</Row>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
</React.Fragment>
);
};
export default App;
在此文件中,我们暂时渲染一个表单来添加title
和description
。我们将在本文后面添加添加文件的选项。
对于每个输入字段,我们添加了一个handleInputChange
处理程序来更新每个输入字段的状态。我们name
为每个输入字段添加了一个与状态变量名称完全匹配的属性,以便我们能够使用 ES6 简写语法来更新状态。
const handleInputChange = (event) => {
setState({
...state,
[event.target.name]: event.target.value
});
};
对于 Hooks 来说,状态不会自动合并,因此我们首先传播状态的所有属性,然后更新相应的输入字段。
AppRouter.js
在文件夹内创建一个名为的新文件,router
其内容如下:
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import App from '../components/App';
import Header from '../components/Header';
const AppRouter = () => (
<BrowserRouter>
<div className="container">
<Header />
<div className="main-content">
<Switch>
<Route component={App} path="/" exact={true} />
</Switch>
</div>
</div>
</BrowserRouter>
);
export default AppRouter;
现在,打开src/index.js
文件并在其中添加以下内容:
import React from 'react';
import ReactDOM from 'react-dom';
import AppRouter from './router/AppRouter';
import 'bootstrap/dist/css/bootstrap.min.css';
import './styles.scss';
ReactDOM.render(<AppRouter />, document.getElementById('root'));
yarn start
现在,通过从终端执行命令来启动应用程序。
您将看到以下屏幕:
添加文件上传功能
现在,让我们添加从 UI 上传文件的选项。
打开src/App.js
文件,在提交按钮之前和结束Row
标签之后,添加以下代码
<div className="upload-section">
<Dropzone onDrop={onDrop}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps({ className: 'drop-zone' })} ref={dropRef}>
<input {...getInputProps()} />
<p>Drag and drop a file OR click here to select a file</p>
{file && (
<div>
<strong>Selected file:</strong> {file.name}
</div>
)}
</div>
)}
</Dropzone>
{previewSrc ? (
isPreviewAvailable ? (
<div className="image-preview">
<img className="preview-image" src={previewSrc} alt="Preview" />
</div>
) : (
<div className="preview-message">
<p>No preview available for this file</p>
</div>
)
) : (
<div className="preview-message">
<p>Image preview will be shown here after selection</p>
</div>
)}
</div>
在这里,我们使用DropZone
带有 React 渲染道具模式的组件,其中我们需要在拖放区域中显示的文本被添加在DropZone
组件内的输入字段之后。
在文件顶部DropZone
添加导入。axios
App.js
import Dropzone from 'react-dropzone';
import axios from 'axios';
OnDrop
在处理程序后添加该函数handleInputChange
。
const onDrop = (files) => {
const [uploadedFile] = files;
setFile(uploadedFile);
const fileReader = new FileReader();
fileReader.onload = () => {
setPreviewSrc(fileReader.result);
};
fileReader.readAsDataURL(uploadedFile);
setIsPreviewAvailable(uploadedFile.name.match(/\.(jpeg|jpg|png)$/));
};
这里,该onDrop
函数接收一个包含已拖放或选定文件的文件数组。
我们一次只上传一个文件,因此上传的文件将是可用的文件[0],因此我们使用数组解构语法来获取该文件的值。
const [uploadedFile] = files;
为了显示图像的预览,我们使用 JavaScript FileReader
API。
为了将文件转换为,dataURL
我们调用该fileReader.readAsDataURL
方法。
一旦文件成功读取为,就会调用dataURL
的 onload 函数。fileReader
fileReader.onload = () => {
setPreviewSrc(fileReader.result);
};
读取操作的结果将在我们分配给状态变量result
的属性中可用。fileReader
previewSrc
我们仅显示图像的预览,因此我们正在检查上传的文件是否具有正确的格式(仅 jpg、jpeg 和 png 图像)并更新previewAvailable
变量的状态。
setIsPreviewAvailable(uploadedFile.name.match(/\.(jpeg|jpg|png)$/));
现在,通过运行命令重新启动应用程序yarn start
并验证功能。
这里,我们通过浏览的方式添加了一个文件。您也可以像下面这样通过拖放的方式添加文件。
如果您选择的文件不是图像,我们将不会显示消息所指示的预览No preview available for this file
。
添加掉落指示
如果您看到了拖放功能,我们没有显示任何表明文件被拖放到拖放区域的迹象,所以让我们添加它。
我们已经在文件内的ref
div 中添加了 class 。drop-zone
App.js
<div {...getRootProps({ className: 'drop-zone' })} ref={dropRef}>
并且还使用钩子dropRef
在顶部创建了变量。useRef
将onDragEnter
和onDragLeave
道具添加到Dropzone
组件。
<Dropzone
onDrop={onDrop}
onDragEnter={() => updateBorder('over')}
onDragLeave={() => updateBorder('leave')}
>
onDragEnter
当文件超出放置区域时将触发该函数,onDragLeave
当文件从放置区域移除时也将触发该函数。
在处理程序之前在组件内部创建一个新updateBorder
函数。App
handleOnSubmit
const updateBorder = (dragState) => {
if (dragState === 'over') {
dropRef.current.style.border = '2px solid #000';
} else if (dragState === 'leave') {
dropRef.current.style.border = '2px dashed #e9ebeb';
}
};
由于我们已将dropRef
ref 添加到具有类的 div drop-zone
,它将指向该 div,我们可以使用它的current
属性来更新放置区域的边框dropRef.current.style.border
。
另外,在onDrop
函数内部,在函数末尾添加以下行。
dropRef.current.style.border = '2px dashed #e9ebeb';
因此,当我们将文件拖放到拖放区域上时,边框将恢复到正常状态。
现在,如果您检查应用程序,您将看到边框变化的拖放效果。
调用API进行文件上传
constants.js
在文件夹中创建一个新文件,文件名称如下src/utils
,内容如下
export const API_URL = 'http://localhost:3030';
我们将很快在端口上启动我们的 Express 服务器,3030
因此我们在这里提到了这一点。
handleOnSubmit
现在,让我们在处理程序中编写代码App.js
来调用后端 API。
handleOnSubmit
使用以下代码替换处理程序
const handleOnSubmit = async (event) => {
event.preventDefault();
try {
const { title, description } = state;
if (title.trim() !== '' && description.trim() !== '') {
if (file) {
const formData = new FormData();
formData.append('file', file);
formData.append('title', title);
formData.append('description', description);
setErrorMsg('');
await axios.post(`${API_URL}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
} else {
setErrorMsg('Please select a file to add.');
}
} else {
setErrorMsg('Please enter all the field values.');
}
} catch (error) {
error.response && setErrorMsg(error.response.data);
}
};
API_URL
另外,在文件顶部导入。
import { API_URL } from '../utils/constants';
在处理程序内部handleOnSubmit
,我们首先检查用户是否输入了所有字段值并选择了文件,并且我们正在对/upload
API 进行 API 调用,我们将在下一部分中编写该 API。
await axios.post(`${API_URL}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
我们正在向 API发出对象POST
请求formData
并发送title
和实际文件。description
请注意,提及 multipart/form-data 的内容类型非常重要,否则文件将不会发送到服务器。
添加文件上传的服务器端代码
现在,让我们添加服务器端功能来上传文件。
server
在文件夹内创建一个具有名称的文件夹,并从文件夹react-upload-download-files
执行以下命令server
yarn init -y
这将在文件夹内创建一个package.json
文件server
。
server
通过从文件夹内的终端执行以下命令来安装所需的依赖项
yarn add cors@2.8.5 express@4.17.1 mongoose@5.10.7 multer@1.4.2 nodemon@2.0.4
.gitignore
在文件夹内创建一个同名的新文件server
,并在其中添加以下行,这样node_modules
文件夹就不会添加到您的 Git 存储库中。
node_modules
现在在文件夹内创建db
、files
、model
、文件夹。同样,在文件夹内创建。routes
server
index.js
server
在文件夹中server/db
,创建一个db.js
包含以下内容的新文件
const mongoose = require('mongoose');
mongoose.connect('mongodb://127.0.0.1:27017/file_upload', {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true
});
在此提供您的MongoDB
数据库连接详细信息。file_upload
是我们将使用的数据库的名称。
file.js
在文件夹中创建一个新文件,文件名称如下model
,内容如下
const mongoose = require('mongoose');
const fileSchema = mongoose.Schema(
{
title: {
type: String,
required: true,
trim: true
},
description: {
type: String,
required: true,
trim: true
},
file_path: {
type: String,
required: true
},
file_mimetype: {
type: String,
required: true
}
},
{
timestamps: true
}
);
const File = mongoose.model('File', fileSchema);
module.exports = File;
这里,我们定义了集合的模式,因为我们使用非常流行的Mongoose库来操作 MongoDB。我们将在集合中存储title
、和description
,因此我们在此文件中描述了每个类型的类型。file_path
file_mimetype
请注意,即使我们将模型名称定义为File
,MongoDB 也会创建集合的复数版本。因此集合名称将是files
。
file.js
现在,在文件夹中创建一个新文件,routes
其内容如下
const path = require('path');
const express = require('express');
const multer = require('multer');
const File = require('../model/file');
const Router = express.Router();
const upload = multer({
storage: multer.diskStorage({
destination(req, file, cb) {
cb(null, './files');
},
filename(req, file, cb) {
cb(null, `${new Date().getTime()}_${file.originalname}`);
}
}),
limits: {
fileSize: 1000000 // max file size 1MB = 1000000 bytes
},
fileFilter(req, file, cb) {
if (!file.originalname.match(/\.(jpeg|jpg|png|pdf|doc|docx|xlsx|xls)$/)) {
return cb(
new Error(
'only upload files with jpg, jpeg, png, pdf, doc, docx, xslx, xls format.'
)
);
}
cb(undefined, true); // continue with upload
}
});
Router.post(
'/upload',
upload.single('file'),
async (req, res) => {
try {
const { title, description } = req.body;
const { path, mimetype } = req.file;
const file = new File({
title,
description,
file_path: path,
file_mimetype: mimetype
});
await file.save();
res.send('file uploaded successfully.');
} catch (error) {
res.status(400).send('Error while uploading file. Try again later.');
}
},
(error, req, res, next) => {
if (error) {
res.status(500).send(error.message);
}
}
);
Router.get('/getAllFiles', async (req, res) => {
try {
const files = await File.find({});
const sortedByCreationDate = files.sort(
(a, b) => b.createdAt - a.createdAt
);
res.send(sortedByCreationDate);
} catch (error) {
res.status(400).send('Error while getting list of files. Try again later.');
}
});
Router.get('/download/:id', async (req, res) => {
try {
const file = await File.findById(req.params.id);
res.set({
'Content-Type': file.file_mimetype
});
res.sendFile(path.join(__dirname, '..', file.file_path));
} catch (error) {
res.status(400).send('Error while downloading file. Try again later.');
}
});
module.exports = Router;
在这个文件中,我们使用multer
库来处理文件上传。我们创建了一个multer
配置,并将其存储在名为 的变量中upload
。
const upload = multer({
storage: multer.diskStorage({
destination(req, file, cb) {
cb(null, './files');
},
filename(req, file, cb) {
cb(null, `${new Date().getTime()}_${file.originalname}`);
}
}),
limits: {
fileSize: 1000000 // max file size 1MB = 1000000 bytes
},
fileFilter(req, file, cb) {
if (!file.originalname.match(/\.(jpeg|jpg|png|pdf|doc|docx|xlsx|xls)$/)) {
return cb(
new Error(
'only upload files with jpg, jpeg, png, pdf, doc, docx, xslx, xls format.'
)
);
}
cb(undefined, true); // continue with upload
}
});
该multer
函数以一个对象作为参数,该对象具有许多属性,其中一些是storage
和limits
和fileFilter
函数。
该函数接受一个具有和函数的multer.diskStorage
对象。destination
filename
这里我们使用 ES6 函数简写语法,因此
destination(req, file, cb) {
和...相同
destination: function(req, file, cb) {
destination
and函数filename
接收三个输入参数,即req(request)
、file(actual uploaded file object)
和cb(callback function)
。
对于回调函数(cb)参数,
- 如果出现错误,它将作为第一个参数传递
- 如果没有错误,则第一个参数将为空或未定义,第二个参数将包含传递给回调函数的数据。
在函数中destination
,我们传递了存储上传文件的文件夹路径。在本例中,它将是文件夹files
内的文件夹server
。
在filename
函数中,我们为每个上传的文件指定一个名称。在本例中,它将是current_timestamp_name_of_the_file
。
我们在该limits
属性中指定了允许上传文件的最大大小。在本例中,我们设置的最大文件大小为 1MB。
然后在fileFilter
函数内部,我们可以决定接受要上传的文件或拒绝它。
如果文件扩展名与任一扩展名匹配,jpeg|jpg|png|pdf|doc|docx|xlsx|xls
则我们通过调用回调函数允许文件上传,cb(undefined, true)
否则我们将引发错误。
如果我们在函数cb(undefined, false)
内部调用fileFilter
,那么文件将始终被拒绝并且不会被上传。
现在,让我们看看/upload
路线
Router.post(
'/upload',
upload.single('file'),
async (req, res) => {
try {
const { title, description } = req.body;
const { path, mimetype } = req.file;
const file = new File({
title,
description,
file_path: path,
file_mimetype: mimetype
});
await file.save();
res.send('file uploaded successfully.');
} catch (error) {
res.status(400).send('Error while uploading file. Try again later.');
}
},
(error, req, res, next) => {
if (error) {
res.status(500).send(error.message);
}
}
);
在这里,我们将upload.single
函数作为第二个参数传递给/upload
路由,因此它将充当中间件,并将在执行函数主体之前首先执行。
请注意,该file
参数upload.single
必须与前端上传文件时使用的名称相匹配。
记住我们之前用于从文件进行 API 调用的代码App.js
。
const formData = new FormData();
formData.append('file', file);
我们将文件添加到formData
名为 的属性中file
。该名称必须与upload.single
参数名称匹配,否则文件上传将无法进行。
在函数内部,我们将获得对象title
内部description
的req.body
和实际文件req.file
,因为我们使用了该multer
库。
File
然后我们将这些值传递给我们创建的模型的对象。
const file = new File({
title,
description,
file_path: path,
file_mimetype: mimetype
});
并且调用save
对象上的方法实际上会将数据保存在 MongoDB 数据库中。
如果文件类型不匹配jpeg|jpg|png|pdf|doc|docx|xlsx|xls
或文件大小大于我们提到的(1MB),则将执行以下代码
(error, req, res, next) => {
if (error) {
res.status(500).send(error.message);
}
};
并将错误消息发送回客户端(我们的 React 应用程序)。
现在,打开server/index.js
文件并在其中添加以下内容。
const express = require('express');
const cors = require('cors');
const fileRoute = require('./routes/file');
require('./db/db');
const app = express();
app.use(cors());
app.use(fileRoute);
app.listen(3030, () => {
console.log('server started on port 3030');
});
在此文件中,我们使用Express
服务器在端口上启动我们的 Node.js 应用程序3030
。
我们还使用npm 包作为中间件,因此当我们从在端口上运行的 React 应用程序向在端口上运行的 Node.js 应用程序进行 API 调用时cors
不会出现错误。CORS
3000
3030
现在,让我们运行应用程序来检查上传功能。
打开文件并在属性中server/package.json
添加脚本。start
scripts
"scripts": {
"start": "nodemon index.js"
}
server
现在,打开另一个终端,保持 React 终端运行,并从文件夹内执行以下命令
yarn start
这将启动我们的 Node.js 快速服务器,以便我们可以对其进行 API 调用。
还可以通过从终端运行以下命令来启动 MongoDB 数据库服务器(如果您已经关注过前面提到的这篇文章)
./mongod --dbpath=<path_to_mongodb-data_folder>
现在您将打开三个终端:一个用于 React 应用程序,一个用于 Node.js 服务器,另一个用于 MongoDB 服务器。
现在让我们验证上传功能。
如您所见,当我们上传文件时,它会被添加到 files 文件夹中,并且条目也会被添加到 MongoDB 数据库中。因此文件上传成功。
但是用户界面上没有显示任何文件上传成功的提示。现在就来解决这个问题。
FilesList.js
在文件夹中创建一个新文件components
,内容如下
import React, { useState, useEffect } from 'react';
import download from 'downloadjs';
import axios from 'axios';
import { API_URL } from '../utils/constants';
const FilesList = () => {
const [filesList, setFilesList] = useState([]);
const [errorMsg, setErrorMsg] = useState('');
useEffect(() => {
const getFilesList = async () => {
try {
const { data } = await axios.get(`${API_URL}/getAllFiles`);
setErrorMsg('');
setFilesList(data);
} catch (error) {
error.response && setErrorMsg(error.response.data);
}
};
getFilesList();
}, []);
const downloadFile = async (id, path, mimetype) => {
try {
const result = await axios.get(`${API_URL}/download/${id}`, {
responseType: 'blob'
});
const split = path.split('/');
const filename = split[split.length - 1];
setErrorMsg('');
return download(result.data, filename, mimetype);
} catch (error) {
if (error.response && error.response.status === 400) {
setErrorMsg('Error while downloading file. Try again later');
}
}
};
return (
<div className="files-container">
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<table className="files-table">
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Download File</th>
</tr>
</thead>
<tbody>
{filesList.length > 0 ? (
filesList.map(
({ _id, title, description, file_path, file_mimetype }) => (
<tr key={_id}>
<td className="file-title">{title}</td>
<td className="file-description">{description}</td>
<td>
<a
href="#/"
onClick={() =>
downloadFile(_id, file_path, file_mimetype)
}
>
Download
</a>
</td>
</tr>
)
)
) : (
<tr>
<td colSpan={3} style={{ fontWeight: '300' }}>
No files found. Please add some.
</td>
</tr>
)}
</tbody>
</table>
</div>
);
};
export default FilesList;
在这个文件中,最初在useEffect
钩子内部,我们对/getAllFiles
API 进行 API 调用。
API/getAllFiles
如下routes/file.js
所示:
Router.get('/getAllFiles', async (req, res) => {
try {
const files = await File.find({});
const sortedByCreationDate = files.sort(
(a, b) => b.createdAt - a.createdAt
);
res.send(sortedByCreationDate);
} catch (error) {
res.status(400).send('Error while getting list of files. Try again later.');
}
});
在这里,我们调用模型上的库.find
的方法来获取数据库中添加的所有文件的列表,然后按日期降序对它们进行排序,这样我们就可以在列表中首先获得最近添加的文件。mongoose
File
createdAt
然后我们将 API 的结果赋值给filesList
状态中的数组
const { data } = await axios.get(`${API_URL}/getAllFiles`);
setErrorMsg('');
setFilesList(data);
然后我们使用 Array map 方法循环遍历数组并以表格格式将它们显示在 UI 上。
我们还在表格中添加了一个下载链接。downloadFile
点击download
链接时,会调用该函数
const downloadFile = async (id, path, mimetype) => {
try {
const result = await axios.get(`${API_URL}/download/${id}`, {
responseType: 'blob'
});
const split = path.split('/');
const filename = split[split.length - 1];
setErrorMsg('');
return download(result.data, filename, mimetype);
} catch (error) {
if (error.response && error.response.status === 400) {
setErrorMsg('Error while downloading file. Try again later');
}
}
};
在函数内部downloadFile
,我们调用/download/:id
API。请注意,我们将 设置responseType
为blob
。这一点非常重要,否则您将无法获取正确格式的文件。
/download
文件中的 API如下routes/file.js
所示:
Router.get('/download/:id', async (req, res) => {
try {
const file = await File.findById(req.params.id);
res.set({
'Content-Type': file.file_mimetype
});
res.sendFile(path.join(__dirname, '..', file.file_path));
} catch (error) {
res.status(400).send('Error while downloading file. Try again later.');
}
});
这里,首先,我们用提供的 来检查是否存在这样的文件。如果存在,我们先设置文件的 ,id
然后返回存储在文件夹中的文件。files
content-type
设置content-type
对于获取正确的文件格式至关重要,因为我们不仅上传图片,还上传 doc、xls 和 pdf 文件。因此,为了正确返回文件内容,content-type
是必需的。
/download
一旦我们从函数内部的 API获得响应downloadFile
,我们就会调用downloadjs npm 库download
提供的函数。
downloadjs
是一个非常流行的库,可以下载任何类型的文件。您只需提供文件内容、内容类型以及您希望文件在下载时具有的文件名,它就会触发浏览器的下载功能。
现在,打开router/AppRouter.js
文件并为组件添加路由FilesList
。
您的AppRouter.js
文件现在看起来如下:
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import App from '../components/App';
import Header from '../components/Header';
import FilesList from '../components/FilesList';
const AppRouter = () => (
<BrowserRouter>
<div className="container">
<Header />
<div className="main-content">
<Switch>
<Route component={App} path="/" exact={true} />
<Route component={FilesList} path="/list" />
</Switch>
</div>
</div>
</BrowserRouter>
);
export default AppRouter;
现在,打开src/App.js
并handleOnSubmit
在调用/upload
API 后在处理程序内部添加一条语句以将用户重定向到FilesList
组件
await axios.post(`${API_URL}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
props.history.push('/list'); // add this line
所以现在,一旦文件上传完成,我们将被重定向到FilesList
组件,在那里我们将看到上传的文件列表。
如果上传文件时出现错误,您将在 UI 上看到错误消息,并且不会被重定向到列表页面。
假设您已经yarn start
在两个单独的终端中执行了用于启动 React 和 Node.js 应用程序的命令,并在另一个终端中执行了用于运行 MongoDB 服务器的命令。现在,让我们验证应用程序的功能。
上传图片文件演示
上传 PDF 文件演示
上传 Excel 文件演示
上传 Doc 文件演示
上传不受支持的文件演示
如您所见,我们能够成功上传和下载我们支持的格式列表中的任何类型的文件。
消除对 CORS 的需求
如前所述,为了在从 React App 向 Node.js App 调用 API 时停止出现 CORS 错误,我们cors
在服务器端使用如下库:
app.use(cors());
尝试从文件中删除此行,您将看到从 React 到 Node.js 的 API 调用失败。
为了避免此错误,我们使用了 cors 中间件。但正因如此,世界上任何人都可以直接从他们的应用访问我们的 API,这出于安全考虑并不理想。
因此,为了消除对 cors 的需求,我们将在同一个端口上运行 Node.js 和 React 应用程序,这也将消除运行两个单独命令的需要。
cors
因此,首先,从文件中删除的使用server/index.js
,并删除require
的语句cors
。
然后在语句前添加如下代码app.use(fileRoute)
。
app.use(express.static(path.join(__dirname, '..', 'build')));
在这里,我们告诉 express 静态提供构建文件夹的内容。
yarn build
当我们为 React App运行命令时,将创建构建文件夹。
要详细了解其实际工作原理,请查看我之前的文章
并path
在文件顶部导入 Node.js 包。
const path = require('path');
您的server/index.js
文件现在看起来如下:
const express = require('express');
const path = require('path');
const fileRoute = require('./routes/file');
require('./db/db');
const app = express();
app.use(express.static(path.join(__dirname, '..', 'build')));
app.use(fileRoute);
app.listen(3030, () => {
console.log('server started on port 3030');
});
现在,打开主package.json
文件并在部分中添加start-app
脚本scripts
。
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"start-app": "yarn build && (cd server && yarn start)"
},
现在,假设您已经启动了 MongoDB 服务器,您只需要从yarn run start-app
终端运行命令。
此命令将创建一个build
包含我们所有 React 应用程序的文件夹,然后在端口上启动我们的 Node.js 服务器3030
。
现在,我们可以在同一个3030
端口访问 React 和 Node.js 应用程序了。因此,无需运行两个单独的命令,您可以通过http://localhost:3030/访问该应用程序。
但是有一个问题,如果你刷新/list
页面,就会出现 404 错误。这是因为我们使用 Express 服务器启动应用,所以当我们访问/list
路由时,它会去服务器检查该路由。
但是服务器不包含这样的路由,但我们的 React App 有该路由,所以要解决这个问题,我们需要添加一些代码。
打开server/index.js
文件并在app.listen
调用之前添加以下代码。
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '..', 'build', 'index.html'));
});
build/index.html
当我们遇到服务器端不存在的任何路由时,上述代码会将文件发送回我们的 React 应用程序。
因此,由于/list
服务器端不存在该路由,因此当我们将用户重定向到index.html
文件时,React 应用程序将处理该路由。
因此,请确保在所有服务器端路由之后添加上述代码行,因为*
inapp.get
将匹配任何路由。
您的最终server/index.js
文件现在看起来如下:
const express = require('express');
const path = require('path');
const fileRoute = require('./routes/file');
require('./db/db');
const app = express();
app.use(express.static(path.join(__dirname, '..', 'build')));
app.use(fileRoute);
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '..', 'build', 'index.html'));
});
app.listen(3030, () => {
console.log('server started on port 3030');
});
现在,通过运行yarn run start-app
命令重新启动您的应用程序,刷新/list
路由将不会给您 404 错误。
结论
我们现在已经完成了使用 MERN 堆栈创建完整的文件上传和下载功能。
您可以在此存储库中找到此应用程序的完整源代码。
不要忘记订阅我的每周新闻通讯,其中包含精彩的提示、技巧和文章,直接发送到您的收件箱中。
文章来源:https://dev.to/myogeshchavan97/drag-and-drop-file-upload-with-image-preview-and-download-file-functionity-using-mern-stack-5dgn