全栈实战:React + Nodejs 搭建带预览的「上传图片/预览」管理后台

A kitten
知见秋
前端工程师
最近更新 2022年06月27日

全栈实战:React + Nodejs 搭建带预览的「上传图片/预览」管理后台

手把手教你开发带预览的 React 图片上传组件,即图片上传管理后台。只要你跟本教程一步一步走,最终能很好的理解整个前后端传图的工程逻辑。前端我们使用 React + Axios + Multipart 来搭建前端上传图片应用,后端我们使用 Node.js + Express + Multer 来搭建后端上传图片的后端处理。

本教程还会教给大家如何写一个可限制上传图片大小,有进度条,可报错,可显示图片列表,可下载已上传图片的图片管理后台。

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

React + Nodejs 搭建带预览的「上传图片/预览」管理后台-完成图

实现的功能:

  • 图片上传功能
  • 图片上传显示进度条功能
  • 图片预览功能
  • 图片列表功能
  • 图片下载功能

前端使用的技术:

  • Reactjs
  • Bootstrap
  • Axios

后端使用的技术/数据库:

  • Nodejs
  • Express
  • Multer
  • Mongodb
  • Multer-gridfs-storage
  • Cors

跟随本示例学习,你也可以搭建出来。

全栈实战教程:

后端实战教程:

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

✦ 前端部分 - React + React 图片上传组件 + Axios + Multipart

React 前端项目结构

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

配置 React 环境

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

npx create-react-app kalacloud-react-upload-images

完成后,cd 进入项目根目录

安装 Axios

npm install axios

安装完成后,我们使用命令 npm start 启动项目,可以看到 项目已经正常启动了

这里我们使用 bootstrap 的样式,导入 Bootstrap 到项目中

打开文件 public/index.html ,将以下代码添加到 head

<link type="text/css" rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" />

配置 Axios

src 文件夹下新建文件 http-common.js, 并添加如下内容

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

这里 baseURL 是你本地的地址。

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

React 图片上传功能

前端项目的基本配置已经完成,接下来我们开始图片上传功能,在 src 文件下 创建如下文件

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

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

import http from "../http-common";

class FileUploadService {
  upload(file, onUploadProgress) {
    let formData = new FormData();
    formData.append("file", file);
    return http.post("/upload", formData, {
      headers: {
        "Content-Type": "multipart/form-data",
      },
      onUploadProgress,
    });
  }

  getFiles() {
    return http.get("/files");
  }
}

export default new FileUploadService();
  • upload: 用于将文件数据以 post 请求格式,FormData 键值对的形式提交到后端, onUploadProgress 则是用于获取进度条数据
  • getFiles: 用于获取服务器上的文件列表数据

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

React 搭建后台太难?

试试卡拉云,无需懂前端,拖拽组件连接 API 和数据库直接生成后台系统,两个月的工期降低至1天

在 React 中创建图片上传组件

图片上传组件要满足功能有 图片上传,进度条,预览,提示信息等

首先建一个文件,并引入 import UploadService from "../services/fileUpload"

src/components/UploadFiles

代码如下:

import React, { Component } from "react";
import UploadService from "../services/UploadFilesService"

export default class UploadImages extends Component {
  constructor(props) {
    super(props);
  }

  render() {

    return (
      <div>

        
      </div>
    );
  }
}

接着我们在 constructor() 方法内部定义状态, 代码如下

export default class UploadImages extends Component {
  constructor(props) {
    super(props);
    ...
    this.state = {
      selectedFiles: undefined,
      previewImages: [],
      progressInfos: [],
      message: [],
      imageInfos: [],
    };
  }
}

状态定义好后,我们在添加一个选择图片的方法 selectFiles(),并添加 <input type="file" >,

export default class UploadImages extends Component {
  ...
  selectFiles(event) {
    let images = [];
    for (let i = 0; i < event.target.files.length; i++) {
      images.push(URL.createObjectURL(event.target.files[i]))
    }
    this.setState({
      progressInfos: [],
      message: [],
      selectedFiles: event.target.files,
      previewImages: images
    });
  }
}

