图片、缓存与文件

在前面的章节,我们非常强调JavaScript对数据的操作,这一节我们来了解一下小程序与客户端(手机)更深的交互。前面章节将数据存储到通讯录(添加手机联系人)、存储到剪切板(用手机复制粘贴),小程序就已经与客户端手机有了交互,这一节我们将来获取手机相册里的图片和手机相机拍照的照片、手机的定位以及获取手机里的缓存、文件,并使用JavaScript操作图片、操作缓存和操作文件等。

获取手机相册或拍照的图片

用小程序来获取手机相册里的图片和拍照的照片听起来好像挺复杂的,不过因为有了API,我们只需要结合前面的点击事件、事件处理函数以及调用API、传入指定的参数就能很容易做到。

技术文档:wx.chooseImage()

上传一张照片

使用开发者工具新建一个file的页面,然后在file.wxml里输入以下代码:

然后在file.js的data里给imgurl设置一个初始值,由于链接src是一个字符串类型,我们这里可以设置为一个字符串空值,完成imgurl的初始化:

再在file.js里添加事件处理函数chooseImg,在chooseImg里我们来调用上传函数的API wx.chooseImage(),其中count、sizeType、sourceType都是API已经写好的属性,API调用成功(图片上传成功)之后,会在success回调函数里返回图片的一些信息,返回的信息可以看技术文档。

虽然在开发者工具的模拟器也可以看到效果,但是wx.chooseImage()是一个与手机客户端交互性很强的API,我们最好在手机上体验。点击开发者工具的预览,在手机微信里查看效果,点击选择图片按钮,上传一张图片或拍照看看。

  • count:可以选择的照片数量,默认为9张(由于imgurl声明的是字符串,多张照片需为数组Array,后面有上传多张图片的案例)
  • sourceType:选择图片的来源,album就是图片可以来自手机相册;而camera是可以来自手机拍照,两个都写就是来自相册或拍照都可以;
  • sizeType:所选的图片的尺寸,original为原图,compressed为压缩图,为了减轻服务器压力,建议为压缩图;
  • tempFilePaths为临时文件的路径列表tempFiles为临时文件列表,注意这两个值都为数组。

小任务:将sourceType的值修改为 ['album'],在手机微信上看看有什么效果?再将sizeType改为 ['compressed'],看手机是否还能够上传原图?

空值的处理

我们可以看到由于imgurl为空值,image组件有默认宽度300px、高度225px(会随css而改变大小),所以显示上传的图片会与选择图片的button有一段空白,处理的方法有三种:

方法一:我们可以给imgurl弄一张初始图片的链接,为了让界面更加美观、交互性更好,通常都会设置一个默认的图片,比如默认的头像,当用户上传时,setData就会取代初始图片;

方法二:判断imgurl是否有内容,比如我们可以加一层逻辑判断,当Page()里的data下的imgurl属性非空时,组件才会显示;空时就不显示。

方法三:这个方法和方法二类似,设置一个逻辑判断,比如在data里设置一个boolean属性比如hasImg,初始值为false,

当chooseImg回调成功之后,在that.setData里把hasImg修改为true,也就是将wx.chooseImage()的success回调函数里的that.setData()修改为:

这样是否有图片就进入到了回调函数的逻辑里了,接着我们把file.wxml的代码改为如下:

没有图片也就是hasImg的值为false时,会显示选择图片的button;而当有图片时,没有button只有图片,在一定的场合用户体验会更好(button要是一直在,用户就还会去点,体验不好)。

注意:这里所说的上传图片与我们日常生活中的上传图片不是一样的哦,日常生活中上传图片,图片不仅会显示在小程序(网页、App)上,还会继续上传到存储服务器里面,而我们这里只是进行了第一步,上传的图片只是存储在临时文件里面,所以重新编译,图片就不显示了。后面会有临时文件的内容以及会在云开发部分将图片上传到云存储。

上传多张照片

如果上传的是多张照片,那么imgurl的初始值就不能是字符串了,而是一个数组Array,

而file.wxml的代码也要相应的改为列表渲染即可,这种写法在代码上通用性比较强,上传一张图片、多张图片都可以,不过具体还是要看实际产品开发需求。

然后再把count的值修改为2~9张,编译之后,在手机微信上体验一下效果。

操作图片

使用小程序图片API不仅可以上传图片,还可以对上传的图片进行一定的操作,比如获取图片信息、预览图片、保存图片、压缩图片等等。

获取图片信息

无论是存储在小程序本地,还是存储在临时文件、缓存、网络上的图片,使用wx.getImageInfo() 都可以获取到该图片的宽度、高度、路径、格式以及拍照方向。

技术文档:wx.getImageInfo()

