首頁 > 軟體

Reactjs + Nodejs + Mongodb 實現檔案上傳功能範例詳解

2022-06-28 18:03:47

Reactjs + Nodejs + Mongodb 實現檔案上傳

概述

今天是使用 Reactjs + Nodejs + Mongodb 實現檔案上傳功能。前端我們使用 Reactjs + Axios 來搭建前端上傳檔案應用,後端我們使用 Node.js + Express + Multer + Mongodb 來搭建後端上傳檔案處理應用。

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 + Axios

設定 React 環境

這裡我們使用 pnpm vite 建立一個 React 專案

npx create-react-app 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;

初始化 Axios

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 資料庫中的資料

最後將這個物件匯出去

建立 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;

將檔案上傳元件新增到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>
      <div className="content">
        <UploadFiles />
      </div>
    </div>
  );
}

export default App;

上傳檔案設定埠

考慮到大多數的 HTTP Server 伺服器使用 CORS 設定,我們這裡在根目錄下新建一個 .env 的檔案,新增如下內容

PORT=8081

專案執行

到這裡我們可以執行下前端專案了,使用命令 pnpm start,瀏覽器位址列輸入 http://localhost:8081/, ok 專案正常執行

檔案選擇器、上傳按鈕、檔案列表都已經可以顯示出來了,但還無法上傳。這是因為後端部分還沒有跑起來,接下來,我帶領大家手把手搭建上傳檔案的後端部分。

後端部分

後端部分我們使用 Nodejs + Express + Multer + Mongodb 來搭建檔案上傳的專案,配合前端 Reactjs + Axios 來共同實現檔案上傳功能。

後端專案我們提供以下幾個API

  • POST /upload 檔案上傳介面
  • GET /files 檔案列表獲取介面
  • GET /files/[filename] 下載指定檔案

設定 Node.js 開發環境

我們先使用命令 mkdir 建立一個空資料夾,然後 cd 到資料夾裡面 這個資料夾就是我們的專案資料夾

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

接著使用命令

npm init --yes

初始化專案,接著安裝專案需要的依賴包, 輸入如下命令

npm install express cors multer multer-gridfs-storage mongodb

package.js 檔案

{
  "name": "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"
  }
}

設定 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()}-zhijianqiu-${file.originalname}`;
      return filename;
    }
    return {
      bucketName: dbConfig.filesBucket,
      filename: `${Date.now()}-zhijianqiu-${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 } 以限制檔案大小

建立檔案上傳的控制器

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,
};

定義路由

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 專案正常執行

檔案上傳介面

檔案列表介面

資料庫

React + Node.js 上傳檔案前後端一起執行

在 nodejs-mongodb-upload-files 資料夾根目錄執行後端 Nodejs

node src/server.js

在 react-multiple-files-upload 資料夾根目錄執行前端 React

pnpm start

然後開啟瀏覽器輸入前端存取網址:

到這裡整個前後端「上傳檔案」管理工具就搭建完成了。

到此這篇關於Reactjs + Nodejs + Mongodb 實現檔案上傳功能的文章就介紹到這了,更多相關Reactjs  Nodejs Mongodb檔案上傳內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com