3

全栈实战教程:React + Axios + Node.js + Express 搭建「文件上传」管理后台(内附开...

 1 year ago
source link: https://kalacloud.com/blog/react-axios-multiple-node-express-file-upload/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

全栈实战教程:React + Axios + Node.js + Express 搭建「文件上传」管理后台(内附开源代码)

前端工程师
最近更新 2022年06月27日

React + Node.js 全栈实战教程 - 手把手教你搭建「文件上传」管理后台

本教程手把手带领大家搭建一套通过 React + Node.js + Mongodb 上传文件的后台系统,只要你跟随本教程一步步走,一定能很好的理解整个前后端上传文件的代码逻辑。前端我们使用 Reactjs + Axios 来搭建前端上传文件应用,后端我们使用 Node.js + Express + Multer + Mongodb 来搭建后端上传文件处理应用。

当然,本教程还会教给大家如何写一个可以限制上传文件大小、有百分比进度条、可报错、可显示服务器上文件列表、可点击下载文件的前端操作界面。

最后完成的上传文件工具后台如下图,跟随本教学习,你也可以搭建出来。

文件上传完成

全栈实战教程:

后端实战教程:

如果你正在搭建后台管理工具,又不想处理前端问题,推荐使用卡拉云,卡拉云是新一代低代码开发工具,可一键接入常见数据库及 API ,无需懂前端,仅需拖拽即可快速搭建属于你自己的后台管理工具,一周工作量缩减至一天,详见本文文末。

React + Node.js + Mongodb「上传文件」前后端项目结构

前端项目结构

├── README.md
├── package-lock.json
└── node_modules
    └── ...
├── package.json
├── public
│   └── index.html
└── src
    ├── App.css
    ├── App.js
    ├── components
    │   └── UploadFiles.js
    ├── http-common.js
    ├── index.js
    └── services
        └── UploadFilesService.js

Reactjs 前端部分

  • App.js: 把我们的组件导入到 React 的起始页
  • components/UploadFiles.js: 文件上传组件
  • http-common.js: 使用 HTTP 基础 Url 和标头初始化 Axios。
  • 我们在.env中为我们的应用程序配置端口
  • services/UploadFilesService.js: 这个文件中的函数用于文件上传和获取数据库中文件数据

后端项目结构

├── README.md
├── package.json
├── pnpm-lock.yaml
└── node_modules
    └── ...
└── src
    ├── config
    │   └── db.js
    ├── controllers
    │   └── flileUploadController.js
    ├── middleware
    │   └── upload.js
    ├── routes
    │   └── index.js
    └── server.js

后端项目结构

  • src/db.js 包括 MongoDB 和 Multer 的配置(url、数据库、文件存储桶)。
  • middleware/upload.js:初始化 Multer GridFs 存储引擎(包括 MongoDB)并定义中间件函数。
  • controllers/flileUploadController.js:配置 Rest API
  • routes/index.js:路由,定义前端请求后端如何执行
  • server.js:Node.js入口文件

扩展阅读:《React Echarts 使用教程 - 如何在 React 中加入图表

✦ 前端部分-上传文件 React + Axios

配置 React 环境

这里我们使用 pnpm vite 创建一个 React 项目

npx create-react-app kalacloud-react-multiple-files-upload

项目创建完成后,cd 进入项目,安装项目运行需要的依赖包和 Axios 终端分别依次如下命令

pnpm install

pnpm install axios

执行完成我们启动项目 pnpm start

可以看到控制台中已经输出了信息,在浏览器地址栏中输入控制台输出的地址,项目已经跑起来了

导入 bootstrap 到项目中

运行如下命令

npm install bootstrap

bootstrap 安装完成后,我们打开 src/App.js 文件, 添加如下代码

import React from "react";
import "./App.css";
import "bootstrap/dist/css/bootstrap.min.css";

function App() {
  return (
    <div className="container">
      ...
    </div>
  );
}

export default App;

扩展阅读:《7 款最棒的开源 React 移动端 UI 组件库和模版框架

初始化 Axios HTTP 客户端

src 目录下 我们新建 http-common.js文件,并添加如下代码

import axios from "axios";

export default axios.create({
  baseURL: "http://localhost:8080",
  headers: {
    "Content-type": "application/json"
  }
});

这里 baseURL 的地址是我们后端服务器的 REST API 地址,要根据个人实际情况进行修改。本教程后文,教你搭建上传文件的后端部分,请继续阅读。

创建「上传文件」功能

src/services/UploadFilesService.js,这个文件主要的作用就是和后端项目通讯,以进行文件的上传和文件列表数据的获取等。

在文件中我们加入如下内容

import http from "../http-common";
const upload = (file, onUploadProgress) => {
  let formData = new FormData();
  formData.append("file", file);
  return http.post("/upload", formData, {
    headers: {
      "Content-Type": "multipart/form-data",
    },
    onUploadProgress,
  });
};
const getFiles = () => {
  return http.get("/files");
};
const FileUploadService = {
  upload,
  getFiles,
};
export default FileUploadService;

首先导入我们之前写好的 Axios HTTP 配置文件 http-common.js,并定义一个对象,在对象中添加两个属性函数,作用如下

  • upload:函数以 POST 的方式将数据提交到后端,接收两个参数 fileonUploadProgress
    • file 上传的文件,以 FormData 的形式上传
    • onUploadProgress 文件上传进度条事件,监测进度条信息
  • getFiles: 函数用于获取存储在 Mongodb 数据库中的数据

最后将这个对象导出去。

扩展阅读:《7 款最棒的开源 React UI 组件库和模版框架测评

创建 React 多文件上传组件

接下来我们来创建文件上传组件,首先组件要满足功能有文件上传,上传进度条信息展示,文件预览,提示信息,文件下载等功能

这里我们使用 React Hooks 和 useState 来创建文件上传组件,创建文件 src/components/UploadFiles,添加如下代码

import React, { useState, useEffect, useRef } from "react";
import UploadService from "../services/UploadFilesService";
const UploadFiles = () => {
  
  return (
    
  );
};
export default UploadFiles;

然后我们使用 React Hooks 定义状态

const UploadFiles = () => {
    const [selectedFiles, setSelectedFiles] = useState(undefined);
    const [progressInfos, setProgressInfos] = useState({ val: [] });
    const [message, setMessage] = useState([]);
    const [fileInfos, setFileInfos] = useState([]);
    const progressInfosRef = useRef(null)
}

状态定义好后,我们在添加一个获取文件的方法 selectFiles()

const UploadFiles = () => {
  ...
  const selectFiles = (event) => {
    setSelectedFiles(event.target.files);
    setProgressInfos({ val: [] });
  };
  ...
}

selectedFiles 用来存储当前选定的文件,每个文件都有一个相应的进度信息如文件名和进度信息等,我们将这些信息存储在 fileInfos中。

const UploadFiles = () => {
  ...
  const uploadFiles = () => {
    const files = Array.from(selectedFiles);
    let _progressInfos = files.map(file => ({ percentage: 0, fileName: file.name }));
    progressInfosRef.current = {
      val: _progressInfos,
    }
    const uploadPromises = files.map((file, i) => upload(i, file));
    Promise.all(uploadPromises)
      .then(() => UploadService.getFiles())
      .then((files) => {
        setFileInfos(files.data);
      });
    setMessage([]);
  };
  ...
}

我们上传多个文件的时候会将文件信息存储在 selectedFiles, 在上面的代码中 我们使用 Array.from 方法将可迭代数据转换数组形式的数据,接着使用 map 方法将文件的进度信息,名称信息存储到 _progressInfos

接着我们使用 map 方法调用 files 数组中的每一项,使 files 中的每一项都经过 upload 函数的处理,在 upload 函数中我们会返回上传文件请求函数 UploadService.uploadPromise 状态

所以 uploadPromises 中存储的就是处于 Promise 状态的上传文件函数,接着我们使用 Promise.all 同时发送多个文件上传请求,在所有文件都上传成功后,我们将会调用获取所有文件数据的接口,并将获取到的数据展示出来。

upload 函数代码如下

const upload = (idx, file) => {
    let _progressInfos = [...progressInfosRef.current.val];
    return UploadService.upload(file, (event) => {
        _progressInfos[idx].percentage = Math.round(
            (100 * event.loaded) / event.total
        );
        setProgressInfos({ val: _progressInfos });
    })
        .then(() => {
            setMessage((prevMessage) => ([
                ...prevMessage,
                "文件上传成功: " + file.name,
            ]));
        })
        .catch(() => {
            _progressInfos[idx].percentage = 0;
            setProgressInfos({ val: _progressInfos });
            setMessage((prevMessage) => ([
                ...prevMessage,
                "不能上传文件: " + file.name,
            ]));
        });
};

每个文件的上传进度信息根据 event.loadedevent.total 百分比值来计算,因为在调用 upload 函数的时候,已经将对应文件的索引传递进来了,所有我们根据对应的索引设置对应文件的上传进度

除了这些工作,我们还需要在 Effect HookuseEffect() 做如下功能,这部分代码的作用其实 componentDidMount 中起到的作用一致

const UploadFiles = () => {
  ...
  useEffect(() => {
    UploadService.getFiles().then((response) => {
      setFileInfos(response.data);
    });
  }, []);
  ...
}

到这里我们就需要将文件上传的 UI 代码添加上了,代码如下

const UploadFiles = () => {
  ...
  return (
    <div>
      {progressInfos && progressInfos.val.length > 0 &&
        progressInfos.val.map((progressInfo, index) => (
          <div className="mb-2" key={index}>
            <span>{progressInfo.fileName}</span>
            <div className="progress">
              <div
                className="progress-bar progress-bar-info"
                role="progressbar"
                aria-valuenow={progressInfo.percentage}
                aria-valuemin="0"
                aria-valuemax="100"
                style={{ width: progressInfo.percentage + "%" }}
              >
                {progressInfo.percentage}%
              </div>
            </div>
          </div>
        ))}
      <div className="row my-3">
        <div className="col-8">
          <label className="btn btn-default p-0">
            <input type="file" multiple onChange={selectFiles} />
          </label>
        </div>
        <div className="col-4">
          <button
            className="btn btn-success btn-sm"
            disabled={!selectedFiles}
            onClick={uploadFiles}
          >
            上传
          </button>
        </div>
      </div>
      {message.length > 0 && (
        <div className="alert alert-secondary" role="alert">
          <ul>
            {message.map((item, i) => {
              return <li key={i}>{item}</li>;
            })}
          </ul>
        </div>
      )}
      <div className="card">
        <div className="card-header">List of Files</div>
        <ul className="list-group list-group-flush">
          {fileInfos &&
            fileInfos.map((file, index) => (
              <li className="list-group-item" key={index}>
                <a href={file.url}>{file.name}</a>
              </li>
            ))}
        </ul>
      </div>
    </div>
  );
};

在 UI 相关的代码中, 我们使用了 Bootstrap 的进度条

  • 使用 .progress 作为最外层包装
  • 内部使用 .progress-bar 显示进度信息
  • .progress-bar 需要 style 按百分比设置进度信息
  • .progress-bar 进度条还可以设置 rolearia 属性

文件列表信息的展示我们使用 map 遍历 fileInfos 数组,并且将文件的 url,name 信息展示出来

最后,我们将上传文件组件导出

const UploadFiles = () => {
  ...
}
export default UploadFiles;

扩展阅读:《最好用的 8 款 React Datepicker 时间日期选择器测评推荐

将文件上传组件添加到 App 组件

import React from "react";
import "./App.css";
import "bootstrap/dist/css/bootstrap.min.css";
import UploadFiles from "./components/UploadFiles.js"

function App() {
  return (
    <div className="container">
      <h4> 使用 React 搭建文件上传 Demo</h4>
      <p> <a href="https://kalacloud.com/">卡拉云</a>-低代码开发工具1秒搭建 </p>
      <p>使用卡拉云无需懂任何前端技术,仅需拖拽即可搭建属于你的后台管理系统</p>
      <div className="content">
        <UploadFiles />
      </div>
    </div>
  );
}

export default App;

上传文件配置端口

考虑到大多数的 HTTP Server 服务器使用 CORS 配置,我们这里在根目录下新建一个 .env 的文件,添加如下内容

PORT=8081

运行 React 项目

到这里我们可以运行下前端项目了,使用命令 pnpm start,浏览器地址栏输入 http://localhost:8081/, ok 项目正常运行

前端运行

文件选择器、上传按钮、文件列表都已经可以显示出来了,但还无法上传。这是因为后端部分还没有跑起来,接下来,我带领大家手把手搭建上传文件的后端部分。

React 前端「文件上传」源码

你可以在我们的 github 上下载到完整的 React 图片上传 Demo。

当然你也可以不用这么费劲搭建前端做图片上传功能,直接使用卡拉云,无需懂前后端,简单拖拽即可生成一套属于你自己的后台管理工具。

扩展阅读:《React form 表单验证终极教程

✦ 后端部分 - 文件上传 Node.js + Express + Multer + MongoDB

后端部分我们使用 Nodejs + Express + Multer + Mongodb 来搭建文件上传的项目,配合前端 Reactjs + Axios 来共同实现文件上传功能。

后端项目我们提供以下几个API

  • POST /upload 文件上传接口
  • GET /files 文件列表获取接口
  • GET /files/[filename] 下载指定文件

配置 Node.js 开发环境

我们先使用命令 mkdir 创建一个空文件夹,然后 cd 到文件夹里面 这个文件夹就是我们的项目文件夹

mkdir kalacloud-nodejs-mongodb-upload-files
cd kalacloud-nodejs-mongodb-upload-files

接着使用命令

npm init --yes

初始化项目,接着安装项目需要的依赖包, 输入如下命令

npm install express cors multer multer-gridfs-storage mongodb

package.js 文件

{
  "name": "kalacloud-nodejs-mongodb-upload-files",
  "version": "1.0.0",
  "description": "Node.js upload multiple files to MongoDB",
  "main": "src/server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "node",
    "upload",
    "multiple",
    "files",
    "mongodb"
  ],
  "license": "ISC",
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "mongodb": "^4.1.3",
    "multer": "^1.4.3",
    "multer-gridfs-storage": "^5.0.2"
  }
}