使用开发者工具在file.js里添加以下代码,我们使用wx.getImageInfo() 来获取之前上传的图片的信息。由于获取图片信息需要等上传图片成功之后才能执行,因此我们可以在wx.chooseImage()的success回调函数里来调用wx.getImageInfo(),而获取图片信息之后才能返回图片信息,因此这又是一个回调函数:

编译之后,我们再来上传一张图片,图片上传成功之后,在控制台console里可以看到打印的信息。在上面的代码里,我们发现success回调函数嵌套success回调函数。

回调函数

经过之前的学习,相信大家对回调函数success、fail有了一定的认识,那什么是回调函数呢?简单一点说就是:回调Callback是指在另一个函数执行完成之后被调用的函数。success、fail就都是在小程序的API函数执行完成之后,才会被调用,而success和fail它们本身也是函数,也能返回数据。而复杂一点说,就是回调函数本身就是函数,但是它们却被其他函数给调用,而调用函数的函数被称为高阶函数。这些大家只需要粗略了解就可以了。

异步与同步

我们前面也提及过异步,那什么会有异步呢?因为JavaScript是单线程的编程语言,就是从上到下、一行一行去执行代码,类似于排队一样一个个处理,第一个不处理完,就不会处理后面的。但是遇到网络请求、I/O操作(比如上面的读取图片信息)以及定时函数(后面会涉及)以及类似于成功反馈的情况,等这些不可预知时间的任务都执行完再处理后面的任务,肯定不行,于是就有了异步处理

把要等待其他函数执行完之后,才能执行的函数(比如读取图片信息)放到回调函数里,先不处理,等图片上传成功之后再来处理,这就是异步。比如wx.showToast()消息提示框,可以放到回调函数里,当API调用成功之后再来显示提示消息。回调函数相当于是异步的一个解决方案。

预览所有上传的图片

预览图片就是在新页面里全屏打开图片,预览的过程中用户可以进行保存图片、发送给朋友等操作。可以预览一张照片或者多张照片。

技术文档:wx.previewImage()

使用开发者工具在file.wxml里输入以下代码,我们要预览的是从手机相册里上传的图片(保留上面的代码,接着写),如果没有上传图片,那就把预览图片的按钮给隐藏,我们来写一段完整的代码:

然后在file.js添加事件处理函数previewImg,调用预览图片的API wx.previewImage():

当上传图片之后点击预览图片按钮就能预览所有图片了。

这个场景主要用于让用户可以预览、保存或分享图片,毕竟image组件是不支持图片的放大预览、保存到本地、转发给好友,现在微信还支持预览小程序码,长按就可以打开小程序,这个API主要是为了增强用户的交互体验的。

那我们应该如何实现点击其中的某一张图片,就会弹出所有图片的预览呢?这里就要用到current了。

将之前file.wxml里图片上传的代码改成如下,把事件处理函数previewImg绑定在图片上面,

然后将file.js的事件处理函数previewImg修改为:

这样点击图片就会弹出预览窗口来预览图片了。

保存图片到相册

小程序不支持直接将网络图片保存到本地手机的系统相册,支持临时文件路径和小程序本地路径。

技术文档:wx.saveImageToPhotosAlbum()

比如我们在小程序的根目录下新建一个image文件夹并放一张图片到里面比如background.jpg,然后再在file.wxml里输入以下代码,让image组件绑定事件处理函数saveImg:

然后在file.js里添加事件处理函数saveImg,

编译之后预览在手机里体验,点击图片就会触发事件处理函数saveImg,调用wx.saveImageToPhotosAlbum() API,filePath为小程序文件的永久链接,文件就会保存到手机相册(没有相册权限会提示)。

当然永久链接实际开发用得不会太多,使用最多的场景是把网络图片下载到临时链接(因为不能直接保存网络图片),再将临时链接的图片保存到相册,只需把上面的永久链接换成临时链接就可以了。最重要的是要搞清楚图片到底在哪里,在网络上?还是在小程序本地?还是在临时文件里?还是在缓存里?

压缩图片

小程序是有压缩图片的API的wx.compressImage(),尤其是在上传图片时,为了减轻存储服务器的压力,不能让用户上传分辨率过高的照片。

  • 可以先让用户上传图片;
  • 图片上传成功之后(也就是在上传图片的success回调函数里)再来获取图片的信息;
  • 获取信息成功后(也就是在获取图片信息的success回调函数里)判断宽度或高度是否过大,如果图片过大,就压缩图片,
  • 压缩图片成功后(也就是在压缩图片的success回调函数里),再把压缩好的图片上传到服务器

上传图片、获取图片信息、压缩图片、上传图片到服务器,每一步都依赖上一步,所以会不断在success回调函数里写函数,实际开发涉及的业务会更复杂,就会不断回调,这被称之为回调地狱。这就是为什么会有Promise写法的原因,这个我们会在以后提及。

