基于vue+axios+lrz.js微信端图片压缩上传方法


Posted in Javascript onJune 25, 2019

业务场景

微信端项目是基于Vux + Axios构建的,关于图片上传的业务场景有以下几点需求:

1、单张图片上传(如个人头像,实名认证等业务)

2、多张图片上传(如某类工单记录)

3、上传图片时期望能按指定尺寸压缩处理

4、上传图片可以从相册中选择或者直接拍照

遇到的坑

采用微信JSSDK上传图片

在之前开发的项目中(mui + jquery),有使用过微信JSSDK的接口上传图片,本想应该能快速迁移至此项目。事实证明编程没有简单的事:

1、按指定尺寸压缩图片

JSSDK提供的接口wx.chooseImage 是不能指定图片压缩尺寸的,只能在后端的接口通过localId获取图片时,再转换成指定的尺寸。

2、微信JSSDK的接口权限验证

只要是单页面应用项目,微信JSSDK注入权限验证都会有这个坑,而这个与路由模式(hash 或 history)也有关联。有关此坑, 后续会再次写文总结。参考解决方案 [微信JSSDK] 解决SDK注入权限验证 安卓正常,IOS出现config fail

经过权衡考虑网页可能需要在微信以外的浏览器上也能上传文件,顾后来放弃了采用微信JSSDK接口上传图片的方式。

android版微信,input onchange事件不触发

这个坑,圈内有很多人踩过了。在PC端测试是正常的,发布之后,微信端上传时能选择文件,但之后没有任何效果。日志跟踪,后台的api都未调用,由此判断是input的onchange事件未被触发。

解决方案, 更改input的 accept属性:

<input ref="file" type="file" accept="image/jpeg,image/png" @change="selectImgs" />

将以上代码更改为:

<input ref="file" type="file" accept="image/*" @change="selectImgs" />

如果不允许从相册中选择,只能拍照,增加capture="camera":

<input ref="file" type="file" accept="image/*" capture="camera" @change="selectImgs" />

(注:如果场景支持从相册选择或拍照,测试发现某些机型拍照后返回到了主页。哈哈,也有可能是其他因素引起的问题,未做深究了)

使用Lrz.js压缩图片

目前手机拍照的图片文件大小一般在3-4M,如果在上传时不做压缩处理会相当浪费流量并且占用服务器的存储空间(期望上传原图的另做讨论)。如果能够在前端压缩处理,那肯定是最理想的方案。而 lrz.js 则提供了前端图片文件的压缩方案,并且可以指定尺寸压缩。实测:3M左右的图片文件,按宽度450px尺寸压缩上传后的文件大小在500kb左右,上传时间2s以内。

其核心源码,如下:

selectImgs () {
 let file = this.$refs.file.files[0]
 lrz(file, { width: 450, fieldName: 'file' }).then((rst) => {
  var xhr = new XMLHttpRequest()
  xhr.open('POST', 'http://xxx.com/upload')

  xhr.onload = () => {
   if (xhr.status === 200 || xhr.status === 304) {
    // 无论后端抛出何种错误,都会走这里
    try {
     // 如果后端跑异常,则能解析成功, 否则解析不成功
     let resp = JSON.parse(xhr.responseText)
     console.log('response: ', resp)
    } catch (e) {
     this.imageUrl = xhr.responseText
    }
   }
  }

  // 添加参数
  rst.formData.append('folder', 'wxAvatar') // 保存的文件夹
  rst.formData.append('base64', rst.base64)
  // 触发上传
  xhr.send(rst.formData)

  return rst
 })
}

单个图片上传组件完整代码,如下(注: icon图标使用的是svg-icon组件):

<template>
 <div class="imgUploader">
  <section v-if="imageUrl"
       class="file-item ">
   <img :src="imageUrl"
      alt="">
   <span class="file-remove"
      @click="remove()">+</span>
  </section>
  <section v-else
       class="file-item">
   <div class="add">
    <svg-icon v-if="!text"
         class="icon"
         icon-class="plus" />
    <span v-if="text"
       class="text">{{text}}</span>
    <input type="file"
        accept="image/*"
        @change="selectImgs"
        ref="file">
   </div>
  </section>
 </div>
</template>

<script>
import lrz from 'lrz'
export default {
 props: {
  text: String,
  // 压缩尺寸,默认宽度为450px
  size: {
   type: Number,
   default: 450
  }
 },
 data () {
  return {
   img: {
    name: '',
    src: ''
   },
   uploadUrl: 'http://ff-ff.xxx.cn/UploaderV2/Base64FileUpload',
   imageUrl: ''
  }
 },
 watch: {
  imageUrl (val, oldVal) {
   this.$emit('input', val)
  },
  value (val) {
   this.imageUrl = val
  }
 },
 mounted () {
  this.imageUrl = this.value
 },
 methods: {
  // 选择图片
  selectImgs () {
   let file = this.$refs.file.files[0]
   lrz(file, { width: this.size, fieldName: 'file' }).then((rst) => {
    var xhr = new XMLHttpRequest()
    xhr.open('POST', this.uploadUrl)

    xhr.onload = () => {
     if (xhr.status === 200 || xhr.status === 304) {
      // 无论后端抛出何种错误,都会走这里
      try {
       // 如果后端跑异常,则能解析成功, 否则解析不成功
       let resp = JSON.parse(xhr.responseText)
       console.log('response: ', resp)
      } catch (e) {
       this.imageUrl = xhr.responseText
      }
     }
    }

    // 添加参数
    rst.formData.append('folder', this.folder) // 保存的文件夹
    rst.formData.append('base64', rst.base64)
    // 触发上传
    xhr.send(rst.formData)

    return rst
   })
  },
  // 移除图片
  remove () {
   this.imageUrl = ''
  }
 }
}
</script>

<style lang="less" scoped>
.imgUploader {
 margin-top: 0.5rem;
 .file-item {
  float: left;
  position: relative;
  width: 100px;
  text-align: center;
  left: 2rem;
  img {
   width: 100px;
   height: 100px;
   border: 1px solid #ececec;
  }
  .file-remove {
   position: absolute;
   right: 0px;
   top: 4px;
   width: 14px;
   height: 14px;
   color: white;
   cursor: pointer;
   line-height: 12px;
   border-radius: 100%;
   transform: rotate(45deg);
   background: rgba(0, 0, 0, 0.5);
  }

  &:hover .file-remove {
   display: inline;
  }
  .file-name {
   margin: 0;
   height: 40px;
   word-break: break-all;
   font-size: 14px;
   overflow: hidden;
   text-overflow: ellipsis;
   display: -webkit-box;
   -webkit-line-clamp: 2;
   -webkit-box-orient: vertical;
  }
 }
 .add {
  width: 100px;
  height: 100px;
  float: left;
  text-align: center;
  line-height: 100px;
  font-size: 30px;
  cursor: pointer;
  border: 1px dashed #40c2da;
  color: #40c2da;
  position: relative;
  background: #ffffff;
  .icon {
   font-size: 1.4rem;
   color: #7dd2d9;
   vertical-align: -0.25rem;
  }
  .text {
   font-size: 1.2rem;
   color: #7dd2d9;
   vertical-align: 0.25rem;
  }
 }
}
input[type="file"] {
 position: absolute;
 left: 0;
 top: 0;
 width: 100%;
 height: 100%;
 border: 1px solid #000;
 opacity: 0;
}
</style>

后端图片存储处理

后端api对图片的处理,是必不可少的环节,需要将前端提交过来的base64字符串转换成图片格式,并存放至指定的文件夹,接口返回图片的Url路径。各项目后端对图片的处理逻辑都不一致,以下方案仅供参考(我们使用asp.net MVC 构建了独立的文件存储站点)。

其核心源码,如下:

/// <summary>
/// 图片文件base64上传
/// </summary>
/// <param name="folder">对应文件夹位置</param>
/// <param name="base64">图片文件base64字符串</param>
/// <returns></returns>
public ActionResult Base64FileUpload(string folder, string base64)
{
  var context = System.Web.HttpContext.Current;
  context.Response.ClearContent();
  // 因为前端调用时,需要做跨域处理
  context.Response.AddHeader("Access-Control-Allow-Origin", "*");
  context.Response.AddHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
  context.Response.AddHeader("Access-Control-Allow-Headers", "content-type");
  context.Response.AddHeader("Access-Control-Max-Age", "30");
  if (context.Request.HttpMethod.Equals("OPTIONS"))
  {
    return Content("");
  }

  var resultStr = base64.Substring(base64.IndexOf(",") + 1);//需要去掉头部信息,这很重要
  byte[] bytes = Convert.FromBase64String(resultStr);
  var fileName = Guid.NewGuid().ToString() + ".png";
  if (folder.IsEmpty()) folder = "folder";
  //本地上传
  string root = string.Format("/Resource/{0}/", folder);
  string virtualPath = root + fileName;
  string path = Server.MapPath("~" + virtualPath);
  //创建文件夹
  if (!Directory.Exists(Path.GetDirectoryName(path)))
  {
    Directory.CreateDirectory(Path.GetDirectoryName(path));
  }
  System.IO.MemoryStream ms = new System.IO.MemoryStream(bytes);//转换成无法调整大小的MemoryStream对象
  System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(ms);
  bitmap.Save(path, System.Drawing.Imaging.ImageFormat.Png);//保存到服务器路径
  ms.Close();//关闭当前流,并释放所有与之关联的资源
  return Content(Net.Url + virtualPath); //返回文件路径
}

结语

