如何使用进度条上传多个文件(ReactJS + Redux 和 ExpressJS)
入门
设置服务器和 API
设置前端
上传物品
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
如果你以前从未接触过文件上传,现在突然接到这样的任务,你可能会感到不知所措(嗯,我这算是半个亲身经历吧😛)。
事实上,如果你是一名Web开发人员,你迟早会遇到这项任务,因为它几乎在所有Web应用程序中都会用到。
在本文中,我将向你展示如何使用JavaScript以我的方式实现它。
我已经发布了第二篇与此相关的文章,内容是关于如何添加取消和重试功能。如果您感兴趣,可以去看看,链接在这里:如何使用 ReactJS 上传多个文件并实现取消和重试功能
在我们继续之前,这里举一个我们想要达到的最终结果的例子:
如果你想查看源代码,可以点击这里。但我会一步一步地解释如何从头开始构建它。
入门
首先,我们来谈谈后端和前端将使用哪些技术。
- ReactJS—— 我们的主要前端应用程序框架[FE]
- Redux - 用于 ReactJS [FE] 的状态管理
- Redux-thunk - 用于在 Redux 上执行异步逻辑 [FE]
- Axios - 基于 Promise 的客户端和服务端 HTTP 请求 [前端]
- Lodash - 一套实用的 JavaScript 函数 [FE]
- ExpressJS - 一个用于模拟我们 API 服务器的 NodeJS 服务器 [BE]
- Multer - 用于处理的 Node.js 中间件
multipart/form-data[BE]
现在我们开始创建项目文件夹:
$ mkdir file-upload-example
$ cd file-upload-example
$ mkdir server
// Our folder structure will be like this
./file-upload-example
../server
设置服务器和 API
首先,我们需要安装后端的所有依赖项。
$ cd server
$ touch server.js // creating new file
$ npm init -y // creating default package.json file
$ npm i express multer cors
我将直接展示server.js代码,因为我们将更侧重于前端部分,以下是代码:
让我们在终端输入命令来运行它node server.js。
如果看到消息Server running on port 5000 ,说明你的服务器运行成功了。太棒了!后端配置已经完成,接下来我们来看看前端。对了,如果你对multer库感兴趣,可以点击这里查看。
NOTE: you can let the server running while we're developing our frontend side
设置前端
现在打开一个新的终端(因为我们要运行两个本地主机,#1 服务器和 #2 客户端),并进入文件夹根目录。我们将使用create-react-app设置前端,并安装依赖项,那么让我们开始吧:
$ npx create-react-app client
$ cd client
$ npm i redux react-redux redux-thunk axios lodash
$ npm start
// Now our folder structure will be like this
./file-upload-example
../server
../client
现在你的 React 应用将在 localhost:3000 的新浏览器标签页中打开。太棒了,让我们开始添加内容吧!首先,我们将修改我们的App.js
这样一来,我们就添加了一个输入按钮,当我们上传文件时,它会显示console.log正在上传的文件。
现在我们来配置 Redux。
其思路是,每次我们上传文件时,这些文件都会以特定的数据结构存储到 Redux store 中。
首先,我们创建一个新文件夹,redux并在其中创建一个文件(目前为空),如下所示:
//uploadFile.types.js
const uploadFileTypes = {
SET_UPLOAD_FILE: 'SET_UPLOAD_FILE',
}
export default uploadFileTypes
//uploadFile.actions.js
import uploadFileTypes from './uploadFile.types'
export const setUploadFile = data => ({
type: uploadFileTypes.SET_UPLOAD_FILE,
payload: data,
})
// uploadFile.reducer.js
import uploadFileTypes from './uploadFile.types'
import { modifyFiles } from './uploadFile.utils'
const INITIAL_STATE = {
fileProgress: {
// format will be like below
// 1: { --> this interpreted as uploaded file #1
// id: 1,
// file,
// progress: 0,
// },
},
}
const fileProgressReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case uploadFileTypes.SET_UPLOAD_FILE:
return {
...state,
fileProgress: {
...state.fileProgress,
...modifyFiles(state.fileProgress, action.payload),
},
}
default:
return state
}
}
export default fileProgressReducer
我们稍后会定义这些modifyFiles工具,但现在我想解释一下数据结构fileProgress 。我们将以对象格式而不是数组格式保存这些文件,但为什么呢?这是因为每次上传进度递增时,我们需要更新 Redux store 中每个文件的进度字段。
为了实现这一点,如果fileProgress类型是数组:
- 我们应该先遍历数组(找到索引),然后才能更新所需的项。但是每次想要更新每个文件的进度时,都需要重复遍历数组。这很不合理。
但如果我们使用对象类型代替fileProgress :
- 我们不需要进行循环,只需要提供每个文件的确切对象键,它就可以直接更新进度。
可能有些人会对此感到困惑,我们先跳过这一步,稍后再通过查看实际代码来理解它。
现在让我们在 . 上定义 modifyFiles 工具uploadFile.utils.js。
import { size } from 'lodash'
export const modifyFiles = (existingFiles, files) => {
let fileToUpload = {}
for (let i = 0; i < files.length; i++) {
const id = size(existingFiles) + i + 1
fileToUpload = {
...fileToUpload,
[id]: {
id,
file: files[i],
progress: 0,
},
}
}
return fileToUpload
}
这个工具函数会将传入的文件修改为一个对象,最后将每个文件对象填充为与注释中的数据结构相同的内容INITIAL_STATE(正如我们之前提到的)。
现在为了测试它,我们应该将这个 Redux 应用到我们的应用程序中,让我们开始吧。
// root-reducer.js
import { combineReducers } from 'redux'
import UploadFile from './uploadFile/uploadFile.reducer'
const rootReducer = combineReducers({
UploadFile,
})
export default rootReducer
别忘了使用setUploadFile上传按钮App.js
现在该检查一下本地主机了,其行为应该与此类似。
如上图所示,我们可以追踪上传到 Redux store 的文件。有些人可能会有以下两个疑问:第一,为什么我们看不到任何文件?第二,为什么 Redux store 中`on`console.log的值是一个空对象,而不是文件数据? 让我们逐一探讨。filefileProgress
console.log因为在将文件保存到 Redux store 后,我们直接将输入元素的值赋给了变量。现在页面什么也没显示''(e.target.value = '')。我们需要清除这个input值,以便之后可以上传另一个文件。- 现在我们可以跟踪 redux-store 中的文件,但其值为空对象
{},这是因为 Files 类型的数据不是字面对象,redux-dev-tools 无法读取该类型,因此 redux-dev-tools 将其显示为空对象(但文件实际上存在)。
上传物品
现在我们已经成功将文件保存到 Redux 中,最后一步是将其上传到后端。
步骤1
首先,我们来创建UploadProgress用于显示文件上传进度的组件。这是我们想要的文件夹结构。
./src/components
../UploadProgress/
.../UploadProgress.js
.../UploadProgress.module.css
../UploadItem/
.../UploadItem.js
.../UploadItem.module.css
然后在App.js调用UploadProgress组件中:
...
...
import UploadProgress from './components/UploadProgress/UploadProgress'
...
...
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<input type="file" multiple onChange={handleAttachFIle} />
</header>
<UploadProgress /> // --> call the component here
</div>
)
...
现在在本地主机上运行当前行为,我们将看到上传进度组件正常工作。
步骤 2
现在我们应该创建一个函数,将文件上传到后端,同时增加上传进度,以便进度条递增。
// uploadFile.types.js
...
SET_UPLOAD_PROGRESS: 'SET_UPLOAD_PROGRESS',
SUCCESS_UPLOAD_FILE: 'SUCCESS_UPLOAD_FILE',
FAILURE_UPLOAD_FILE: 'FAILURE_UPLOAD_FILE',
...
// uploadFile.reducer.js
...
...
case uploadFileTypes.SET_UPLOAD_PROGRESS:
return {
...state,
fileProgress: {
...state.fileProgress,
[action.payload.id]: {
...state.fileProgress[action.payload.id],
progress: action.payload.progress,
},
},
}
case uploadFileTypes.SUCCESS_UPLOAD_FILE:
return {
...state,
fileProgress: {
...state.fileProgress,
[action.payload]: {
...state.fileProgress[action.payload],
status: 1,
},
},
}
case uploadFileTypes.FAILURE_UPLOAD_FILE:
return {
...state,
fileProgress: {
...state.fileProgress,
[action.payload]: {
...state.fileProgress[action.payload],
status: 0,
progress: 0,
},
},
}
...
...
// uploadFile.actions.js
...
...
export const setUploadProgress = (id, progress) => ({
type: uploadFileTypes.SET_UPLOAD_PROGRESS,
payload: {
id,
progress,
},
})
export const successUploadFile = id => ({
type: uploadFileTypes.SUCCESS_UPLOAD_FILE,
payload: id,
})
export const failureUploadFile = id => ({
type: uploadFileTypes.FAILURE_UPLOAD_FILE,
payload: id,
})
export const uploadFile = files => dispatch => {
if (files.length) {
files.forEach(async file => {
const formPayload = new FormData()
formPayload.append('file', file.file)
try {
await axios({
baseURL: 'http://localhost:5000',
url: '/file',
method: 'post',
data: formPayload,
onUploadProgress: progress => {
const { loaded, total } = progress
const percentageProgress = Math.floor((loaded/total) * 100)
dispatch(setUploadProgress(file.id, percentageProgress))
},
})
dispatch(successUploadFile(file.id))
} catch (error) {
dispatch(failureUploadFile(file.id))
}
})
}
}
这里无需过多解释:
uploadFile该函数将接收一个待上传到后端的文件数组。函数内部会循环遍历数组中的每个文件。每次循环,都会将文件添加到数组中FormData(这是我们通过 HTTP 向服务器发送文件数据类型的方式),然后使用 POST 方法将其发送到本地axios服务器。- Axios 会接收一个参数
onUploadProgress,用于订阅每次上传进度,我们希望在这里使用我们的函数来上传进度条(您可以在这里setUploadProgress阅读文档)。 - 如果成功,我们将分发;
successUploadFile如果失败,我们将分发。failureUploadFile
最后,我们在组件中UploadProgress.js像这样调用 uploadFile。
import React, { useEffect } from 'react'
...
...
const { fileProgress, uploadFile } = props
const uploadedFileAmount = size(fileProgress)
useEffect(() => {
const fileToUpload = toArray(fileProgress).filter(file => file.progress === 0)
uploadFile(fileToUpload)
}, [uploadedFileAmount])
...
...
const mapDispatchToProps = dispatch => ({
uploadFile: files => dispatch(uploadFile(files)),
})
export default connect(mapStateToProps, mapDispatchToProps)(UploadProgress)
UploadProgress该组件会监视每次uploadFileAmount使用情况的变化useEffect。因此,每次上传新文件(且文件进度为 0)时,它都会调用uploadFile函数并将其上传到后端。
现在让我们看看本地主机(别忘了也运行你的本地主机服务器)。
看,成功了!进度条不再是 0% 了,我们成功上传了多个文件,而且文件类型也多种多样(pdf、png、mp4)。
但这还没完,你发现吗?上传文件时,进度条好像没有增加,而是像故障一样从 0% 跳到 100%。这是怎么回事?🤔
现在,原因在这里有详细的解释,但我还是尝试简单概括一下。
问题在于,我们的前端和后端应用程序是在同一台机器上开发的(笔记本电脑上的本地主机),因此向后端发送数据不存在实时问题。但如果是在生产环境中,我们通常会将文件保存到云存储(例如:AWS S3),那么就需要一定的时间将文件从我们的服务器传输到 AWS 服务器,而进度条正是在这段时间内正常工作的。
不过别担心,我们实际上可以在浏览器中模拟这段时间,请查看下面的 GIF 动画了解如何操作。
好了!就到这里!本教程到此结束。如果你想查看完整的源代码,可以点击这里。
感谢各位耐心读完这篇文章。这是我的第一篇博客文章,如果有什么不妥之处或难以理解的地方,敬请谅解。我会努力写更多文章,力求精益求精。
我已经发布了第二篇与此相关的文章,内容是关于如何添加取消和重试功能。如果您感兴趣,可以去看看,链接在这里:如何使用 ReactJS 上传多个文件并实现取消和重试功能
祝你编程愉快!🎉🎉
文章来源:https://dev.to/devinekadeni/how-to-upload-multiple-file-with-progress-bar-reactjs-redux-and-expressjs-4hb3