由于压缩图片使用到的场景不算太多,毕竟我们在上传照片时可以不支持上传原图original,只支持压缩compressed就能保证上传图片的大小了。而且wx.compressImage()压缩图片的API也比较简单,所以这里就不写实际案例了,相信大家看文档也能玩得明白。

上传文件

小程序不仅支持上传图片image,还支持上传视频、Excel、PDF、音频等等其他文件格式,但是我们只能从客户端会话里(也就是微信单聊、群聊的聊天记录里)选择其他格式的文件。

技术文档:wx.chooseMessageFile()

使用开发者工具在file.wxml里添加以下代码,给选择文件的button绑定事件处理函数chooseFile:

在file.js文件里添加事件处理chooseFile,并打印上传成功后回调函数里的参数对象。

使用开发者工具上传一张图片或其他格式的文件,在控制台console我们可以看到打印的res对象里有tempFiles的数组对象Array(没有tempFilePaths,此处技术文档有误),tempFiles对象包含文件的名称name、文件的临时路径path、文件的大小size、选择的文件的会话发送时间戳time、文件的类型type

我们可以把上传的文件所取得的信息给渲染到页面上,在file.wxml里添加列表渲染的代码,也就是

在Page()的data里初始化一个属性temFiles,初始值为一个空数组Array:

然后再在chooseFile的success回调函数里将数据使用setData赋值给tempFiles:

编译之后预览在微信上体验,看看什么效果?注意需选择微信有文件的会话框。还是再强调一下,这个上传和我们实际里的上传还是不一样的,这里只是把文件上传到了一个临时文件里面,并没有上传到服务器。

上传地理位置

除了可以上传图片、音视频以及各种文件格式以外,小程序还支持上传地理位置。

技术文档:wx.chooseLocation()

使用开发者工具在file.wxml里输入以下代码,前面我们上传了文件,这一次我们把手机的位置给上传并渲染出来:

然后在file.js的Page()的data里初始化location

在file.js里添加事件处理函数chooseLocation,

编译之后预览用手机点击选择地理位置的button,就会弹出地图选择位置(这个位置既可以是你当前的位置,也可以自己选择一个位置),然后点击确定,就能在小程序上看到我们上传的位置了。要让位置信息显示在地图上,可以在file.wxml里添加一个地图组件:

小任务:上传地理位置,将该地址显示在地图上,并添加该地理位置的markers。关于markers的知识,可以去看map组件的技术文档。

模块化与格式化

在新建模板小程序里(不使用云开发服务),有一个日志logs页面,这个日志logs虽然简单,但是包含着非常复杂的JavaScript知识,是一个非常好的学习参考案例,这里我们来对它进行一一解读。

模块化与引入模块

在实际开发中,日期、时间的处理经常会使用到,但是使用Date对象所获取到的时间格式与我们想要展现的形式是有非常大的差异的。这时我们可以把时间的处理抽离成为一个单独的 js 文件比如util.js(util是utility的缩写,表示程序集,通用程序等意思),作为一个模块。

把通用的模块放在util.js或者common.js,把util.js放在utils文件夹里等就跟把css放在style文件夹,把页面放在pages文件夹,把图片放在images文件夹里是一样的道理,尽管文件夹或文件的名称你可以任意修改,但是为了代码的可读性,文件结构的清晰,推荐大家采用这种一看就懂的方式。

使用开发者工具在小程序根目录新建一个utils文件夹,再在文件夹下新建util.js文件,在util.js里输入以下代码(也就是参考模板小程序的logs页面调用的util.js)

我们再来在file.js里调用这个模块文件util.js,也就是在file.js的Page()对象前面使用require引入util.js文件(需要引入模块文件相对于当前文件的相对路径,不支持绝对路径)

然后再在onLoad页面生命周期函数里打印看看这段时间处理的代码到底做了什么效果,这里也要注意调用模块里的函数的方式。

util.formatTime()就调用了模块里的函数,通过控制台打印的日志可以看到日期时间格式的不同,比如:

显然格式化后的日期时间的展现形式更符合我们的日常习惯,而9这个数值被转化成了字符串”09″。那这段格式化日期时间的代码是怎么实现的呢?这里就涉及到高阶函数的知识,一般函数调用参数,而高阶函数会调用其他函数,也就是把其他函数作为参数。

map

相信格式化数字的代码比较好理解,如果是15日里的15,由于n[1]是15的第二位数字5,为true会直接return返回n,也就是15;比如9月里的数字9,n[1]不存在,也就是没有第二位数,于是执行 '0' + n给它加一个0,变成09;而formatNumber是一个箭头函数。

而格式化日期时间则涉及到map,比如下面的这段代码就有map,

