使用 MERN 堆栈构建具有图像预览的文件上传/下载功能

2025-05-27

使用 MERN 堆栈构建具有图像预览的文件上传/下载功能

介绍

在本文中,我们将使用 MERN 堆栈创建具有图像预览功能的文件上传和下载功能。

通过创建此应用程序,您将学习

  • 如何使用拖放功能上传文件
  • 如何上传和下载任何类型的文件
  • 如何在上传时限制文件类型
  • 如何在上传时限制文件的大小
  • 如何在选择图像后显示图像预览
  • 如何使用MongoDB存储和获取文件的详细信息

等等。

我们不会将文件以 base64 编码的形式存储在 MongoDB 数据库中,而是将文件存储在服务器上,并且只将文件的路径存储在数据库内,以保持数据库大小较小,并根据需要轻松访问和移动文件。

我们正在使用非常流行的react-dropzone npm 库来实现拖放功能。

对于实际的文件上传,我们使用multer npm 库,该库对于文件上传也非常流行。

我们将使用 React Hooks 来构建此应用程序,因此如果您不熟悉它,请查看我之前的文章了解 Hooks 的介绍。

我们将使用MongoDB数据库,因此请确保按照我之前的文章在本地安装

初始设置

使用以下方式创建新项目create-react-app



create-react-app react-upload-download-files


Enter fullscreen mode Exit fullscreen mode

项目创建完成后,删除src文件夹中的所有文件,并在文件夹中创建index.jsstyles.scss文件src。同时,在文件夹中创建componentsrouterutils文件夹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


Enter fullscreen mode Exit fullscreen mode

打开并从这里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;


Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

在此文件中,我们暂时渲染一个表单来添加titledescription。我们将在本文后面添加添加文件的选项。

对于每个输入字段,我们添加了一个handleInputChange处理程序来更新每个输入字段的状态。我们name为每个输入字段添加了一个与状态变量名称完全匹配的属性,以便我们能够使用 ES6 简写语法来更新状态。



const handleInputChange = (event) => {
  setState({
    ...state,
    [event.target.name]: event.target.value
  });
};


Enter fullscreen mode Exit fullscreen mode

对于 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;


Enter fullscreen mode Exit fullscreen mode

现在,打开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'));


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

在这里,我们使用DropZone带有 React 渲染道具模式的组件,其中我们需要在拖放区域中显示的文本被添加在DropZone组件内的输入字段之后。

在文件顶部DropZone添加导入axiosApp.js



import Dropzone from 'react-dropzone';
import axios from 'axios';


Enter fullscreen mode Exit fullscreen mode

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)$/));
};


Enter fullscreen mode Exit fullscreen mode

这里,该onDrop函数接收一个包含已拖放或选定文件的文件数组。

我们一次只上传一个文件,因此上传的文件将是可用的文件[0],因此我们使用数组解构语法来获取该文件的值。



const [uploadedFile] = files;


Enter fullscreen mode Exit fullscreen mode

为了显示图像的预览,我们使用 JavaScript FileReaderAPI。

为了将文件转换为,dataURL我们调用该fileReader.readAsDataURL方法。

一旦文件成功读取为,就会调用dataURL的 onload 函数。fileReader



fileReader.onload = () => {
  setPreviewSrc(fileReader.result);
};


Enter fullscreen mode Exit fullscreen mode

读取操作的结果将在我们分配给状态变量result的属性中可用fileReaderpreviewSrc

我们仅显示图像的预览,因此我们正在检查上传的文件是否具有正确的格式(仅 jpg、jpeg 和 png 图像)并更新previewAvailable变量的状态。



setIsPreviewAvailable(uploadedFile.name.match(/\.(jpeg|jpg|png)$/));


Enter fullscreen mode Exit fullscreen mode

现在,通过运行命令重新启动应用程序yarn start并验证功能。

上传预览

这里,我们通过浏览的方式添加了一个文件。您也可以像下面这样通过拖放的方式添加文件。

拖放文件

如果您选择的文件不是图像,我们将不会显示消息所指示的预览No preview available for this file

无预览

添加掉落指示

如果您看到了拖放功能,我们没有显示任何表明文件被拖放到拖放区域的迹象,所以让我们添加它。

我们已经在文件内的refdiv 中添加了 class drop-zoneApp.js



<div {...getRootProps({ className: 'drop-zone' })} ref={dropRef}>


Enter fullscreen mode Exit fullscreen mode

并且还使用钩子dropRef在顶部创建了变量。useRef

onDragEnteronDragLeave道具添加到Dropzone组件。



<Dropzone
  onDrop={onDrop}
  onDragEnter={() => updateBorder('over')}
  onDragLeave={() => updateBorder('leave')}
>


Enter fullscreen mode Exit fullscreen mode

onDragEnter当文件超出放置区域时将触发该函数,onDragLeave当文件从放置区域移除时也将触发该函数。

在处理程序之前在组件内部创建一个新updateBorder函数ApphandleOnSubmit



