零、问题的由来
一般在前端展示图片时都会碰到这两个常见的需求:
- 图片未加载完成时先展示占位图,等到图片加载完毕后再展示实际的图片。
- 假如图片链接有问题(比如 404),依然展示占位图。甚至你还可以增加点击图片再次加载的功能。(例如知乎)
然鹅,小程序原生组件 image 并没有提供这些常用功能...

注:这里加了 2s 的延迟
一、常规操作
在小程序没还没推出自定义组件功能时,只能通过改变 Page 中的 data 来展示兜底的占位图,所以当时的处理方式十分蛋疼...
1.1.相同默认图
由于需要知道这个图片的数据路径,所以不得不在每个 image
上加上类似 data-img-path
的东西。
<view
wx:for="{{ obj.arr }}"
wx:key="imgSrc"
wx:for-item="item"
wx:for-index="itemIdx"
>
<image
src="{{ item.imgSrc }}"
binderror="onImageError"
data-img-path="obj.arr[{{ itemIdx }}].imgSrc"
/>
</view>
复制代码
const DEFAULT_IMG = '/assets/your_default_img'
Page({
data: {
obj: {
arr: [
{ imgSrc: 'your_img1' },{ imgSrc: 'your_img2' },],},onImageError ({
target: { dataset: { imgPath } },}) {
this.setData({
[imgPath]: DEFAULT_IMG,})
},})
复制代码
1.2.不同默认图
如果默认图片不同呢?例如球员、球队和 feed 的默认图片一般都是不同的。
那么一般只好再增加一个属性例如 data-img-type
来标识默认图的类型。
<image
...
data-img-type="team"
/>
<image
...
data-img-type="player"
/>
复制代码
const DEFAULT_IMG_MAP = {
feed: '/assets/default_feed',team: '/assets/default_team',player: '/assets/default_player',}
Page({
data: {
obj: {
arr: [
{ imgSrc: 'your_img1' },onImageError ({
target: { dataset: { imgPath,imgType } },}) {
this.setData({
[imgPath]: DEFAULT_IMG_MAP[imgType],})
复制代码
1.3.图片在模板中
页面层级浅倒还好,如果跨模板了,那么模板就可能要用一个类似于 pathPrefix
的属性来传递模板数据的路径前缀。
<template name="srPlayerRank">
<view
wx:for="{{ playerList }}"
wx:key="imgSrc"
wx:for-item="item"
wx:for-index="itemIdx"
>
<image
src="{{ item.imgSrc }}"
binderror="onImageError"
data-img-type="player"
data-img-path="{{ pathPrefix }}.playerList[{{ itemIdx }}].imgSrc"
/>
</view>
</template>
复制代码
最后在失败回调里调用 setData({ [path]: DEFAULT_IMG })
重新设置图片地址。
就问你蛋不蛋疼?这一坨 data-img-path="{{ pathPrefix }}.playerList[{{ itemIdx }}].imgSrc"
代码真让人无发可脱...

二、自定义组件
有了自定义组件后,用领袖【窃·格瓦拉】的话来说的话就是:“感觉好 door 了~”

2.1.原生自定义组件
原生写法一般要写4个文件:.json
/.wxml
/.js
/.wxss
{
"component": true
}
复制代码
<image
hidden="{{ !isLoading }}"
src="{{ errSrc }}"
style="width: {{ width }}; height: {{ height }}; {{ styleStr }}"
mode="{{ imgMode }}"
/>
<image
hidden="{{ isLoading }}"
src="{{ imgSrc || src }}"
mode="{{ imgMode }}"
style="width: {{ width }}; height: {{ height }}; {{ styleStr }}"
bindload="_onImageLoad"
binderror="_onImageError"
lazy-load="{{ true }}"
/>
复制代码
const DEFAULT_IMG = '/assets/your_default_img'
Component({
properties: {
src: String,
errSrc: {
type: String,
value: DEFAULT_IMG,width: {
type: String,value: '48rpx',height: {
type: String,
styleStr: {
type: String,value: '',
imgMode: {
type: String,value: 'scaleToFill',data: {
imgSrc: '',isLoading: true,methods: {
_onImageError (e) {
this.setData({
imgSrc: this.data.errSrc,})
this.triggerEvent('onImageError',e)
},
_onImageLoad (e) {
this.setData({ isLoading: false })
this.triggerEvent('onImageLoad',})
复制代码
布吉岛大家使用原生写法时有木有一些感觉不方便的地方:
- 4个文件:
.json
/.wxml
/.js
/.wxss
,这样老需要切来切去的降低效率
-
properties
是什么鬼?大家(React/Vue)一般不都用 props
么?
-
style="width: {{ width }}; height: {{ height }}; {{ styleStr }}"
样式字符串怎么辣么长...
2.2.TuaImage.vue
所以以下是一个使用单文件组件封装原生 image 组件的例子。
- 使用单文件组件将配置、模板、脚本、样式写在一个文件中,方便维护。
- 使用计算属性
computed
将样式字符串写在 js 中。
- 使用
this.imgSrc = this.errSrc
而不是 this.setData
来改变 data
。
<config>
{
"component": true
}
</config>
<template lang="wxml">
<image
hidden="{{ !isLoading }}"
src="{{ errSrc }}"
style="{{ imgStyleStr }}"
mode="{{ imgMode }}"
/>
<image
hidden="{{ isLoading }}"
src="{{ imgSrc || src }}"
mode="{{ imgMode }}"
style="{{ imgStyleStr }}"
bindload="_onImageLoad"
binderror="_onImageError"
lazy-load="{{ true }}"
/>
</template>
<script>
const DEFAULT_IMG = '/assets/your_default_img'
export default {
props: {
src: String,
default: DEFAULT_IMG,default: '48rpx',default: '',default: 'scaleToFill',data () {
return {
imgSrc: '',}
},computed: {
imgStyleStr () {
return `width: ${this.width}; height: ${this.height}; ${this.styleStr}`
},methods: {
_onImageError (e) {
this.imgSrc = this.errSrc
this.$emit('onImageError',
_onImageLoad (e) {
this.isLoading = false
this.$emit('onImageLoad',}
</script>
<style lang="scss">
</style>
复制代码
采用框架是 tua-mp:
相关文章:
- 终极蛇皮上帝视角之微信小程序之告别 setData
- 终极蛇皮上帝视角之微信小程序之告别“刀耕火种”