map也是一个数据结构,它背后的知识非常复杂,但是我们只需了解它是做什么的就可以,如果你想对数组里面的每一个值进行函数操作并且返回一个新数组,那你可以使用map

上面这段代码就是对数组[year, month, day][hour, minute, second]里面的每一个数值都进行格式化数字的操作,这一点我们可以在file.js的onLoad里打印看效果就明白了

从控制台打印的结果就可以看到数组里面的数字被格式化处理,有两位数的不处理,没有2位数的前面加0,而且返回的也是数组。至于数组Array的join方法,就是将数组元素拼接为字符串,以分隔符分割,上面[year, month, day]分隔符为”/”,[hour, minute, second]的分隔符为”:”。

数据缓存Storage

logs页面还涉及到数据缓存Storage方面的知识。通过前面的学习,我们了解到点击事件生成的事件对象也好,使用数据表单提交的数据也好,还是上传的图片、文件也好,只要我们重新编译小程序,这些数据都会消失。前面我们也提到存储数据、文件的方式有三种,一是保存到本地手机、二就是缓存,三是上传到服务器(云开发会讲解),这里我们就来了解数据缓存方面的知识。

技术文档:wx.setStorageSync()wx.getStorageSync()

保存文件

注意:尽管上传图片和上传文件都是把图片或文件先上传到临时文件里,但是保存图片wx.saveImageToPhotosAlbum()保存文件wx.saveFile()是完全不同的概念,保存图片是把图片保存到手机本地相册;而保存文件则是把图片由临时文件移动到本地存储里,而本地存储每个小程序用户只有10M的空间。

保存文件技术文档:wx.saveFile()

在了解logs的数据缓存案例之前,我们先来看一个将上传的图片由临时文件保存到缓存的案例,使用开发者工具在file.wxml里输入以下代码:

然后在file.js的data里初始化临时文件的路径tempFilePath和本地缓存的路径savedFilePath:

再在file.js里添加事件处理函数chooseImage和saveImage(函数名有别于之前的chooseImg和saveImg,不要弄混了哦):

还没有完~我们还需要在file.js的onLoad生命周期函数里将缓存里存储的路径赋值给本地缓存的路径savedFilePath:

编译之后,点击请上传文件的button,会触发chooseImage事件处理函数,然后调用上传图片的API wx.chooseImage,这时会将图片上传到临时文件,并将取得的临时文件地址赋值给tempFilePath,有了tempFilePath,图片就能渲染出来了。

然后再点击保存文件到缓存的button,会触发saveImage事件处理函数,然后保存文件API wx.saveFile,将tempFilePath里的图片保存到缓存,并将取得的缓存地址赋值给savedFilePath(注意tempFilePath也就是临时路径是保存文件的必备参数),这时缓存保存的图片就渲染到页面了。然后会再来调用缓存API wx.setStorageSync(),将缓存文件的路径保存到缓存的key savedFilePath里面。有些参数名称相同但是含义不同,这个要注意

通过wx.setStorageSync()保存到缓存里的数据,可以使用wx.getStorageSync()获取出来,我们在onLoad里把获取出来的缓存文件路径再赋值给savedFilePath。

编译页面,看看临时文件与缓存文件的不同,临时文件由于小程序的编译会被清除掉,而缓存文件有10M的空间,只要用户不刻意删除,它就会一直在。

缓存的好处非常多,比如用户的浏览文章、播放视频的进度(看了哪些文章,给个特别的样式,免得用户不知道看到哪里了)、用户的登录信息(用户登录一次,可以很长时间不用再登录)、自定义的模板样式(用户选择自己喜欢的样式,下次打开小程序还是一样)、最经常使用的小图片(保存在缓存,下次打开小程序速度更快)等等。

logs的数据缓存处理

我们再回头看logs的缓存案例,在小程序app.js的生命周期函数onLaunch里输入以下代码,也就是在小程序初始化的时候就执行日志进行记录:

当我们不断编译,logs数组里面的记录会不断增加,增加的值都是时间戳。那如何把缓存里面的日志给渲染到页面呢?

在file.wxml里添加以下代码,由于logs是数组,我们使用列表渲染,这里有个数组的index值,由于index是从0开始记录,给index加1,符合我们日常使用习惯。

然后在file.js的data里初始化logs

然后再在file.js的生命周期函数onLoad里把缓存里的日志取出来通过setData赋值给data里的logs

结合前面所了解的map、模块化知识就不难理解上面的这段代码了。缓存有同步API和异步API的区别,结合之前我们了解的同步和异步的知识,看看缓存的同步API与异步API的区别。

注意:打开开发者工具调试面板的Storage标签页,小程序的缓存记录都会在这里可以直观的看到,调试时可以留意,这一点非常重要。