扩展阅读:《React Router 6 (React路由) 最详细教程

配置 MongoDB 数据库

src/config/db.js

module.exports = {
  url: "mongodb://localhost:27017/",
  database: "files_db",
  filesBucket: "photos",
};

配置文件上传存储的中间件

src/middleware/upload.js

const util = require("util");
const multer = require("multer");
const { GridFsStorage } = require("multer-gridfs-storage");
const dbConfig = require("../config/db");

var storage = new GridFsStorage({
  url: dbConfig.url + dbConfig.database,
  options: { useNewUrlParser: true, useUnifiedTopology: true },
  file: (req, file) => {
    const match = ["image/png", "image/jpeg", "image/gif"];

    if (match.indexOf(file.mimetype) === -1) {
      const filename = `${Date.now()}-kalacloud-${file.originalname}`;
      return filename;
    }
    return {
      bucketName: dbConfig.filesBucket,
      filename: `${Date.now()}-kalacloud-${file.originalname}`
    };
  }
});
const maxSize = 2 * 1024 * 1024;
var uploadFiles = multer({ storage: storage, limits: { fileSize: maxSize } }).single("file");
var uploadFilesMiddleware = util.promisify(uploadFiles);
module.exports = uploadFilesMiddleware;

这里我们定义一个 storage 的配置对象 GridFsStorage

  • url: 必须是指向 MongoDB 数据库的标准 MongoDB 连接字符串。multer-gridfs-storage 模块将自动为您创建一个 mongodb 连接。

  • options: 自定义如何建立连接

  • file: 这是控制数据库中文件存储的功能。该函数的返回值是一个具有以下属性的对象:filename, metadata, chunkSize, bucketName, contentType... 我们还检查文件是否为图像 file.mimetypebucketName 表示文件将存储在 photos.chunksphotos.files 集合中。

  • 接下来我们使用 multer 模块来初始化中间件 util.promisify() 并使导出的中间件对象可以与 async-await.

  • single() 带参数的函数是 input 标签的名称

  • 这里使用 Multer API 来限制上传文件大小,添加 limits: { fileSize: maxSize } 以限制文件大小