const updateBorder = (dragState) => {
  if (dragState === 'over') {
    dropRef.current.style.border = '2px solid #000';
  } else if (dragState === 'leave') {
    dropRef.current.style.border = '2px dashed #e9ebeb';
  }
};


Enter fullscreen mode Exit fullscreen mode

由于我们已将dropRefref 添加到具有类的 div drop-zone,它将指向该 div,我们可以使用它的current属性来更新放置区域的边框dropRef.current.style.border

另外,在onDrop函数内部,在函数末尾添加以下行。



dropRef.current.style.border = '2px dashed #e9ebeb';


Enter fullscreen mode Exit fullscreen mode

因此,当我们将文件拖放到拖放区域上时,边框将恢复到正常状态。

现在,如果您检查应用程序,您将看到边框变化的拖放效果。

掉落效果

调用API进行文件上传

constants.js在文件夹中创建一个新文件,文件名称如下src/utils,内容如下



export const API_URL = 'http://localhost:3030';


Enter fullscreen mode Exit fullscreen mode

我们将很快在端口上启动我们的 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);
  }
};


Enter fullscreen mode Exit fullscreen mode

API_URL另外,在文件顶部导入。



import { API_URL } from '../utils/constants';


Enter fullscreen mode Exit fullscreen mode

在处理程序内部handleOnSubmit,我们首先检查用户是否输入了所有字段值并选择了文件,并且我们正在对/uploadAPI 进行 API 调用,我们将在下一部分中编写该 API。



await axios.post(`${API_URL}/upload`, formData, {
  headers: {
    'Content-Type': 'multipart/form-data'
  }
});


Enter fullscreen mode Exit fullscreen mode

我们正在向 API发出对象POST请求formData并发送title和实际文件。description

请注意,提及 multipart/form-data 的内容类型非常重要,否则文件将不会发送到服务器。

添加文件上传的服务器端代码

现在,让我们添加服务器端功能来上传文件。

server在文件夹内创建一个具有名称的文件夹,并从文件夹react-upload-download-files执行以下命令server



yarn init -y


Enter fullscreen mode Exit fullscreen mode

这将在文件夹内创建一个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


Enter fullscreen mode Exit fullscreen mode

.gitignore在文件夹内创建一个同名的新文件server,并在其中添加以下行,这样node_modules文件夹就不会添加到您的 Git 存储库中。



node_modules


Enter fullscreen mode Exit fullscreen mode

现在在文件夹内创建dbfilesmodel、文件夹。同样,在文件夹创建routesserverindex.jsserver

在文件夹中server/db,创建一个db.js包含以下内容的新文件



const mongoose = require('mongoose');

mongoose.connect('mongodb://127.0.0.1:27017/file_upload', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  useCreateIndex: true
});


Enter fullscreen mode Exit fullscreen mode

在此提供您的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;


Enter fullscreen mode Exit fullscreen mode

这里,我们定义了集合的模式,因为我们使用非常流行的Mongoose库来操作 MongoDB。我们将在集合中存储titledescription,因此我们在此文件中描述了每个类型的类型。file_pathfile_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;


Enter fullscreen mode Exit fullscreen mode

在这个文件中,我们使用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
  }
});


Enter fullscreen mode Exit fullscreen mode

multer函数以一个对象作为参数,该对象具有许多属性,其中一些是storagelimitsfileFilter函数。

该函数接受一个具有和函数的multer.diskStorage对象destinationfilename

这里我们使用 ES6 函数简写语法,因此



destination(req, file, cb) {


Enter fullscreen mode Exit fullscreen mode

和...相同



destination: function(req, file, cb) {


Enter fullscreen mode Exit fullscreen mode

destinationand函数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);
    }
  }
);


Enter fullscreen mode Exit fullscreen mode

在这里,我们将upload.single函数作为第二个参数传递给/upload路由,因此它将充当中间件,并将在执行函数主体之前首先执行。

请注意,该file参数upload.single必须与前端上传文件时使用的名称相匹配。

记住我们之前用于从文件进行 API 调用的代码App.js



const formData = new FormData();
formData.append('file', file);


Enter fullscreen mode Exit fullscreen mode

我们将文件添加到formData名为 的属性中file。该名称必须与upload.single参数名称匹配,否则文件上传将无法进行。

在函数内部,我们将获得对象title内部descriptionreq.body和实际文件req.file,因为我们使用了该multer库。

File然后我们将这些值传递给我们创建的模型的对象。



const file = new File({
  title,
  description,
  file_path: path,
  file_mimetype: mimetype
});


Enter fullscreen mode Exit fullscreen mode

并且调用save对象上的方法实际上会将数据保存在 MongoDB 数据库中。

如果文件类型不匹配jpeg|jpg|png|pdf|doc|docx|xlsx|xls或文件大小大于我们提到的(1MB),则将执行以下代码



(error, req, res, next) => {
  if (error) {
    res.status(500).send(error.message);
  }
};


Enter fullscreen mode Exit fullscreen mode

并将错误消息发送回客户端(我们的 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');
});


Enter fullscreen mode Exit fullscreen mode

