2022-04-13_有赞表单组件van-uploader文件上传前后台学习笔记

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:

单个文件格式:

image-20220413212126161.png

多个文件格式:

image-20220414144011520.png

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
      })
    }
image-20220413211038593.png

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容