书接上回, 讲到 "使用同一个新增弹框" 中有未解决的问题, 比如复杂的字段, 文件, 图片上传, 这一篇就解决文件上传的问题. 这里的场景是在新增弹出框中要上传一个图片, 并且这个上传组件放在一个 Form 中, 和其他文本字段一起提交给接口.
这里就有几个要注意的问题:
图片上传时最好能在前端指定图片类型, 根据这个类型上传到指定的目录. 比如这里是新增用户, 上传用户图片, 那么这里就指定类型是 "user", 那么就把这个文件上传到服务器的 upload/user 目录中. 这样方便后期维护, 比如要把项目中的文件统一迁移到另外一个服务器, 只要把 upload 目录复制出来就好了.
上传组件是通用的, 上传完之后回传给前端一个路径信息, 由于使用的是 and design 中的 Form, 这时要把这个路径赛到 form 的数据中一并提交给新增接口.
1. 后端上传文件接口
1.1 使用 multer
前面在写新增数据, 请求数据的时候使用的到中间件 bodyParser, 解析客户端请求的时候, 使用的 JSON 类型接受数据, 这个很方便, 但是上传文件的时候是一般是 multipart/form-data 这种类型, bodyParser 不能解析这种类型. 于是这里引入另外一种中间件 https://github.com/expressjs/multer .multer 专门处理 multipart/form-data 类型的表单数据, 专业的.
multer 有两种使用方式, 如果只是一般的网页应用, 直接指定 dest, 也就是上传路径就可以了. 如果上传时进行更多的控制, 可以使用 storage 选项. 这里我从简单的入手, 直接指定文件路径上传一个文件.
- // 指定文件上传路径
- var upload = multer({
- dest: path.join(__dirname, './../public/upload/tmp')
- });
这里使用到 node.JS 中的 path 模块, 将./../public/upload/tmp 这个相对路径转换成计算机本地路径, 注意这里我们在 express 项目的 public 目录下新建了 upload/tmp 目录, 至于为啥是 tmp 这样的临时文件夹, 请继续往下看.
接着定义上传接口:
- router.post('/singleFile', upload.single('file'), function (req, res, next) {
- })
这里我们定义了一个 API/base/singleFile 接口, 接受 Form 中一个名叫 file 的上传文件标签, 这样定义之后就可以吧文件上传到 public/upload/tmp 目录下.
1.2 指定上传目录
multer 这种指定路径上传的方式是一开始就指定好了, 后面都上传到这个目录, 就是说这个目录不能是一个变量, 那如何能够根据前端传过来的参数将图片上传到指定的目录呢? 我这里首先想到的就是 "剪切" 文件. 既然用的是 node.JS, 文件操作的 API 就少不了剪切文件了. 还有官方文档上说明了, 回调函数中除了文件之外, 还可以有 req.body, 如果有文本域数据, 将在这个 req.body 中, 这个和 bodyParser 是类似的.
- App.post('/profile', upload.single('avatar'), function (req, res, next) {
- // req.file 是 `avatar` 文件的信息
- // req.body 将具有文本域数据, 如果存在的话
- })
有了 req.file,req.body 这两个对象之后剩下的工作就交给 node.JS 了, 代码如下:
- // 文件上传
- router.post('/singleFile', upload.single('file'), function (req, res, next) {
- if(req.body.fileLocation) {
- const newName = req.file.path.replace(/\\tmp/, '\\' + req.body.fileLocation) + path.parse(req.file.originalname).ext
- fs.rename(req.file.path, newName, err => {
- if (err) {
- res.JSON(result.createResult(false, { message: err.message }))
- } else {
- let fileName = newName.split('\\').pop()
- res.JSON(result.createResult(true, { path: `${req.body.fileLocation}/${fileName}` }))
- }
- })
- } else {
- res.JSON(result.createResult(false, {message: '未指定文件路径'}))
- }
- })
注意在这里还使用了 fs 模块的 rename 方法, 这个方法可以将文件重命名并修改文件路径, 就是剪切文件了. 这里用 replace 方法把 tmp 目录替换成前端传过来的 fileLocalhost, 然后将文件移动到这个 fileLocation 目录中. 下面使用 postman 来 debug 跟踪一下执行过程:
postman 请求:
上传到 tmp 目录:
移动到指定的 user 目录:
postman 返回:
至此, 接口就写好了, 下面就是在前端调用这个接口.
2. 前端 Form 里调用接口
2.1 定义字段类型
在上一篇 node.JS+react 全栈实践 - 开篇中, 使用的是统一的数据添加组件来添加, 数据. columns.JS 中未指定字段类型, 都是文本框, 这显然不切合实际, 在这里再加上一个属性 type:file 表示在添加数据组件中, 这个字段对应一个上传文件组件. 另外, 如果对文件类型, 大小有限制, 这里也可以添加 accept,size 字段. 代码如下:
const thumb = { title: '头像', dataIndex: 'thumb', key: 'thumb', render: src => <img className={style.tableImg} alt=''src={ `${config.baseUrl.resource.upload}${src}` }/>, type:'file', accept:'image/gif,image/jpeg', size: 2 }
2.2 Upload 上传组件
剩下的就要研究一下 ant design 中的 Upload 组件, 看一下文档就明白了. 关键代码如下:
- {field.map((f, index) => {
- switch (f.type) {
- case 'file':
- return <FormItem
- name='file'
- headers={headers}
- key={f.key}
- label={f.title}>
- {getFieldDecorator(f.key)(<div>
- <Upload
- name="file"
- accept={f.accept}
- data={data}
- listType="picture-card"
- showUploadList={false}
- action="http://localhost:3332/api/base/singleFile"
- beforeUpload={this.beforeFileUpload.bind(this, f)}
- onChange={this.handleFileChange.bind(this, f)}>
- {imageURL ? <img src={imageURL} alt="avatar" style={{ width: '100%' }} /> : uploadButton}
- </Upload>
- </div>)}
- </FormItem>
- default:
- return <FormItem key={f.key} label={f.title}>
- {getFieldDecorator(f.key, { rules: [{ validator: this.customerValidator.bind(this, f) }] })(<Input placeholder={'请输入' + f.title}/>)}
- </FormItem>
- }
- })}
name: 这个是字段名字, 如果是要调用 API/base/singleFile 这个接口, 就要设置为 file, 和上面的 upload.single('file')是对应起来的
accept: 接受的文件类型, 从 columns.JS 中 thumb 字段中获取, 也可以在 beforUpload 回调中验证类型
data: 这个就是除了文件之外额外的参数, 可以指定为 {fileLocation: 'user'} 表示要上传到 user 子目录, 这里要赞美一下 ant design, 已经考虑了额外参数
listType: 显示样式, 参考 antd design 文档, 不解释
showUploadList: 同上, 不解释
action: 上传文件接口, 注意这里要使用本地 API 文件中定义的接口, 不能使用服务端的接口路径, 否则会代理失败的
beforUpload: 上传文件之前的钩子, 这里要赞美一下 ant design, 可以额外传一个参数 f, 带入字段信息, 这样就可以获取字段的 accept,size 信息, 进行验证
onChange: 文件状态改变时的钩子, 继续赞美一下 ant design, 同上, 可以额外传递一个参数
这里有一个小疑问: antd design 中解释 onChange:"上传中, 完成, 失败都会调用这个函数", 我测试了一下, 确实会调用三次, 但是有两次都返回了 response,status 都是 done, 和我想象的不一样. 这上传成功了, 按说有上传中, 完成个回调, 那都是 done 是怎么回事,"完成" 调用了两次?
onChang 回调:
到这里, 接口已调通, 文件已经能够成功的从前端传到后端了.
2.3 Form 获取文件路径
最后一个问题, 这里使用 Form 组件填充, 收集数据, Form 中上传组件是单独的跑起来的, 最后得到的是一个 url, 不是文件本身, 如何将这个 url 给到 form 中呢? 这里使用的是 form.setFieldsValue({name: value})这个方法, 简答粗暴. 代码如下:
- handleFileChange(field, info) {
- let file = info.file
- if (file.response && file.response.success && file.response.data && file.response.data.path) {
- let { upload } = this.state
- upload.imageURL = `${config.baseUrl.resource.upload}${file.response.data.path}`
- // 为 Form 对应的字段设置值
- this.props.form.setFieldsValue({ [`${field.key}`]: file.response.data.path })
- this.setState({ upload })
- upload.loading = false
- }
- }
注意这里 FormItem 是动态加载出来的, 并不知道是那个字段, 所以 onChange 回调中额外传递了参数 f, 这样, setFildsValue 中就知道这是要设置 Form 中哪一个数据.
最后看一下效果:
上传文件:
数据表:
未解决的问题:
1. 上传过程中如果因为其他问题导致失败, 并且是在转移之前失败, 服务器上 upload/tmp 目录会有很多的垃圾文件, 这里可以在转移之后把 tmp 目录中的文件全部删掉
2. 文件的校验是放在 beforUpdate 钩子里通过全局提示 message.error 弹出, 这个是不是可以放在 getFieldDecorator 的 rules 里面, 体验会更好
来源: https://www.cnblogs.com/tylerdonet/p/12021663.html