20220413_有赞表单组件van-uploader文件上传前后台学习笔记
1概述
van-uploader用于将本地的图片或文件上传至服务器,并在上传过程中展示预览图和上传进度。目前 Uploader 组件不包含将文件上传至服务器的接口逻辑,该步骤需要自行实现。
目前 Chrome、Safari 等浏览器不支持展示 HEIC/HEIF 格式的图片,因此上传后无法在 Uploader 组件中进行预览。
文件上传的核心点:构建FormData,将文件以表单的形式发送出去。
本文主要基于该组件进行全流程功能实现。
1.1van-uploader
1.1.1API
Props
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
v-model (fileList) | 已上传的文件列表 | FileListItem[] | - |
accept | 允许上传的文件类型,详细说明 | string | image/* |
name | 标识符,可以在回调函数的第二项参数中获取 | number | string | - |
preview-size | 预览图和上传区域的尺寸,默认单位为 px
|
number | string | 80px |
preview-image | 是否在上传完成后展示预览图 | boolean | true |
preview-full-image | 是否在点击预览图后展示全屏图片预览 | boolean | true |
preview-options v2.9.3
|
全屏图片预览的配置项,可选值见 ImagePreview | object | - |
multiple | 是否开启图片多选,部分安卓机型不支持 | boolean | false |
disabled | 是否禁用文件上传 | boolean | false |
readonly v2.12.26
|
是否将上传区域设置为只读状态 | boolean | false |
deletable | 是否展示删除按钮 | boolean | true |
show-upload v2.5.6
|
是否展示上传区域 | boolean | true |
lazy-load v2.6.2
|
是否开启图片懒加载,须配合 Lazyload 组件使用 | boolean | false |
capture | 图片选取模式,可选值为 camera (直接调起摄像头) |
string | - |
after-read | 文件读取完成后的回调函数 | Function | - |
before-read | 文件读取前的回调函数,返回 false 可终止文件读取, 支持返回 Promise
|
Function | - |
before-delete | 文件删除前的回调函数,返回 false 可终止文件读取, 支持返回 Promise
|
Function | - |
max-size v2.12.20
|
文件大小限制,单位为 byte
|
number | string | (file: File) => boolean | - |
max-count | 文件上传数量限制 | number | string | - |
result-type | 文件读取结果类型,可选值为 file text
|
string | dataUrl |
upload-text | 上传区域文字提示 | string | - |
image-fit | 预览图裁剪模式,可选值见 Image 组件 | string | cover |
upload-icon v2.5.4
|
上传区域图标名称或图片链接 | string | photograph |
注意:accept、capture 和 multiple 为浏览器 input 标签的原生属性,移动端各种机型对这些属性的支持程度有所差异,因此在不同机型和 WebView 下可能出现一些兼容性问题。
1.1.2事件
1.1.3回调参数
before-read、after-read、before-delete 执行时会传递以下回调参数:
参数名 | 说明 | 类型 |
---|---|---|
file | file 对象 | object |
detail | 额外信息,包含 name 和 index 字段 | object |
1.1.3.1file对象格式
file具有的属性如下:
content:文件对应的base64编码
file:文件元信息对象
message:
status:
单个文件格式:
多个文件格式:
1.4桌面端适配(桌面预览鼠标点击无法关闭,好人)
Vant 是一个面向移动端的组件库,因此默认只适配了移动端设备,这意味着组件只监听了移动端的 touch
事件,没有监听桌面端的 mouse
事件。
如果你需要在桌面端使用 Vant,可以引入我们提供的 @vant/touch-emulator,这个库会在桌面端自动将 mouse
事件转换成对应的 touch
事件,使得组件能够在桌面端使
https://youzan.github.io/vant/v2/#/zh-CN/advanced-usage#zhuo-mian-duan-gua-pei
# 安装模块
npm i @vant/touch-emulator -S
// 引入模块后自动生效
import '@vant/touch-emulator';
2代码示例
2.1前端请求
2.1.1注册Vant(main.js)
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
// 支持一次性导入所有组件,引入所有组件会增加代码包体积,但比较爽
// 因此不推荐这种做法
// 1.引入 Vant组件
import Vant from 'vant'
// 2.引入 Vant样式
import 'vant/lib/index.css'
// 3.引入拦截器
import './config/httpinterceptor'
Vue.config.productionTip = false
// 3.注册 Vant
Vue.use(Vant)
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
// 通过main.js将App.vue渲染到index.html的指定区域中。
2.1.2导入业务组件(main.js)
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
// 支持一次性导入所有组件,引入所有组件会增加代码包体积,但比较爽
// 因此不推荐这种做法
// 1.引入 Vant组件
import Vant from 'vant'
// 2.引入 Vant样式
import 'vant/lib/index.css'
import './config/httpinterceptor'
Vue.config.productionTip = false
// 3.注册 Vant
Vue.use(Vant)
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
// 通过main.js将App.vue渲染到index.html的指定区域中。
2.1.3httpinterceptor拦截器
// 接口请求拦截器
import Vue from 'vue'
import VueResource from 'vue-resource'
Vue.use(VueResource)
// 接口请求拦截器
Vue.http.interceptors.push((request, next) => {
debugger
// 1.credentials
// request.credentials = true;
// 2.Content_Type
// 'multipart/form-data'
// request.headers.get('Content-Type')
// url:/server/paging -->/api/server/paging
// 3.判断方法的类型
// if(/^post$/i.test(request.method))
console.log(request.url)
// 每个接口请求拦截前加前缀: /api,这样可以走同一的后台代理
request.url = '/api' + request.url
next((response) => {
// 在响应之后传给then之前对response进行修改和逻辑判断。
// 对于token时候已过期的判断,就添加在此处,
// 页面中任何一次 http请求都会先调用此处方法
if (response.status === 401) {
console.log('response.body:', response.body)
window.location.reload()
}
return response
})
})
2.1.4编写业务组件
2.1.4.1编写模板
<!--1.模板-->
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h2>Vant</h2>
<ul>
<li>
<!--最大不能超过1M=1024Kb=1024*1024b-->
<!--1kb=1024b-->
<van-uploader :after-read="afterUploader" :max-size="1024 * 1024" icon="plus"
type="primary"
v-model="uploadFileList"
:max-count="1"
:before-delete="beforeDelete"
></van-uploader>
<!--<van-uploader :after-read="afterUploader" :max-size="1024 * 1024" icon="plus"-->
<!--type="primary" multiple></van-uploader>-->
</li>
</ul>
<!--<ul>-->
<!--<li>-->
<!--<van-uploader :after-read="afterUploader" :max-size="1024 * 1024" icon="plus"-->
<!--type="primary" multiple>-->
<!--<van-button icon="plus" type="primary">上传文件</van-button>-->
<!--</van-uploader>-->
<!--</li>-->
<!--</ul>-->
</div>
</template>
2.1.4.2编写行为
<!--2.行为-->
<script>
import Vue from 'vue'
import {Dialog, Toast, Uploader} from 'vant'
Vue.use(Uploader)
export default {
name: 'HelloVant',
data () {
return {
msg: 'Welcome to Your Vant.js App',
show: false,
checked: false,
// 已上传的文件列表
// 数组中设置单个预览图片属性
// 展示文件列表的预览图
uploadFileList: [
// {
// url: 'https://img01.yzcdn.cn/vant/leaf.jpg',
// deletable: true,
// imageFit: 'contain',
// previewSize: 200
// }
],
uploadToServerFileList: []
}
},
methods: {
showPopup () {
this.show = true
},
showToast () {
Toast('hello,kikop')
},
showDialog () {
Dialog.alert({
title: '标题',
message: '弹窗内容'
}).then(() => {
// on close
})
},
// 压缩图片
compressToDataUrl (img) {
let url = ''
var w = Math.min(700, img.width)// 当图片像素>700的时候,等比例压缩,这个数字可以调
var h = img.height * (w / img.width)
var canvas = document.createElement('canvas')
var ctx = canvas.getContext('2d')
canvas.width = w
canvas.height = h
ctx.drawImage(img, 0, 0, w, h)
url = canvas.toDataURL('image/png', 1)// 1代表精细度,越高越好
return url
},
// base64转 Blob
dataURLtoBlob (dataurl) {
let arr = dataurl.split(',')
let mime = arr[0].match(/:(.*?);/)[1]
let bstr = atob(arr[1])
let n = bstr.length
let u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new Blob([u8arr], {type: mime})
},
afterUploaderForCompress: function (file) {
// 此时可以自行将文件上传至服务器
console.log(file)
// 1.创建 Imgage对象,这个 img就是传给上面的compress
let img = new Image()
img.src = file.content
// 2.压缩
// 压缩后的一串base64代码,目测3M图片压缩后800kb
let imageUrl = this.compressToDataUrl(img)
// 3.base64转Blob
// 转化成 png格式
const imageName = new Date().getTime() + '.png'
let imageBlob = this.dataURLtoBlob(imageUrl, imageName)
// 4.构建FormData
// 这下面写接口,这里传base64格式给后台
const formData = new FormData()
formData.append('picture', imageBlob, imageName)
// 5.添加文件类型请求头
let httpOption = {
headers: {
// 添加文件类型请求头
'Content-Type': 'multipart/form-data'
}
}
// 6.发送
// vue-resource
this.$http.post('/file/upload', formData, httpOption).then(({body}) => {
debugger
}).catch(() => {
debugger
})
},
// detail:额外信息,包含 name 和 index 字段
beforeDelete (file, detail) {
for (let i = 0; i < this.uploadToServerFileList.length; i++) {
let backFile = this.uploadToServerFileList[i]
if (file.filename === backFile.orientation) {
debugger
}
}
},
afterUploader (file) {
// debugger
// // 此时可以自行将文件上传至服务器
console.log(file)
//
this.uploadToServerFileList = []
file.status = 'uploading'
file.message = '上传中...'
// 1.添加文件类型请求头
let httpOption = {
headers: {
// 添加文件类型请求头
'Content-Type': 'multipart/form-data'
}
}
// 2.构建 FormData
let formData = new FormData()
// 单个文件:
// file.filename可作为文件的标识,用于业务处理
formData.append('file', file.file, file.filename)
// 多个文件:
// for (let singlefile of file) {
// // 分多次向formData中同一个键名下添加一个文件即可
// formData.append('files', singlefile.file, singlefile.filename)
// }
// 3.发送
// vue-resource
this.$http.post('/file/upload', formData, httpOption).then(({body}) => {
if (body.success === true) {
file.status = 'done '
file.message = '上传完成'
let destContextPath = body.destContextPath
let destFileName = body.destFileName
this.uploadToServerFileList.push({
url: destContextPath + destFileName, originFileName: file.filename
})
} else {
file.status = 'failed '
file.message = '上传失败'
Toast('文件上传失败!')
}
}).catch(() => {
debugger
})
}
},
// 局部注册
components: {
[Dialog.Component.name]: Dialog.Component
}
}
</script>
2.1.4.2.1afterUploader核心发送函数分析
请特别注意headers设置。
afterUploader (file) {
debugger
// 此时可以自行将文件上传至服务器
console.log(file)
// 1.添加文件类型请求头
let httpOption = {
headers: {
// 添加文件类型请求头
'Content-Type': 'multipart/form-data'
}
}
// 2.构建 FormData
let formData = new FormData()
// 单个文件:
formData.append('file', file.file, file.filename)
// 多个文件:
// for (let singlefile of file) {
// // 分多次向formData中同一个键名下添加一个文件即可
// formData.append('files', singlefile.file, singlefile.filename)
// }
// 3.发送
// vue-resource
this.$http.post('/file/upload', formData, httpOption).then(({body}) => {
debugger
}).catch(() => {
debugger
})
}
2.1.4.3编写样式
<!--3.样式-->
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
/*共享同一行*/
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
2.2后台处理
2.2.1FileController
package com.kikop.controller;
import com.alibaba.fastjson.JSONObject;
import com.kikop.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
/**
* @author kikop
* @version 1.0
* @project myantbackdemo
* @file FileController
* @desc
* @date 2022/04/13
* @time 20:39
* @by IDE: IntelliJ IDEA
*/
@RestController
@RequestMapping("/api/file")
public class FileController {
@Autowired
FileService fileService;
// web容器临时存储目录:
// C:\Users\kikop\AppData\Local\Temp\tomcat.1514260066643344745.8086\work\Tomcat\localhost\ROOT\
@RequestMapping("/upload")
public JSONObject upload(@RequestParam(value = "file", required = true) MultipartFile file, HttpServletRequest request) {
return fileService.upload(file, request);
}
@RequestMapping("/upload")
public JSONObject multipleUpload(@RequestParam(value = "files", required = true) MultipartFile file, HttpServletRequest request) {
return fileService.upload(file, request);
}
}
2.2.2FileServiceImpl
package com.kikop.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.kikop.myconst.CommonResponse;
import com.kikop.myconst.ConstVarManager;
import com.kikop.service.FileService;
import com.kikop.utils.MyFileUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
@Service
public class FileServiceImpl implements FileService {
@Override
public JSONObject upload(MultipartFile multipartFile, HttpServletRequest request) {
// tomcat临时文件
// C:\Users\kikop\AppData\Local\Temp\tomcat-docbase.5125138269731218182.8086\
// String realPath = request.getSession().getServletContext().getRealPath("/");
// 1.生成文件名
String strDestFileName = UUID.randomUUID().toString().replace("-", "");
// 2.文件元信息
String originalFilename = multipartFile.getOriginalFilename();
try {
JSONObject result = new JSONObject();
result.put("success", true);
// 3.文件保存
String fileType = originalFilename.substring(originalFilename.lastIndexOf("."));
if (StringUtils.isEmpty(fileType)) {
result.put("success", false);
result.put("msg", "文件类型不合法!");
return result;
}
// C:\Users\kikop/myuploadfile\
String strDestFilePathName_first = ConstVarManager.MyUploadFile_Name;
// 2ea0b69709e947c48984acf248d077f5.png
String strDestFilePathName_more = strDestFileName + fileType;
System.out.println("文件存储全路径为:" + strDestFilePathName_first + strDestFilePathName_more);
MyFileUtils.copyFile2DestDirectory(multipartFile.getInputStream(), strDestFilePathName_first, strDestFilePathName_more);
// http://localhost:8086/myuploadfile/2ea0b69709e947c48984acf248d077f5.png
// 获取服务上下文
String requestContextPath = request.getScheme() + "://" + request.getServerName() + ":"
+ request.getServerPort() + "/" + request.getContextPath();
// http://localhost:8086/
result.put("destContextPath", requestContextPath);
// myuploadfile/2ea0b69709e947c48984acf248d077f5.png
result.put("destFileName", ConstVarManager.MyUploadFile_VisitName + File.separator + strDestFilePathName_more);
// 4.返回
return result;
} catch (Exception ex) {
ex.printStackTrace();
return CommonResponse.getCommonResponse(ex);
}
}
}
2.2.3ConstVarManager
package com.kikop.myconst;
import java.io.File;
public class ConstVarManager {
public static final String MyUploadFile_Protocol = "file:///";
public static final String MyUploadFile_VisitName = "myuploadfile";
// C:\Users\kikop/myuploadfile/
public static final String MyUploadFile_Name = System.getProperties().getProperty("user.home") + File.separator +
MyUploadFile_VisitName + File.separator;
static {
// 自动创建目录判断
// https://www.cnblogs.com/qbdj/p/10980840.html
File uploadDirectory = new File(MyUploadFile_Name);
if (!uploadDirectory.exists()) {
uploadDirectory.mkdir();
// uploadDirectory.mkdirs(); //多层目录需要调用mkdirs
}
}
}
2.2.4MyFileUtils
package com.kikop.utils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class MyFileUtils {
public static void copyFile2DestDirectory(InputStream inputStream, String strDestFilePathName_first, String strDestFilePathName_more) throws IOException {
Path destFilePath = Paths.get(strDestFilePathName_first, strDestFilePathName_more);
Files.copy(inputStream, destFilePath);
}
}
2.2.5WebAppConfig
package com.kikop.config;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import com.kikop.myconst.ConstVarManager;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* @author kikop
* @version 1.0
* @project myantbackdemo
* @file
* @desc
* @date 2020/10/31
* @time 21:56
* @by IDE: IntelliJ IDEA
*/
@Configuration
public class WebAppConfig implements WebMvcConfigurer {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"};
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
converters.add(fastJsonHttpMessageConverter);
}
/**
* 静态资源(不需要,用默认的即可)
* 配置请求的解析映射路径
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 1.默认不配置也行
// spring mvc默认的
// http://localhost:8080/myveuback.jpeg
registry.addResourceHandler("/**").addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);
// 2.自定义
registry.addResourceHandler("/myuploadfile/**")
.addResourceLocations(ConstVarManager.MyUploadFile_Protocol+ConstVarManager.MyUploadFile_Name);
}
}
2.3测试
[图片上传失败...(image-6419a0-1649936781086)]
[图片上传失败...(image-34730d-1649936781087)]
参考
1vant uploader组件,回显文件、文件名
https://blog.csdn.net/weixin_42540974/article/details/121539208
2vue 使用vant Uploader 文件上传(图片压缩)
https://blog.csdn.net/weixin_40918145/article/details/108267163
3vant中uploader上传图片
https://blog.csdn.net/weixin_50651378/article/details/123765330