扩展阅读:《最好的 6 个 React Table 组件详细亲测推荐

创建文件上传的控制器

controllers/flileUploadController.js

这个文件主要用于文件上传,我们创建一个名 upload 函数,并将这个函数导出去

  • 我们使用 文件上传中间件函数处理上传的文件
  • 使用 Multer 捕获相关错误

文件列表数据获取和下载

  • getListFiles: 函数主要是获取 photos.files,返回 url, name
  • download(): 接收文件 name 作为输入参数,从 mongodb 内置打开下载流 GridFSBucket,然后 response.write(chunk) API 将文件传输到客户端。
const upload = require("../middleware/upload");
const dbConfig = require("../config/db");

const MongoClient = require("mongodb").MongoClient;
const GridFSBucket = require("mongodb").GridFSBucket;

const url = dbConfig.url; 

const baseUrl = "http://localhost:8080/files/";

const mongoClient = new MongoClient(url);

const uploadFiles = async (req, res) => {
  try {
    await upload(req, res);
    if (req.file == undefined)  {
      return res.status(400).send({ message: "请选择要上传的文件" });
    }
    return res.status(200).send({
      message: "文件上传成功" + req.file.originalname,
    });
  } catch (error) {
    console.log(error);
     if (error.code == "LIMIT_FILE_SIZE") {
      return res.status(500).send({
        message: "文件大小不能超过 2MB",
      });
    }
    return res.status(500).send({
      message: `无法上传文件:, ${error}`
    });
  }
};

const getListFiles = async (req, res) => {
  try {
    await mongoClient.connect();

    const database = mongoClient.db(dbConfig.database); 
    const files = database.collection(dbConfig.filesBucket + ".files");
    let fileInfos = [];

    if ((await files.estimatedDocumentCount()) === 0) {
        fileInfos = []
    }

    let cursor = files.find({})
    await cursor.forEach((doc) => {
      fileInfos.push({
        name: doc.filename,
        url: baseUrl + doc.filename,
      });
    });

    return res.status(200).send(fileInfos);
  } catch (error) {
    return res.status(500).send({
      message: error.message,
    });
  }
};

const download = async (req, res) => {
  try {
    await mongoClient.connect();
    const database = mongoClient.db(dbConfig.database);
    const bucket = new GridFSBucket(database, {
      bucketName: dbConfig.filesBucket,
    });

    let downloadStream = bucket.openDownloadStreamByName(req.params.name);
    downloadStream.on("data", function (data) {
      return res.status(200).write(data);
    });

    downloadStream.on("error", function (err) {
      return res.status(404).send({ message: "无法获取文件" });
    });

    downloadStream.on("end", () => {
      return res.end();
    });
  } catch (error) {
    return res.status(500).send({
      message: error.message,
    });
  }
};

module.exports = {
  uploadFiles,
  getListFiles,
  download,
};