在此文件中,我们使用Express服务器在端口上启动我们的 Node.js 应用程序3030

我们还使用npm 包作为中间件,因此当我们从在端口上运行的 React 应用程序向在端口上运行的 Node.js 应用程序进行 API 调用时cors不会出现错误CORS30003030

现在,让我们运行应用程序来检查上传功能。

打开文件并在属性中server/package.json添加脚本startscripts



"scripts": {
  "start": "nodemon index.js"
}


Enter fullscreen mode Exit fullscreen mode

server现在,打开另一个终端,保持 React 终端运行,并从文件夹内执行以下命令



yarn start


Enter fullscreen mode Exit fullscreen mode

这将启动我们的 Node.js 快速服务器,以便我们可以对其进行 API 调用。

还可以通过从终端运行以下命令来启动 MongoDB 数据库服务器(如果您已经关注过前面提到的这篇文章)



./mongod --dbpath=<path_to_mongodb-data_folder>


Enter fullscreen mode Exit fullscreen mode

现在您将打开三个终端:一个用于 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;


Enter fullscreen mode Exit fullscreen mode

在这个文件中,最初在useEffect钩子内部,我们对/getAllFilesAPI 进行 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.');
  }
});


Enter fullscreen mode Exit fullscreen mode

在这里,我们调用模型上的库.find的方法来获取数据库中添加的所有文件的列表,然后按日期降序对它们进行排序,这样我们就可以在列表中首先获得最近添加的文件。mongooseFilecreatedAt

然后我们将 API 的结果赋值给filesList状态中的数组



const { data } = await axios.get(`${API_URL}/getAllFiles`);
setErrorMsg('');
setFilesList(data);


Enter fullscreen mode Exit fullscreen mode

然后我们使用 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');
    }
  }
};


Enter fullscreen mode Exit fullscreen mode

在函数内部downloadFile,我们调用/download/:idAPI。请注意,我们将 设置responseTypeblob。这一点非常重要,否则您将无法获取正确格式的文件。

/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.');
  }
});


Enter fullscreen mode Exit fullscreen mode

这里,首先,我们用提供的 来检查是否存在这样的文件。如果存在,我们先设置文件的 ,id然后返回存储在文件夹中的文件。filescontent-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;


Enter fullscreen mode Exit fullscreen mode

现在,打开src/App.jshandleOnSubmit在调用/uploadAPI 后在处理程序内部添加一条语句以将用户重定向到FilesList组件



await axios.post(`${API_URL}/upload`, formData, {
  headers: {
    'Content-Type': 'multipart/form-data'
  }
});
props.history.push('/list'); // add this line


Enter fullscreen mode Exit fullscreen mode

所以现在,一旦文件上传完成,我们将被重定向到FilesList组件,在那里我们将看到上传的文件列表。

如果上传文件时出现错误,您将在 UI 上看到错误消息,并且不会被重定向到列表页面。

假设您已经yarn start在两个单独的终端中执行了用于启动 React 和 Node.js 应用程序的命令,并在另一个终端中执行了用于运行 MongoDB 服务器的命令。现在,让我们验证应用程序的功能。

上传图片文件演示

图片上传

上传 PDF 文件演示

PDF上传

上传 Excel 文件演示

Excel 上传

上传 Doc 文件演示

文档上传

上传不受支持的文件演示

上传文件错误

如您所见,我们能够成功上传和下载我们支持的格式列表中的任何类型的文件。

消除对 CORS 的需求

如前所述,为了在从 React App 向 Node.js App 调用 API 时停止出现 CORS 错误,我们cors在服务器端使用如下库:



app.use(cors());


Enter fullscreen mode Exit fullscreen mode

尝试从文件中删除此行,您将看到从 React 到 Node.js 的 API 调用失败。

CORS 错误

为了避免此错误,我们使用了 cors 中间件。但正因如此,世界上任何人都可以直接从他们的应用访问我们的 API,这出于安全考虑并不理想。

因此,为了消除对 cors 的需求,我们将在同一个端口上运行 Node.js 和 React 应用程序,这也将消除运行两个单独命令的需要。

cors因此,首先,从文件中删除的使用server/index.js,并删除require的语句cors

然后在语句前添加如下代码app.use(fileRoute)



app.use(express.static(path.join(__dirname, '..', 'build')));


Enter fullscreen mode Exit fullscreen mode

在这里,我们告诉 express 静态提供构建文件夹的内容。

yarn build当我们为 React App运行命令时,将创建构建文件夹。

要详细了解其实际工作原理,请查看我之前的文章

path在文件顶部导入 Node.js 包。



const path = require('path');


Enter fullscreen mode Exit fullscreen mode

您的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');
});


Enter fullscreen mode Exit fullscreen mode

现在,打开主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)"
  },


Enter fullscreen mode Exit fullscreen mode

现在,假设您已经启动了 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'));
});


Enter fullscreen mode Exit fullscreen mode

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');
});


Enter fullscreen mode Exit fullscreen mode

现在,通过运行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
PREV
44 个 React 前端面试题
NEXT
使用 React 构建出色的求职应用程序