服务端使用express,使用express-fileupload库提供的中间件函数来接受从客户端传来的图片,并将图片作为文件存储在服务端。
客户端使用create-react-app框架,bootstrap UI,axios发送http请求和提供进度条当前进度的值,上传成功后,根据图片在服务端上的位置,显示图片。

初始化项目

配置环境
npm init -y // 初始化 npm 创建 package.json
npm i express express-fileupload
npm i -D nodemon
npm i -D concurrently // 可以并行同时运行客户端和服务端(在本机进行测试,就不用频繁进行前后端切换)
更改 react-file-upload/package.json 中的 scripts 脚本

  1. {
  2. "main": "server.js",
  3. "script" : {
  4. "start": "node server.js", //使用node启动express
  5. "server": "nodemon server.js", //启动server服务器
  6. "client": "npm start --prefix client", //启动客户端
  7. "dev": " concurrently \"npm run server\" \"npm run client\" "同时启服务端和客户端,注意需要转义符
  8. }
  9. }

编写服务端

  1. const express = require('express')
  2. const fileUpload = require('express-fileupload')
  3. const app = express()
  4. //使用中间件
  5. app.use(fileUpload())
  6. //处理路由
  7. app.post('/upload',(req,res)=>{
  8. if(req.files === null){
  9. return res.status(400).json({msg:'没有上传的文件'})//客户端错误:没有上传文件
  10. }
  11. // file 由后文中客户端方面的 formData.append('file', file) 的第一个参数定义 可自定义为其他名称
  12. const file = req.files.file
  13. //移动文件到第一参数指定位置 若有错误 返回500
  14. file.mv(`${__dirname}/client/public/uploads/${file.name}`,(err)=>{
  15. if(err){
  16. console.log(err)
  17. return res.status(500).send(err)//服务端错误:拿不到文件
  18. }
  19. res.json({
  20. fileName:file.name,
  21. filePath:`/upload/${file.name}`
  22. })//在文件上传成功后,会根据文件在服务器上的位置显示上传后的文件
  23. //在客户端的public 文件夹下创建uploads 文件夹 用于保存上传的文件
  24. })
  25. })
  26. //监听端口号
  27. app.listen(5000,()=>{
  28. console.log('服务器正在5000端口号运行...')
  29. })

编写客户端

npx create-react-app client 创建客户端
此时,客户端运行在3000端口号,服务端运行在5000端口号,端口不同产生跨域,需要在客户端package中设置proxy代理服务器,仅在开发环境中有效

  1. "proxy":"https://localhost:5000" //代理服务器端口为本地服务器端口

yarn add axios 安装axios

编写组件

  • FileUpload.js 上传组件:用form标签的onSubmit和axios发送上传请求
  • Message.js 提示框:显示信息 上传成功 服务器错误 或 没有选择文件
  • Progress.js 进度条:用axios的onUploadProgress和bootstrap显示上传进度条

    FileUpload.js上传组件

    FormData使用
    H5新增API,专门用于文件上传

    很多时候,在 post 提交数据时我们常采用 application/json、application/x-www-form-urlencoded 等类型,也确实能够覆盖到大部分的场景,但是有一些场景下,比如文件上传的时候,就不算是好的解决方案了

它提供了一种表示表单数据的键值对 key/value 的构造方式,适用于表单enctype属性设为multipart/form-data
image.png
平常开发主要的使用是 append 方法,一般都会封装一层 request,调用层只需要传入参数的对象集合就可以

append or set?

append 的 key 存在,就会附加到已有值集合的后面,而 set 会使用新值覆盖已有的值,所以选择使用哪一种取决于你的需求
当我们传递 File 到 formatData 层,会直接被 append 到 FormData 对象里,且可以通过 get 获取到值,然后发送请求到服务端

  1. import React, { Fragment, useState } from 'react'
  2. import axios from 'axios'
  3. import Message from './Message'
  4. import Progress from './Progress'
  5. export const FileUpload = () => {
  6. const [file, setFile] = useState('');
  7. const [filename, setFilename] = useState('选择文件');
  8. const [uploadedFile, setUploadedFile] = useState({});
  9. const [message, setMessage] = useState('')
  10. const [uploadPercentage, setUploadPercentage] = useState(0);
  11. const onChange = (e) => {
  12. setFile(e.target.files[0]);
  13. setFilename(e.target.files[0].name);
  14. }
  15. const onSubmit = async (e) => {
  16. e.preventDefault();
  17. var formData = new FormData();//实例化FormData对象
  18. formData.append('file', file);//将文件添加到formData层,可自定义命名file
  19. //给服务端发送请求post,同时发送file到服务端
  20. try {
  21. const res = await axios.post('/upload', formData, {
  22. headers: { 'Content-Type': 'multipart/form-data' },
  23. onUploadProgress: progressEvent => {
  24. setUploadPercentage(parseInt(Math.round((progressEvent.loaded * 100) / progressEvent.total)));
  25. setTimeout(() => setUploadPercentage(0), 5000);
  26. }
  27. });
  28. const { fileName, filePath } = res.data;
  29. setUploadedFile({ fileName, filePath });
  30. setMessage('上传成功')
  31. } catch (error) {
  32. if (error.response.status === 500) {
  33. setMessage('服务器存在问题...')
  34. } else {
  35. setMessage(error.response.data.msg);
  36. }
  37. }
  38. }
  39. return (
  40. <Fragment>
  41. {message ? <Message msg={message}/> : null}
  42. <form onSubmit={onSubmit}>
  43. <div className="custom-file">
  44. <input type="file" className="custom-file-input" id="customFile" onChange={onChange}/>
  45. <label className="custom-file-label" htmlFor="customFile">{ filename }</label>
  46. </div>
  47. <Progress percentage={uploadPercentage} />
  48. <input type="submit" value="上传文件" className="btn btn-primary btn-block mt-4"/>
  49. </form>
  50. {uploadedFile ? (
  51. <div className="row mt-5">
  52. <div className="col-md-6 m-auto">
  53. <h3 className="text-center">{uploadedFile.fileName}</h3>
  54. <img src={uploadedFile.filePath} alt='' style={{width:'100%'}}/>
  55. </div>
  56. </div>
  57. ) :null}
  58. </Fragment>
  59. )
  60. }