扩展阅读:《React Draggable 实现拖拽 - 最详细中文教程

定义 routes 路由

routes 文件夹中,使用 Express Routerindex.js 中定义路由

const express = require("express");
const router = express.Router();
const uploadController = require("../controllers/flileUploadController");

let routes = app => {
  router.post("/upload", uploadController.uploadFiles);
  router.get("/files", uploadController.getListFiles);
  router.get("/files/:name", uploadController.download);
  return app.use("/", router);
};
 
module.exports = routes;
  • POST /upload: 调用 uploadFiles控制器的功能。
  • GET /files 获取/files图像列表。
  • GET /files/:name 下载带有文件名的图像。

创建 Express 服务器

const cors = require("cors");
const express = require("express");
const app = express();
global.__basedir = __dirname;
var corsOptions = {
  origin: "http://localhost:8081"
};
app.use(cors(corsOptions));
const initRoutes = require("./routes");
app.use(express.urlencoded({ extended: true }));
initRoutes(app);
let port = 8080;
app.listen(port, () => {
  console.log(`Running at localhost:${port}`);
});

这里我们导入了 ExpressCors,

  • Express 用于构建 Rest api
  • Cors提供 Express 中间件以启用具有各种选项的 CORS。 创建一个 Express 应用程序,然后使用方法添加cors中间件 在端口 8080 上侦听传入请求。