我们使用 URL.createObjectURL() 用来获取图片预览 URL 并将其放入 previewImages 数组中, URL.createObjectURL()方法会创建一个 DOMString ,其中包含一个表示参数中提供的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定,

接着我们定义文件上传方法 upload

export default class UploadImages extends Component {
  ...
  upload(idx, file) {
    let _progressInfos = [...this.state.progressInfos];
    UploadService.upload(file, (event) => {
      _progressInfos[idx].percentage = Math.round((100 * event.loaded) / event.total);
      this.setState({
        progressInfos: _progressInfos,
      });
    })
      .then(() => {
        this.setState((prev) => {
        let nextMessage = [...prev.message, `${file.name} 上传成功!`];
          return {
            message: nextMessage
          };
        });
        return UploadService.getFiles();
      })
      .then((files) => {
        this.setState({
          imageInfos: files.data,
        });
      })
      .catch(() => {
        _progressInfos[idx].percentage = 0;
        this.setState((prev) => {
          let nextMessage = [...prev.message, "Could not upload the image: " + file.name];
          return {
            progressInfos: _progressInfos,
            message: nextMessage
          };
        });
      });
  }
}

这里图片的上传进度根据 event.loaded 和计算 event.total,图片上传完毕后,我们调用 UploadService.getFiles(),来获取服务器上图片信息来展示,另外,我们要需要在 componentDidMount 钩子函数中调用,以便在初始状态时获取数据

export default class UploadImages extends Component {
  ...
  componentDidMount() {
    UploadService.getFiles().then((response) => {
      this.setState({
        imageInfos: response.data,
      });
    });
  }
}

这里我们将界面的上传的 UI 渲染代码添加上

 <div>
        <div className="row">
          <div className="col-8">
            <label className="btn btn-default p-0">
              <input type="file" accept="image/*" onChange={this.selectFiles} />
            </label>
          </div>

          <div className="col-4">
            <button
              className="btn btn-success btn-sm"
              disabled={!selectedFiles}
              onClick={this.uploadImages}
            >
              上传
            </button>
          </div>
        </div>

        {progressInfos &&
          progressInfos.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>
          ))}

        {previewImages && (
          <div>
            {previewImages.map((img, i) => {
              return <img className="preview" src={img} alt={"image-" + i}  key={i}/>;
            })}
          </div>
        )}

        {message.length > 0 && (
          <div className="alert alert-secondary mt-2" role="alert">
            <ul>
              {message.map((item, i) => {
                return <li key={i}>{item}</li>;
              })}
            </ul>
          </div>
        )}

        <div className="card mt-3">
          <div className="card-header">图片列表</div>
          <ul className="list-group list-group-flush">
            {imageInfos &&
              imageInfos.map((img, index) => (
                <li className="list-group-item " key={index}>
                  <img src={img.url} alt={img.name} height="80px" />
                  <span className="ml-10"><a href={img.url} target="_blank">{img.name}</a></span>
                </li>
              ))}
          </ul>
        </div>
      </div>

在上面的代码中我们使用了 Boostrap 的进度条, 具体可查看 Bootstrap 文档。

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

在 React 里将图片上传组件添加到 App 组件

打开 App.js 文件,将组件 UploadImages 引入并添加

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

import UploadImages from "./components/UploadFiles.js"

function App() {
  return (
    <div className="container">
      <h4>卡拉云-低代码开发工具 1秒搭建图片上传组件 Demo</h4>
      <p>使用卡拉云无需懂任何前端技术,仅需要拖拽即可搭建属于您的后台管理系统</p>
      <div className="content">
        <UploadImages />
      </div>
    </div>
  );
}

export default App;

配置上传图片端口

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

PORT = 8081

运行 React 项目

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

react 前端界面

前端项目到这里,大部分已经完成了,现在我们搭建后端部分,连通前后端。

React 前端「图片上传」源码

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

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

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

✦ 后端部分 - 图片上传 Node.js + Express + Multer + MongoDB

后端部分我们使用 Nodejs + Express + Multer 来搭建图片上传的项目,配合前端 React + Axios 来共同实现图片上传的前后端项目。

后端项目目录结构

