视频链接:
2.14 image 组件:如何实现图片的懒加载?
片 1
上节课我们学习了页面导航组件,以及如何自定义实现导航栏。这节课我们学习 image 组件,这是一个最常使用的媒体组件之一,这个组件本身它就有一个 lazy-load 属性,这个属性已经实现了图片的懒加载功能,那么,我们为什么还要自定义实现呢,这节课我们就看一看这个问题。
首先我们先看看一些与 image 组件有关的技术问题。
什么是 webp?
片 2
webp 是 image 组件的一个布尔属性,开启这个属性之后,代表 url 可以设置 webp 格式的图片。
WebP【发音:weppy】是一种同时提供了有损压缩,与无损压缩的,并且是可逆压缩的图片文件格式。这种文件格式是由 Google 推出的。
image 组件默认是不解析 webP 格式的,它只支持网络图片资源,只有开启了 webp 这个属性后,才可以解析 webp 图片网址。
那么我们为什么要用 webp 呢,它有什么优势呢?
WebP 的优势体现在,它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,同时它还同时具备了无损和有损的压缩模式、Alpha 透明,以及动画的特性,对 JPEG 和 PNG 图片格式的转化效果都是相当优秀的。
有科技博客曾经有报道,YouTube 上的视频略缩图,在采用 WebP 格式以后,网页加载速度提升了 1/10;谷歌的 Chrome 网上应用商店,采用 WebP 这种格式以后,每天节省了几 TB 的带宽。
WebP 既可以替代 jpg、png 图片,还可以替代 gif 图片。国内有许多公司,包括大厂,现在都在使用这种格式了。
智图:
片 3
对于不是 webp 格式的图片,有什么简单的方法把它转成 webp 格式吗?
答案是有的,上面这个就是一个在线的转换网址。
对于转换后的 webp 图片,直接用谷歌浏览器就能打开,谷歌浏览器本身就支持打开这种 webp 格式。
如何理解 show-menu-by-longpress 这个属性?
片 4
show-menu-by-longpress,这是 image 组件的一个布尔属性,开启它,代表开启长按图片显示,识别小程序码的菜单。
片 5
但是这种识别,仅是在 wx.previewImage 中,预览图片的时候,才可以识别(见上面)。在真机设备上测试,直接在 image 图片上长按,是没有效果的,但在微信开发者工具中长按也是有效果的(见上面)。
官方的 image 组件已经有了一个 lazy-load 属性,为什么我们还要再另外自定义实现一个懒加载组件呢?
片 6
这也是我们这节课开始时就提出的问题。
官方 image 组件的 lazy 属性,它规定图片即将进入一定范围之内,这个范围是上、下三屏,在即将进入这个范围的时候,才开始加载图片。
那么,上下三屏,其实是一个很大的空间,一般情况下呢,这会导致我们的图片在页面加载时,第 1 次就可能加载几十张图片。
片 7
从上面这张截图可以看出(见上面),使用开启了 lazy-load 属性的 image 组件,第一页什么操作也不做,一上来,图片请求就发出了 31 次。
并且,image 组件提供的图片懒加载功能,限制很多,它只针对 page 与 scroll-view 下面的 image 组件有效。
为了验证我们在 Console 面板里看到的加载次数情况,我们可以使用微信开发者工具里面的 Audits 面板,这个体验评分面板,我们可以使用它运行体验评分。
片 8
在评分结果中(见上面),我们看到,性能评分稍低,只有 77 分,程序在短时间内请求了 34 次图片。这还是使用了 lazy-load 属性之后的测试结果。
因为官方组件 image,虽然有一个 layz-load 属性,但由于种种原因,我们仍然需要自定义实现一个。
(可以从本课源码中查看示例,原生组件示例和自定义组件示例,以及运行效果)
mina-lazy-image 它是怎么实现的?
片 9
刚才我们看到的自定义实现的 mina-lazy-image 的运行效果。那么,它是怎么实现的呢?
主要原理,是使用 wx.createIntersectionObserver() ,使用这个接口创建了一个 IntersectionObserver 实例,在这里 Intersection 的意思是交叉,IntersectionObserver 可以理解为交叉监测,用这个实例判断,图片是否出现在视图窗口之中,进入窗口后再进行加载。
relativeTo(string selector, Object margins)
relativeToViewport(Object margins)
observe(string targetSelector, callback)
disconnect()
片 10
我们现在看一下,这个 IntersectionObserver 对象,它主要用于推断某些节点是否可以被用户看见、有多大比例可以被用户看见。它有四个方法。
第一个方法 relativeTo,这个方法使用选择器,指定一个组件节点作为参照区域。这个选择器,可以是 id 选择器,也可以是类选择器。
第二个方法 relativeToViewport,指定页面的视窗显示区域,作为交叉判断的参照区域。
第三个方法 observe,用选择器指定目标节点,并开始监听交叉状态的变化情况,变化情况会在 callback 回调函数中返回去。
第四个方法 disconnect,代表监听完成,现在停止监听,回调函数不再触发。
left number
right number
top number
bottom number
片 11
其中第二个方法 relativeToViewport,它的参数是一个对象,这个对象描述视图窗口的边界,共有上面 4 个字段。其中 left 是区域的左边界,right 是区域的右边界,top 是区域的上边界,bottom 是区域的下边界。这 4 个边界在使用的时候没有必要全部指定,可以只指定一个。在我们查看的 mina-lazy-image 组件的源码里面,它是只定义了 bottom 下边界。
miniprogram/miniprogram_npm/mina-lazy-image/index.js:
module.exports =
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/ ...
/***/ (function(module, exports, __webpack_require__) {
片 12
现在我们看一看 mina-lazy-image 组件的源码实现。index.js 这个文件是由 webpack 根据模块源码构建出来的,在文件的开始有这样一大段像乱码一样的内容,每行前面都有注释符。
这段内容基本上在所有通过 npm 安装的模拟目标代码中都是存在的,每个模块都有,这是 webpack 自己实现的一套 module 机制,包括__webpack_exports__ 和__webpack_require__。这种模块机制它把每个模块的真正源码,包裹在一个函数里,同时实现在模块间实现引用和导出。
对于这一大段代码,我们了解它的作用即可,它大概就是为了实现模块化。接下来我们继续看这个组件它的懒加载是怎么实现的。
miniprogram/miniprogram_npm/mina-lazy-image/index.js:
addObserver: function addObserver() {
...
var observer = this.createIntersectionObserver();
observer.relativeToViewport(this.properties.viewport).observe('.lazy-image-comp', () => {
this.setData({
showed: true
});
this.clean();
});
this.observer = observer;
return true;
}
viewport: {
type: Object,
value: {
bottom: 0
}
}
片 13
在这段代码里,首先通过 this.createIntersectionObserver()创建了一个 IntersectionObserver 实例,这是在组件内创建的方法,在组件外可以用 wx.createIntersectionObserver()创建,但在组件内,要用 this.createIntersectionObserver()这个方法创建。
接着,使用 relativeToViewport 方法,将参考区域绑定到 bottom:0 的视图窗口之上,也就是从页面底部算起,只要图片区域出现了就开始加载。再接着通过 observe 方法,开始监听.lazy-image-comp 这个组件,这是我们自定义组件的顶层样式名称。
(可以从本课源码中查看示例及运行效果)
结束语
下节课我们来看一下,image媒体组件在开发中经常会遇到哪些问题。
小程序官方组件 image 的 lazy-load 为什么有的时候不起作用?
片 14
文档上说,这个属性只针对 page 与 scroll-view 下的 image 有效。那这句话怎么理解呢?
每个页面最外层都是 page 啊,每个 image 不都在 page 里面吗?
在这里指的是直接位于 page 容器下面,或者位于 scroll-view 容器下面。文档上又说,小程序懒加载需要在图片进入一定范围之内,也就是上下三屏的时候,才开始加载。 这也就意味着,当你的图片不足三屏的时候,所有 image 节点都在范围之内的,这时候很有可能一次都加载完了。
为什么有时候图片链接可正常访问,但 image 组件加载不出来图片?
片 15
image 控件拉取图片的本质,是使用 wx.downloadFile 这个接口加载。很多时候是由于图片格式不规范,例如线上的 SSL 证书有问题,文件描述信息例如 content-type、length 等信息不标准,还有可能服务器发生了 302 跳转等原因,导致拉取不成功,表现的现象就是图片加载不出来。
有时候网络不好,加载超时了,也不会显示,并不是因为图片不可以访问。同样的图片我们使用浏览器加载,可能就是显示的。现在小程序的 image 组件,已经支持 webp 这种图片格式了,在之前不支持的时候,如果给它的 src 属性设置一个 webp 的图片网址,它也是不能显示的。
对于网络不好的这种情况,我们可以用 image 组件的 binderror 处理,监听到错误以后,重新赋值,一般就可以解决这种问题。
这里面有一个关于 302 的错误,我们展开看一下。
我们浏览网页的时候,浏览器在不断地在接收服务器端的应答,从而决定下一步做什么,这个应答就是状态码,在 http 协议里面,状态码是三位数字,这个状态码共分为五类,分别是为 1××,2××,3××,4××,5××,分别以它们开头的。
302 是服务器返回的一个 http 状态码。302 对应生活中的例子,可以类比手机的呼叫转移功能,我们给 A 打号码,但是移动把我们的呼叫转移到了 B 号码上面。
在服务器这边,接到前端的一个 http 请求的时候,前端想加载 A 页面,但是 A 页面因为网站改版,现在不存在了,取而代之的是 B 页面,这时候服务器可以返回 302 的状态码,并且同时返回 B 页面的地址。这就是 302 页面跳转。
涉及到页面跳转的 http 状态码,一共有两个,301 与 302,两者都是转向,不同之处在于,301 是网页永久转移到了新位置,302 是请求的网页临时转移到了新网址。
我们在微博上,经常看到短链接,其实就是利用了服务器端 302 跳转实现的。虽然短链接有跳转,但经过测试我们发现,目前小程序 image 是支持短链接的,这肯定是微信团队在组件内部已经做了处理了,因为我们单独拿 wx.downloadFile 接口去加载一个短链接图片的时候,是直接返回 403 错误的,403 代表资源不可用。
小程序的背景图片如何实现全屏,如何适配所有机型?
片 16
这是个好问题,因为机型不同,尺寸也不一样,用一张图片适配所有机型,是不太容易的。
原图
scaleToFill
aspectFit
aspectFill
片 17
image 组件有一个关于缩放的属性 mode,我们先看一下这个属性。这个属性经常使用的值有三个(见上面)。
片 18
第一个值 scaleToFill,代表不保持纵横比例缩放图片,使图片的宽高完全拉伸至填满整个 image 元素。
片 19
第二个值 aspectFit,代表保持纵横比例缩放图片,使图片的长边可以完全显示出来。也就是说,可以完整地将图片显示出来,而不会对图片有任何的裁剪。
片 20
第三个值 aspectFill,代表保持纵横比例缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在一个水平或垂直方向是完整的,在另一个方向将会发生裁剪。
但是由于 image 在加载图片时存在缺陷,实现背景图片适配所有机型这个事情,最好不要用mode属性去实现,最好还是由 wxss 样式来实现。
/* 背景图样式 */
.container {
position: fixed;
width: 100%;
height: 100%;
background-color:azure;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: -1;
}
.container::after {
content: "";
background: url( ) no-repeat center center;
background-size: cover;
opacity: 0.5;
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
}
片 21
我们看这段样式代码,通过 background 样式可以给页面设置一个背景,其中 background-size: cover,和 image 组件 mode 属性的 aspectFill 值的效果,是类似的。
那么,样式有了,还有一个问题,背景图我们做成多大合适呢?
抛开机型多、尺寸多的 Android 机型不说,现在在 iOS 上现在随着新版 iPhone 不断发布,iOS 尺寸也越来越多了,我们要用一个尽量覆盖所有屏幕比例的大小做背景图片。
一般情况下,我们设计的时候,新建一个 750×1334 的背景,分辨率设置为 72。用这样的尺寸做背景图片。
(可以从本课源码中查看示例及效果)
如何剪切图片?
片 22
例如有时候,在上传图片的时候,需要对头像图片做一些裁剪,这是它的应用场景。
片 23
上面这是有人实现的一个小程序组件(网址见上面),可以将图片通过拖拽的方式选择范围,任意裁剪。原理也很简单,四角的控制点是通过 view 渲染出来的。图片加载之后,绘制到 canvas 画布上,选定裁剪范围之后,通过 wx.canvasToTempFilePath 这个接口生成一个临时图片,这就是我们选择的结果。
(可以从本课视频中查看示例及运行效果)
好,这节课我们就讲到这里。这节课我们主要学习了 image 组件,了解了如何自定义实现一个懒加载的 image 组件。下节课我们学习 live-player 与 live-pusher 组件,了解一下在小程序中如何实现直接的功能。
2020 年 5 月 31 日