服务端使用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 脚本
{
"main": "server.js",
"script" : {
"start": "node server.js", //使用node启动express
"server": "nodemon server.js", //启动server服务器
"client": "npm start --prefix client", //启动客户端
"dev": " concurrently \"npm run server\" \"npm run client\" "同时启服务端和客户端,注意需要转义符
}
}
编写服务端
const express = require('express')
const fileUpload = require('express-fileupload')
const app = express()
//使用中间件
app.use(fileUpload())
//处理路由
app.post('/upload',(req,res)=>{
if(req.files === null){
return res.status(400).json({msg:'没有上传的文件'})//客户端错误:没有上传文件
}
// file 由后文中客户端方面的 formData.append('file', file) 的第一个参数定义 可自定义为其他名称
const file = req.files.file
//移动文件到第一参数指定位置 若有错误 返回500
file.mv(`${__dirname}/client/public/uploads/${file.name}`,(err)=>{
if(err){
console.log(err)
return res.status(500).send(err)//服务端错误:拿不到文件
}
res.json({
fileName:file.name,
filePath:`/upload/${file.name}`
})//在文件上传成功后,会根据文件在服务器上的位置显示上传后的文件
//在客户端的public 文件夹下创建uploads 文件夹 用于保存上传的文件
})
})
//监听端口号
app.listen(5000,()=>{
console.log('服务器正在5000端口号运行...')
})
编写客户端
npx create-react-app client 创建客户端
此时,客户端运行在3000端口号,服务端运行在5000端口号,端口不同产生跨域,需要在客户端package中设置proxy代理服务器,仅在开发环境中有效
"proxy":"https://localhost:5000" //代理服务器端口为本地服务器端口
编写组件
- 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
平常开发主要的使用是 append 方法,一般都会封装一层 request,调用层只需要传入参数的对象集合就可以
append or set?
append 的 key 存在,就会附加到已有值集合的后面,而 set 会使用新值覆盖已有的值,所以选择使用哪一种取决于你的需求
当我们传递 File 到 formatData 层,会直接被 append 到 FormData 对象里,且可以通过 get 获取到值,然后发送请求到服务端
import React, { Fragment, useState } from 'react'
import axios from 'axios'
import Message from './Message'
import Progress from './Progress'
export const FileUpload = () => {
const [file, setFile] = useState('');
const [filename, setFilename] = useState('选择文件');
const [uploadedFile, setUploadedFile] = useState({});
const [message, setMessage] = useState('')
const [uploadPercentage, setUploadPercentage] = useState(0);
const onChange = (e) => {
setFile(e.target.files[0]);
setFilename(e.target.files[0].name);
}
const onSubmit = async (e) => {
e.preventDefault();
var formData = new FormData();//实例化FormData对象
formData.append('file', file);//将文件添加到formData层,可自定义命名file
//给服务端发送请求post,同时发送file到服务端
try {
const res = await axios.post('/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: progressEvent => {
setUploadPercentage(parseInt(Math.round((progressEvent.loaded * 100) / progressEvent.total)));
setTimeout(() => setUploadPercentage(0), 5000);
}
});
const { fileName, filePath } = res.data;
setUploadedFile({ fileName, filePath });
setMessage('上传成功')
} catch (error) {
if (error.response.status === 500) {
setMessage('服务器存在问题...')
} else {
setMessage(error.response.data.msg);
}
}
}
return (
<Fragment>
{message ? <Message msg={message}/> : null}
<form onSubmit={onSubmit}>
<div className="custom-file">
<input type="file" className="custom-file-input" id="customFile" onChange={onChange}/>
<label className="custom-file-label" htmlFor="customFile">{ filename }</label>
</div>
<Progress percentage={uploadPercentage} />
<input type="submit" value="上传文件" className="btn btn-primary btn-block mt-4"/>
</form>
{uploadedFile ? (
<div className="row mt-5">
<div className="col-md-6 m-auto">
<h3 className="text-center">{uploadedFile.fileName}</h3>
<img src={uploadedFile.filePath} alt='' style={{width:'100%'}}/>
</div>
</div>
) :null}
</Fragment>
)
}
Message.js提示框
propTypes类型检查
react 内置的一些类型检查功能 ,在组件的 props 上进行类型检查
import PropTypes from 'prop-types';
class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}
Greeting.propTypes = {
name: PropTypes.string //设置 name 属性类型为 string 字符串
};
当传入的 prop 值类型不正确时,页面能正常显示,但JavaScript 控制台将会显示警告。出于性能方面的考虑,propTypes 仅在开发模式下进行检查。
import React from 'react'
import PropTypes from 'prop-types'
function Message({msg}) {
return (
<div className="alert alert-warning alert-dismissible fade show" role="alert">
{msg}
<button type="button" className="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
)
}
Message.propTypes = {
msg:PropTypes.string.isRequired,
}
export default Message
Progress.js进度条
axios的onUploadProgress上传事件
- 进度条的数据存储,使用useState
- post请求的第三个参数中添加onUploadProgress函数
- onUploadProgress函数中接收两个属性:loaded、total(已 和 总)
在onUploadProgress函数中调用 修改进度条数据函数,通过数据变动 修改 进度条宽度样式
...
const [uploadPercentage, setUploadPercentage] = useState(0);
const res = await axios.post('/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: progressEvent => {
//设置进度条:通过改变state数据,来改变样式
setUploadPercentage(parseInt(Math.round((progressEvent.loaded * 100) / progressEvent.total)));
setTimeout(() => setUploadPercentage(0), 5000);
}
}
)
import React from 'react'
import PropTypes from 'prop-types'
const Progress = ({percentage}) => {
return (
<div className="progress mt-3">
<div className="progress-bar progress-bar-striped bg-warning" role="progressbar" style={{ width: `${percentage}%` }} >{ percentage}%</div>
</div>
)
}
Progress.propTypes = {
percentage:PropTypes.number.isRequired,
}
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控制,需要通过传参到自定义组件内部,在组件内部的元素上添加内联样式即可实现预期效果
总结方法:
- 在jsx中使用JS控制自定义组件的显示隐藏
{show?<Progress percentage={uploadPercentage} />}:null}
- 传参+自定义组件内部的元素使用CSS控制显示隐藏
<Progress percentage={uploadPercentage} show={show}/>