发布于 2026-01-06 19 阅读
0

如何使用进度条上传多个文件(ReactJS + Redux 和 ExpressJS)入门设置服务器和 API 设置前端上传项目 DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

如何使用进度条上传多个文件(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


Enter fullscreen mode Exit fullscreen mode

 

设置服务器和 API

首先,我们需要安装后端的所有依赖项。



$ cd server
$ touch server.js            // creating new file
$ npm init -y                // creating default package.json file
$ npm i express multer cors


Enter fullscreen mode Exit fullscreen mode

我将直接展示server.js代码,因为我们将更侧重于前端部分,以下是代码:

服务器代码

./file-upload-example/server/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


Enter fullscreen mode Exit fullscreen mode

现在你的 React 应用将在 localhost:3000 的新浏览器标签页中打开。太棒了,让我们开始添加内容吧!首先,我们将修改我们的App.js

App.js

client/src/App.js

这样一来,我们就添加了一个输入按钮,当我们上传文件时,它会显示console.log正在上传的文件。

现在我们来配置 Redux。
其思路是,每次我们上传文件时,这些文件都会以特定的数据结构存储到 Redux store 中。
首先,我们创建一个新文件夹,redux并在其中创建一个文件(目前为空),如下所示:

redux 文件夹

Redux文件夹结构

 



//uploadFile.types.js

const uploadFileTypes = {
  SET_UPLOAD_FILE: 'SET_UPLOAD_FILE',
}

export default uploadFileTypes


Enter fullscreen mode Exit fullscreen mode


//uploadFile.actions.js

import uploadFileTypes from './uploadFile.types'

export const setUploadFile = data => ({
  type: uploadFileTypes.SET_UPLOAD_FILE,
  payload: data,
})


Enter fullscreen mode Exit fullscreen mode


// 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


Enter fullscreen mode Exit fullscreen mode

我们稍后会定义这些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
}


Enter fullscreen mode Exit fullscreen mode

这个工具函数会将传入的文件修改为一个对象,最后将每个文件对象填充为与注释中的数据结构相同的内容INITIAL_STATE(正如我们之前提到的)。

现在为了测试它,我们应该将这个 Redux 应用到我们的应用程序中,让我们开始吧。



// root-reducer.js

import { combineReducers } from 'redux'
import UploadFile from './uploadFile/uploadFile.reducer'

const rootReducer = combineReducers({
  UploadFile,
})

export default rootReducer


Enter fullscreen mode Exit fullscreen mode

现在在src/index.js
src/index.js

src/index.js

别忘了使用setUploadFile上传按钮App.js

src/App.js

src/App.js

现在该检查一下本地主机了,其行为应该与此类似。

第一个结果

如上图所示,我们可以追踪上传到 Redux store 的文件。有些人可能会有以下两个疑问:第一,为什么我们看不到任何文件?第二,为什么 Redux store 中`on`console.log的值是一个空对象,而不是文件数据? 让我们逐一探讨。filefileProgress

  1. console.log因为在将文件保存到 Redux store 后,我们直接将输入元素的值赋给了变量。现在页面什么也没显示'' (e.target.value = '')。我们需要清除这个input值,以便之后可以上传另一个文件。
  2. 现在我们可以跟踪 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


Enter fullscreen mode Exit fullscreen mode

 
上传进度

src/components/UploadProgress - (.js 和 .css)

 
上传项

src/components/UploadItem - (.js 和 .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>
)
...


Enter fullscreen mode Exit fullscreen mode

 
现在在本地主机上运行当前行为,我们将看到上传进度组件正常工作。

第二个结果

使用 UploadProgress 组件上传项目 - 步骤 1

步骤 2

现在我们应该创建一个函数,将文件上传到后端,同时增加上传进度,以便进度条递增。



// uploadFile.types.js

...
SET_UPLOAD_PROGRESS: 'SET_UPLOAD_PROGRESS',
SUCCESS_UPLOAD_FILE: 'SUCCESS_UPLOAD_FILE',
FAILURE_UPLOAD_FILE: 'FAILURE_UPLOAD_FILE',
...


Enter fullscreen mode Exit fullscreen mode


// 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,
      },
    },
  }
...
...


Enter fullscreen mode Exit fullscreen mode


// 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))
      }
    })
  }
}


Enter fullscreen mode Exit fullscreen mode

这里无需过多解释:

  • 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)


Enter fullscreen mode Exit fullscreen mode

UploadProgress该组件会监视每次uploadFileAmount使用情况的变化useEffect 。因此,每次上传新文件(且文件进度为 0)时,它都会调用uploadFile函数并将其上传到后端。

现在让我们看看本地主机(别忘了也运行你的本地主机服务器)

第三个结果

文件上传进度条 - 步骤 2

看,成功了!进度条不再是 0% 了,我们成功上传了多个文件,而且文件类型也多种多样(pdf、png、mp4)。
但这还没完,你发现吗?上传文件时,进度条好像没有增加,而是像故障一样从 0% 跳到 100%。这是怎么回事?🤔

现在,原因在这里有详细的解释,但我还是尝试简单概括一下。
问题在于,我们的前端和后端应用程序是在同一台机器上开发的(笔记本电脑上的本地主机),因此向后端发送数据不存在实时问题。但如果是在生产环境中,我们通常会将文件保存到云存储(例如:AWS S3),那么就需要一定的时间将文件从我们的服务器传输到 AWS 服务器,而进度条正是在这段时间内正常工作的。

不过别担心,我们实际上可以在浏览器中模拟这段时间,请查看下面的 GIF 动画了解如何操作。

网络解决方案

将网络设置为慢速 3G 模式以实现进度增量

好了!就到这里!本教程到此结束。如果你想查看完整的源代码,可以点击这里


感谢各位耐心读完这篇文章。这是我的第一篇博客文章,如果有什么不妥之处或难以理解的地方,敬请谅解。我会努力写更多文章,力求精益求精。

我已经发布了第二篇与此相关的文章,内容是关于如何添加取消和重试功能。如果您感兴趣,可以去看看,链接在这里:如何使用 ReactJS 上传多个文件并实现取消和重试功能

祝你编程愉快!🎉🎉

文章来源:https://dev.to/devinekadeni/how-to-upload-multiple-file-with-progress-bar-reactjs-redux-and-expressjs-4hb3