运行项目并测试

在项目根目录下在终端中输入命令 node src/server.js, 控制台显示

Running at localhost:8080

使用 postman 工具测试,ok 项目正常运行

文件上传接口

postman文件上传接口

postman文件上传接口

文件列表接口

postman文件列表接口

MongoDB 数据库

postman数据库

React + Node.js 上传文件前后端一起运行

在 kalacloud-nodejs-mongodb-upload-files 文件夹根目录运行后端 Nodejs

node src/server.js

在 kalacloud-react-multiple-files-upload 文件夹根目录运行前端 React

pnpm start

然后打开浏览器输入前端访问网址:

上传文件最终完成图

到这里整个前后端「上传文件」管理工具就搭建完成了。

Node.js 后端「文件上传」源码 你可以在我们的 github 上下载到完整的 Node.js 后端「文件上传」源码。

如果你还没搞懂,也不用着急,直接使用卡拉云,无需懂任何前后端技术,仅需简单的鼠标拖拽即可快速生成包括「文件上传」管理在内的任何后台管理工具。

「文件上传」前后端搭建总结及卡拉云

本教程手把手教大家搭建 React 前端 + Node.js 后端 的「文件上传」管理工具,如果你一步步跟着走,一定已经把 Demo 跑起来了。但如果你会使用最新的低代码开发工具「卡拉云」,完全不需要这么繁琐,仅需 1 分钟,就能搭建起属于自己的「文件上传」管理工具。

卡拉云文件上传管理

立即开通卡拉云,从侧边工具栏直接拖拽组件到页面,生成上传组件和文件管理工具。1 分钟搞定「上传文件」管理工具。

再看个卡拉云的 Demo 案例,下面是用卡拉云搭建的数据库 CURD 后台管理系统,只需拖拽组件,即可在10分钟内完成搭建。

卡拉云 SQL admin 后台管理系统

可直接分享给同事一起使用:https://my.kalacloud.com/apps/8z9z3yf9fy/published

卡拉云是新一代低代码开发平台,与 React 这类框架相比,卡拉云无需配置开发环境,直接注册即可开始搭建。开发者无需处理任何前端问题,简单拖拽即可生成图表、表格、表单、富文本等功能组件,一键接入数据库及 API,快速完成企业内部工具搭建,还可以分享给团队成员共享使用,数周的开发时间,缩短至 1 小时。

全栈实战教程:

后端实战教程:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK