使用vue完成上传文件页面

需求描述

最近加班加点完成了一个上传文件的需求,听起来很简单想着也很简单,但是知易行难实际操作起来,还是遇到了很多问题,现在就来总结一下,使用vue来构建一个上传文件的页面中会遇到哪些问题,其中的一些问题也可能是用别的框架也会遇到的问题。

平台:主要是在安卓和苹果的微信webview中运行。

先来看下主要的技术点:

  1. 父子组件的动态渲染
  2. 初始化数据和判断数据
  3. input表单隐藏联动
  4. 上传图片后可以预览

遇到的问题:

  1. 在iPhone中,click()事件无效
  2. 在微信安卓webview中,img标签使用background属性无效
  3. 预览上传图片保证宽高比
  4. 在某些安卓机型用rem单位不显示边框

设计图大概是这样的:

完成后的效果是这样:

  1. 首先登陆后,如果没有选择两个选项的话提交按钮点击不了并会有提示
  2. 当选择图片后,下面会显示选择图片的缩略图
  3. 当点击提交后,会显示loading成功后会刷新页面
  4. 提交过的部分不会显示示意图会改为一张提示图片,刚上传过所有图片时会提示已经上传过所有图片

大概就是这么一个步骤流程吧。

技术点分析

这个页面主要使用的UI包括:提示、loading、选项栏等都是从WEUI样式库中扒出来的,因为是在微信公众号中使用的,体验上会一致。上传部分使用的是post表单上传,我把form表单隐藏了起来,通过模拟上传按钮,当点击上传按钮的时候同步触发表单中的input type=‘file’标签,调起原生的选择图片的界面,选择图片后,点击上传按钮的同时触发一个post请求把图片传到后台,完成整个功能。

父子组件的动态渲染

可以从设计图看出上传图片的部分基本上设计是一样的,所以用一个vue组件来复用是最好的,顺便说一下这个页面用到了三个组件,一个是dialog提示的组件,一个是loading的组件,还有一个就是上传这部分用到的组件。

主要说一下上传部分的组件,在vue中,子父组件要传递的参数需要在子组件的props中声明,下面是我的组件中的模板,我主要用到了以下参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div class="com-box">
<h3 v-if="comTitle" class="com-title">{{comTitle}}</h3>
<div class="upload-box">
<h2>{{nameCn}}</h2>
<div class="img-box" id="{{nameId}}" :style="bgUrl">
<!--<img :src="imgUrl">-->
</div>
<p class="tip" v-if="tip">{{tip}}</p>
<!--<div v-if="isUpload" class="upload-btn upload-true">图片已提交</div>-->
<div v-if="!isUpload" class="upload-btn upload-false clickable-div" onclick="document.querySelector('input').click()" @click="inputBtn(name, $event)">上传图片</div>
</div>
</div>
</template>


<script>
/*
* name:主要是让input表单和组件通过id产生关联
* nameCn:组件的title
* imgUrl:(最后弃用但没有删掉)
* isUpload: 标识有没有上传,这个会在父组件中进行判断,如果已经上传了会把上传按钮隐藏掉
* tip:图片下部的说明文字
* bgUrl:默认的说明背景图,上传完成后会在父组件中更改这个图片改成一张上传完成的提示图
*/

props: ['name', 'nameCn', 'imgUrl', 'isUpload', 'tip', 'bgUrl', 'comTitle'],
computed: {
nameId () {
return this.name + 'content'
}
}
</script>

然后在父组件中我们可以引入这个组件,并声明它,然后在标签中把子组件需要用到的参数传递进去就好了。我这里是使用了v-for去遍历了一个result_data的数组,把所有需要上传的组件遍历渲染了出来。如下:

1
2
3
4
5
6
7
8
9
10
<template>
<upload v-for="(index, item) in result_data" :name="item.name" :name-cn="item.nameCn" :bg-url="item.bgUrl" :img-url="item.imgUrl" :tip="item.tip" :is-upload="item.isUpload" :com-title="item.comTitle"> </upload>
</template>

<script>
import upload from './upload.vue'
components: {
upload
},
</script>

这样子父组件就可以传递数据了,具体的大家可以去看一下官方的文档。

初始化数据和判断数据

这是这个任务里最坑的一个部分。因为后台只会把已经上传过图片的字段告诉我,所以我需要在前端去造一些初始化的数据,去和后台返回回来的比较,再去判断哪些上传过需要展示出来,那些没上传需要上传。

注意:可以造数据的前提是,数据量不是很大,且数据是固定的。其实最好是后台可以完成这些判断,因为前端本就不应该去处理数据方面的逻辑。

