├── 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.jsReactjs 前端部分
├── README.md ├── package.json ├── pnpm-lock.yaml └── node_modules └── ... └── src ├── config │ └── db.js ├── controllers │ └── flileUploadController.js ├── middleware │ └── upload.js ├── routes │ └── index.js └── server.js后端项目结构
npx create-react-app kalacloud-react-multiple-files-upload项目创建完成后,cd 进入项目,安装项目运行需要的依赖包和 Axios 终端分别依次如下命令
pnpm install pnpm install axios执行完成我们启动项目 pnpm start
npm install bootstrapbootstrap 安装完成后,我们打开 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;
import axios from "axios"; export default axios.create({ baseURL: "http://localhost:8080", headers: { "Content-type": "application/json" } });这里 baseURL 的地址是我们后端服务器的 REST API 地址,要根据个人实际情况进行修改。本教程后文,教你搭建上传文件的后端部分,请继续阅读。
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,并定义一个对象,在对象中添加两个属性函数,作用如下:
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 中
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.loaded 和 event.total 百分比值来计算,因为在调用 upload 函数的时候,已经将对应文件的索引传递进来了,所有我们根据对应的索引设置对应文件的上传进度
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 的进度条:
const UploadFiles = () => { ... } export default UploadFiles;
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;上传文件配置端口
PORT=8081运行 React 项目
GET /files/[filename] 下载指定文件
mkdir kalacloud-nodejs-mongodb-upload-files cd kalacloud-nodejs-mongodb-upload-files接着使用命令
npm init --yes初始化项目,接着安装项目需要的依赖包, 输入如下命令
npm install express cors multer multer-gridfs-storage mongodbpackage.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" } }
module.exports = { url: "mongodb://localhost:27017/", database: "files_db", filesBucket: "photos", };配置文件上传存储的中间件
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
3. file: 这是控制数据库中文件存储的功能。该函数的返回值是一个具有以下属性的对象:filename, metadata, chunkSize, bucketName, contentType... 我们还检查文件是否为图像 file.mimetype。
4. bucketName 表示文件将存储在 photos.chunks 和 photos.files 集合中。
5. 接下来我们使用 multer 模块来初始化中间件 util.promisify() 并使导出的中间件对象可以与 async-await.3,返回响应
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, };
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/:name 下载带有文件名的图像。
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}`); });这里我们导入了 Express 和 Cors:
2. Cors提供 Express 中间件以启用具有各种选项的 CORS。 创建一个 Express 应用程序,然后使用方法添加cors中间件 在端口 8080 上侦听传入请求。
Running at localhost:8080使用 postman 工具测试,ok 项目正常运行
node src/server.js在 kalacloud-react-multiple-files-upload 文件夹根目录运行前端 React
pnpm start