由于项目实际情况,上述的方案中还存在诸多未完善的点:

1、多张图片上传,还是采用的与单张图片相同的接口处理, 更为完善的方案是,前端的多图上传组件只绑定一个关联Id,即可通过实现上传和将图片列表查询展示(注:该功能在微信端未实现)。

2、后端图片上传的接口,未做严格的安全校验,更为完善的方案是,每个上传的场景,都应该限制文件类型,限制文件大小,以及文件数据来源校验(注: 如软件需要按二级等保标准测评,则后端接口会检测通不过)。

3、上传组件,未显示上传进度,体验性稍差。

正如前文所述,出于项目实际情况考虑,只是简单实现图片压缩上传功能,如要支持更多的场景,还得细细雕琢。

Javascript 相关文章推荐
更换select下拉菜单背景样式的实现代码
Dec 20 Javascript
js获取IP和PcName(IE)在vs中可用
Aug 02 Javascript
js日期相关函数总结分享
Oct 15 Javascript
document节点对象的获取方式示例介绍
Dec 24 Javascript
Angularjs material 实现搜索框功能
Mar 08 Javascript
javascript中call apply 与 bind方法详解
Mar 10 Javascript
使用vue.js制作分页组件
Jun 27 Javascript
JS实现类似51job上的地区选择效果示例
Nov 17 Javascript
koa大型web项目中使用路由装饰器的方法示例
Apr 02 Javascript
Javascript读取上传文件内容/类型/字节数
Apr 30 Javascript
用js简单提供增删改查接口
May 12 Javascript
vue cli3 调用百度翻译API翻译页面的实现示例
Sep 13 Javascript
新手快速入门微信小程序组件库 iView Weapp
Jun 24 #Javascript
前端Vue项目详解--初始化及导航栏
Jun 24 #Javascript
微信小程序调用天气接口并且渲染在页面过程详解
Jun 24 #Javascript
微信小程序-可移动菜单的实现过程详解
Jun 24 #Javascript
webpack自动打包和热更新的实现方法
Jun 24 #Javascript
Promise扫盲贴
Jun 24 #Javascript
深入学习js函数的隐式参数 arguments 和 this
Jun 24 #Javascript
You might like
PHP 网络开发详解之远程文件包含漏洞
2010/04/25 PHP
php Smarty 字符比较代码
2011/02/27 PHP
解决PHP里大量数据循环时内存耗尽的方法
2015/10/10 PHP
thinkPHP使用post方式查询时分页失效的解决方法
2015/12/09 PHP
php获取POST数据的三种方法实例详解
2016/12/20 PHP
js下拉框二级关联菜单效果代码具体实现
2013/08/03 Javascript
利用javascript数组长度循环数组内所有元素
2013/12/27 Javascript
javascript中CheckBox全选终极方案
2015/05/20 Javascript
详解JavaScript中的客户端消息框架设计原理
2015/06/24 Javascript
JS处理json日期格式化问题
2015/10/01 Javascript
javascript生成img标签的3种实现方法(对象、方法、html)
2015/12/25 Javascript
详解AngularJS Filter(过滤器)用法
2015/12/28 Javascript
jQuery根据name属性进行查找的用法分析
2016/06/23 Javascript
Javascript中的prototype与继承
2017/02/06 Javascript
Vue.js实现一个todo-list的上移下移删除功能
2017/06/26 Javascript
javascript计算渐变颜色的实例
2017/09/22 Javascript
EasyUI实现下拉框多选功能
2017/11/07 Javascript
详解小程序循环require之坑
2019/03/08 Javascript
详解Vue.js中引入图片路径的几种方式
2019/06/17 Javascript
Vue路由之JWT身份认证的实现方法
2019/08/26 Javascript
node.js如何操作MySQL数据库
2020/10/29 Javascript
用Python解析XML的几种常见方法的介绍
2015/04/09 Python
Python while、for、生成器、列表推导等语句的执行效率测试
2015/06/03 Python
Python3数据库操作包pymysql的操作方法
2018/07/16 Python
自适应线性神经网络Adaline的python实现详解
2019/09/30 Python
python处理excel绘制雷达图
2019/10/18 Python
纯CSS改变webkit内核浏览器的滚动条样式
2014/04/17 HTML / CSS
HR喜欢的自荐信格式
2013/10/08 职场文书
给民警的表扬信
2014/01/08 职场文书
初婚初育证明
2014/01/14 职场文书
《争吵》教学反思
2014/02/15 职场文书
2014年妇女工作总结
2014/12/06 职场文书
2015年销售工作总结范文
2015/03/30 职场文书
2016寒假社会实践心得体会范文
2015/10/09 职场文书
一封真诚的自荐信帮你赢得机会
2019/05/07 职场文书
Golang Gob编码(gob包的使用详解)
2021/05/07 Golang