Message.js提示框

提交成功、提交失败
请先选择对应文件

propTypes类型检查

react 内置的一些类型检查功能 ,在组件的 props 上进行类型检查

  1. import PropTypes from 'prop-types';
  2. class Greeting extends React.Component {
  3. render() {
  4. return (
  5. <h1>Hello, {this.props.name}</h1>
  6. );
  7. }
  8. }
  9. Greeting.propTypes = {
  10. name: PropTypes.string //设置 name 属性类型为 string 字符串
  11. };

当传入的 prop 值类型不正确时,页面能正常显示,但JavaScript 控制台将会显示警告。出于性能方面的考虑,propTypes 仅在开发模式下进行检查。

  1. import React from 'react'
  2. import PropTypes from 'prop-types'
  3. function Message({msg}) {
  4. return (
  5. <div className="alert alert-warning alert-dismissible fade show" role="alert">
  6. {msg}
  7. <button type="button" className="close" data-dismiss="alert" aria-label="Close">
  8. <span aria-hidden="true">&times;</span>
  9. </button>
  10. </div>
  11. )
  12. }
  13. Message.propTypes = {
  14. msg:PropTypes.string.isRequired,
  15. }
  16. export default Message

Progress.js进度条

显示上传文件的进度条

axios的onUploadProgress上传事件
  1. 进度条的数据存储,使用useState
  2. post请求的第三个参数中添加onUploadProgress函数
  3. onUploadProgress函数中接收两个属性:loaded、total(已 和 总)
  4. 在onUploadProgress函数中调用 修改进度条数据函数,通过数据变动 修改 进度条宽度样式

    1. ...
    2. const [uploadPercentage, setUploadPercentage] = useState(0);
    3. const res = await axios.post('/upload', formData, {
    4. headers: { 'Content-Type': 'multipart/form-data' },
    5. onUploadProgress: progressEvent => {
    6. //设置进度条:通过改变state数据,来改变样式
    7. setUploadPercentage(parseInt(Math.round((progressEvent.loaded * 100) / progressEvent.total)));
    8. setTimeout(() => setUploadPercentage(0), 5000);
    9. }
    10. }
    11. )
  1. import React from 'react'
  2. import PropTypes from 'prop-types'
  3. const Progress = ({percentage}) => {
  4. return (
  5. <div className="progress mt-3">
  6. <div className="progress-bar progress-bar-striped bg-warning" role="progressbar" style={{ width: `${percentage}%` }} >{ percentage}%</div>
  7. </div>
  8. )
  9. }
  10. Progress.propTypes = {
  11. percentage:PropTypes.number.isRequired,
  12. }
  13. export default Progress

组件显示与隐藏的难题

问题出现:
在点击【上传文件】按钮后,无论是否上传成功,进度条组件均会加载并且停留在100%,不符合实际场景

提出需求:
我的预期是在点击【上传文件】按钮后—》上传成功、进度条加载完成后/上传失败——》进度条组件隐藏

做法思路:

  • 我的方案是在FileUpload.js组件中,先初始化变量控制Progress组件显示与隐藏

const [show, setShow] = useState(true);

  • 然后在post请求服务端响应成功后修改变量

setShow(false);

  • 最后将变量挂在Progress组件上的内联样式中

即用css控制显示与隐藏<Progress style={{display: show ? 'block':'none'}} percentage={uploadPercentage} />

问题出现:
之前问题仍然存在,进度条停留100%

排查原因:
服务端server.js在接收到post请求文件后,将文件指定到client目录下的路径,而client目录由cra的webpack管控,webpack监听到client目录做了修改便重新加载组件,导致初始化变量true无法修改为false控制显示隐藏,因为变量一直被重置为true

推倒原因:
由于我一开始就将初始变量设置为false,进度条组件在进入页面后仍然存在,故不是webpack重新渲染组件的原因

排查原因:
在组件入口处打log确定组件是否被多次渲染重置变量,发现打出多个log,询问得知state变了组件会重新渲染,到那时react会diff,不会完全更新整个DOM,所以这个log打在组件入口是不准确的,要检测组件是否被整个重新挂载,需要放在useEffect中

推倒原因:
使用浏览器检查元素发现Progress组件始终没有内联样式display,故可能是jsx中对于自定义组件的显示隐藏用css写法错误,因为react中自定义组件本质上是函数,函数的显示隐藏不能直接用css控制,需要通过传参到自定义组件内部,在组件内部的元素上添加内联样式即可实现预期效果

总结方法:

  1. 在jsx中使用JS控制自定义组件的显示隐藏{show?<Progress percentage={uploadPercentage} />}:null}
  2. 传参+自定义组件内部的元素使用CSS控制显示隐藏<Progress percentage={uploadPercentage} show={show}/>