我初始化的数据大概是这个样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
init_data: [
// 0
{
name: 'shopphoto',
nameCn: '店铺门头照片',
bgUrl: {
backgroundImage: "url('http://near.m1img.com/op_upload/62/146502860635.jpg')",
backgroundSize: '100% 100%'
},
imgUrl: '',
tip: '',
isUpload: false
},

// 1
{
name: 'goodsphoto',
nameCn: '店铺内景照片',
bgUrl: {
backgroundImage: "url('http://near.m1img.com/op_upload/62/146502865726.jpg')",
backgroundSize: '100% 100%'
},
imgUrl: '',
tip: '',
isUpload: false
}
]

然后我去遍历后台返回的那个数组和我自己初始化得数组,用name这个字段产生关联去比较,因为后台只返回了上传过图片的字段,所以我就可以判断出哪些是没有上传的部分。判断的逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
compareArr (oldArr, newArr) {
var _this = this
var _oldArr = oldArr
var _newArr = newArr
for (let i = 0, len1 = _newArr.length; i < len1; i++) {
for (let j = 0, len2 = _oldArr.length; j < len2; j++) {
if (_newArr[i].name === _oldArr[j].name) {
let initData = _this.init_data
initData[i].bgUrl = {
backgroundImage: "url('http://near.m1img.com/op_upload/62/146520417256.png')",
backgroundSize: '100% 100%'
}
initData[i].isUpload = true
_this.$set('init_data', initData)
}
}
}
}

我会给这个方法传入两个数组。oldArr是后台的数组,newArr是我自己初始化的数组。
循环遍历比较两个数组,把我自己初始化的数据做出一些修改,主要是提示图片和是否上传标识字段的更改。

input表单隐藏联动

这个逻辑也不是很复杂,就是在点击上传按钮的时候,调一个方法,在方法中选择到对应的input标签出发一个click()事件,主要是在iphone上面,可能会不支持input type=‘file’的标签,这里有一个坑,在网上找到了一些解决办法,但是试了一下没有效果,最后自己试验了一下找到了一个解决办法,在下面遇到的问题中会叙述。

第二是要注意在form表单中要禁止事件冒泡,不然你点击一次input表单就会提交一次。

上传图片后可以预览

可以我是汲取了广大人民的智慧。。。

主要思路就是把图片读取成一个base64格式,然后再把他装到一个img标签中。实现预览功能,这里我给几个参考的资料,有的demo直接就可以跑,改一改就可以运用到自己的项目中。

  1. 移动前端—图片压缩上传实践
  2. 使用HTML5的两个api,前端js完成图片压缩
  3. 使用HTML5 FILE API上传图片移动端缩略图兼容问题

大概看完他们的思路以后,就能够实现这个功能了,但是他并不能保持宽高比适配屏幕,后面我优化了一下这部分。

遇到的问题

遇到的这四个问题我们来一一解决:

  1. 在iPhone中,click()事件无效
  2. 在微信安卓webview中,img标签使用background属性无效
  3. 预览上传图片保证宽高比
  4. 在某些安卓机型用rem单位不显示边框

在iPhone中click()事件无效

这个问题在网上也有类似问题,地址,但是我依照网上的办法都不行,但是我发现显式的在标签内通过onclick=”document.querySelector(‘input’).click()这种方式是可以调用的,所以我把所有的我需要隐式调用input的地方都绑定了这个onclick="document.querySelector('input').click(),然后逻辑方面我又用vue的@click绑定了一个方法来处理逻辑方面。

img标签使用background属性无效

这个可能是安卓的微信webview支持的不好,因为在iphone和别的手机浏览器当中都可以支持,但是在安卓的微信webview不能显示,这个是兼容性的问题,所以最后我选择在div中加background。

预览上传图片保证宽高比

这个其实也不难优化,因为上面的方法是把base64的图片放到一个img标签当中,所以没法去适配这个宽高比,如果把这个放到divbackground-image属性当中,并把background-size设置成contain,把background-color设置成页面背景一样的颜色,这样就能模拟出浏览图片的功能了。

在某些安卓机型用rem单位不显示边框

因为移动端使用的是rem为单位,用postcss去处理(就是在css中写px,后处理器去处理成rem),在1px时转换为rem单位可能不足屏幕显示的1px,则不会显示边框。解决办法就是当写border时加注释去避免处理器去处理这句css(/*px*/

整个过程大概就是这样吧,我把这个页面放到了github上,但是没有vue的环境跑不起来,如果有需要可以拷到自己vue项目下去尝试。代码垃圾,轻喷。。。。。。

顺嘴说一句,那天晚上加班到两点,然后坐车回家,到地方付款的时候,发现银行维护没法付款,而且微信里也没有余额,正在想怎么办司机师傅说,没事赶紧回家吧等能付款的时候付了就好了,一句赶紧回家吧真实一阵感动啊。。。