├── README.md
├── node_modules
├── package-lock.json
├── package.json
└── src
    ├── config
    │   └── db.js
    ├── controllers
    │   ├── home.js
    │   └── upload.js
    ├── middleware
    │   └── upload.js
    ├── routes
    │   └── index.js
    ├── server.js
    └── views
        └── index.html
  • config/db.js:包括 MongoDB 和 Multer 的配置(url、数据库、图像存储桶)。
  • routes/index.js:定义从视图调用的端点的路由,使用控制器来处理请求。
  • controllers: home.js返回views/index.html upload.js处理上传、存储、显示和下载图像
  • middleware/upload.js:初始化 Multer GridFs 存储引擎(包括 MongoDB)并定义中间件函数。
  • server.js:初始化路由,配置 CORS,入口文件

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

  • 文件上传接口
  • 文件列表获取接口
  • 使用 url 下载文件接口

我们可以使用 postman 工具先看下接口的情况,如下图

向后端服务器发 POST 请求图片上传 postman-post-update

获取文件列表接口 postman-get-update

打开数据库连接工具,可以看到,数据库里,已经有上传的文件了

mongodb-gui

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

创建项目 配置模块

我们先使用命令 mkdir 创建一个空文件夹,然后 cd 到文件夹里面

这个文件夹就是我们的项目文件夹

mkdir kalacloud-nodejs-express-upload-files
cd kalacloud-nodejs-express-upload-files

接着使用命令

npm init

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

npm install express cors multer multer-gridfs-storage mongodb

package.js 文件

{
  "name": "kalacloud-nodejs-express-upload-files",
  "version": "1.0.0",
  "description": "Node.js upload multiple files/images to MongoDB",
  "main": "src/server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "node",
    "upload",
    "multiple",
    "files",
    "images",
    "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 数据库

config/db.js

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

配置图片上传存储的中间件

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"];

    if (match.indexOf(file.mimetype) === -1) {
      const filename = file.originalname;
      return filename;
    }

    return {
      bucketName: dbConfig.imgBucket,
      filename: file.originalname
    };
  }
});
var uploadFiles = multer({ storage: storage }).array("file", 10);
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.mimetype。bucketName表示文件将存储在photos.chunks和photos.files集合中。

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

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

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

创建文件上传的控制器

controllers/upload.js

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

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

文件列表数据获取和下载

  • getListFiles函数主要是获取 photos.files,返回 url, name, id
  • download(): 接收文件 id 作为输入参数,从 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);
    console.log(req.files);

    if (req.files.length <= 0) {
      return res
        .status(400)
        .send({ message: "You must select at least 1 file." });
    }

    return res.status(200).send({
      message: "文件上传成功",
    });

  } catch (error) {
    console.log(error);

    if (error.code === "LIMIT_UNEXPECTED_FILE") {
      return res.status(400).send({
        message: "Too many files to upload.",
      });
    }
    return res.status(500).send({
      message: `Error when trying upload many files: ${error}`,
    });
  }
};

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

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

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

    let cursor = images.find({})
    await cursor.forEach((doc) => {
      fileInfos.push({
        id: doc._id,
        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.imgBucket,
    });

    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: "Cannot download the Image!" });
    });

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

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

定义路由 routes

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

const express = require("express");
const router = express.Router();
const homeController = require("../controllers/home");
const uploadController = require("../controllers/upload");
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控制器的功能。
  • 获取/files图像列表。
  • GET/files/:name下载带有文件名的图像。

扩展阅读:《最好的 6 款 React admin 后台管理系统模板和框架

创建 Express 服务器

server.js

const cors = require("cors");
const express = require("express");
const app = express();
const initRoutes = require("./routes");

var corsOptions = {
  origin: "http://localhost:8081"
};

app.use(cors(corsOptions));
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 项目正常运行

向后端服务器发 POST 请求图片上传 postman-post-update

获取文件列表接口 postman-get-update

数据库 mongodbgui

React + Node.js 图片上传前后端一起运行联调

我们先启动后端项目 node src/server.js, 接着再启动前端项目 npm start,

测试上传,获取等接口,一切正常。

到这里整个前后端「上传图片」功能示例就算完成了。

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

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 小时。

全栈实战教程:

后端实战教程: