update
This commit is contained in:
parent
a289ea61b5
commit
178c3a8179
@ -1,59 +1,127 @@
|
||||
<template>
|
||||
<view class="lazy-image-wrapper" :style="{ width: width, height: height }">
|
||||
<image :src="optimizedSrc" :style="{ width: width, height: height, opacity: imageLoaded ? 1 : 0 }" :mode="mode"
|
||||
@load="onImageLoad" @error="onImageError" :lazy-load="false" :show-menu-by-longpress="false" />
|
||||
<view class="lazy-image-wrapper" :style="wrapStyle">
|
||||
<!-- 仅在可视区才真正挂载 image,避免无意义的下载+解码 -->
|
||||
<image v-if="visible" class="img" :src="realSrc" :mode="mode" :lazy-load="true" webp="true"
|
||||
show-menu-by-longpress="false" :style="{ opacity: imageLoaded ? 1 : 0 }" @load="onImageLoad"
|
||||
@error="onImageError" />
|
||||
<view v-if="!imageLoaded && !showError" class="placeholder">
|
||||
<view class="loading-bg"></view>
|
||||
</view>
|
||||
<view v-if="showError" class="error-placeholder">
|
||||
<image src="/static/images/home/defaultIcon.png" :style="{ width: width, height: height }" :mode="mode" />
|
||||
<image class="fallback" src="/static/images/home/defaultIcon.png" :mode="mode" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'aspectFill'
|
||||
/** ===== 全局简单并发池(同域连接数受限时很有用) ===== */
|
||||
const POOL_MAX = 6;
|
||||
const queue = [];
|
||||
let loadingCount = 0;
|
||||
|
||||
function schedule(task) {
|
||||
if (loadingCount < POOL_MAX) {
|
||||
loadingCount++;
|
||||
task(() => {
|
||||
loadingCount--;
|
||||
const next = queue.shift();
|
||||
if (next) schedule(next);
|
||||
});
|
||||
} else {
|
||||
queue.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'LazyImage',
|
||||
props: {
|
||||
src: { type: String, default: '' },
|
||||
width: { type: String, default: '100%' },
|
||||
height: { type: String, default: '100%' },
|
||||
mode: { type: String, default: 'aspectFill' },
|
||||
/** 后端/CDN若支持按需裁剪,可传如 'w=360' 自动拼接到 url 上 */
|
||||
query: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
imageLoaded: false,
|
||||
showError: false
|
||||
}
|
||||
showError: false,
|
||||
visible: false,
|
||||
retry: 0,
|
||||
io: null,
|
||||
release: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
wrapStyle() {
|
||||
return `width:${this.width};height:${this.height};`;
|
||||
},
|
||||
optimizedSrc() {
|
||||
// 直接返回原图,避免无效参数
|
||||
return this.src || ''
|
||||
if (!this.src) return '';
|
||||
if (!this.query) return this.src;
|
||||
const joiner = this.src.includes('?') ? '&' : '?';
|
||||
return `${this.src}${joiner}${this.query}`;
|
||||
},
|
||||
// 只有在可视区且进入并发池后才真正给 <image> 一个 src
|
||||
realSrc() {
|
||||
return this.visible ? this.optimizedSrc : '';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// 视口观察:进入视区才 visible=true;离开且已加载则释放(长列表更稳)
|
||||
// #ifdef MP-WEIXIN
|
||||
this.io = this.createIntersectionObserver();
|
||||
this.io.relativeToViewport({ top: 200, bottom: 200 }).observe('.lazy-image-wrapper', (res) => {
|
||||
const inView = res?.intersectionRatio > 0;
|
||||
if (inView && !this.visible) {
|
||||
this.visible = true;
|
||||
// 进入视区后,放入并发池启动加载
|
||||
schedule((done) => {
|
||||
this.release = done;
|
||||
// realSrc 已绑定,微信会开始拉取
|
||||
});
|
||||
} else if (!inView && this.visible && this.imageLoaded) {
|
||||
// 离开视区:释放内存,下次滚回来再加载(已缓存,几乎瞬开)
|
||||
this.visible = false;
|
||||
this.imageLoaded = false;
|
||||
this.showError = false;
|
||||
this.retry = 0;
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.io) this.io.disconnect();
|
||||
},
|
||||
methods: {
|
||||
onImageLoad() {
|
||||
this.imageLoaded = true
|
||||
this.showError = false
|
||||
this.imageLoaded = true;
|
||||
this.showError = false;
|
||||
if (this.release) this.release();
|
||||
},
|
||||
onImageError() {
|
||||
this.showError = true
|
||||
this.imageLoaded = false
|
||||
}
|
||||
}
|
||||
if (this.release) this.release();
|
||||
if (this.retry < 2) {
|
||||
// 指数退避重试(常见于弱网/CDN偶发 499/超时)
|
||||
this.retry++;
|
||||
const delay = 200 * Math.pow(2, this.retry); // 200ms, 400ms
|
||||
setTimeout(() => {
|
||||
// 复位触发二次下载(依然通过并发池)
|
||||
this.imageLoaded = false;
|
||||
this.showError = false;
|
||||
schedule((done) => {
|
||||
this.release = done;
|
||||
// realSrc 不变,微信会再次尝试(有些机型需先置空再赋值,可按需启用下面两行)
|
||||
// this.visible = false;
|
||||
// this.$nextTick(() => (this.visible = true));
|
||||
});
|
||||
}, delay);
|
||||
} else {
|
||||
this.showError = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@ -61,42 +129,47 @@ export default {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
.img,
|
||||
.fallback {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: opacity .25s ease;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
.placeholder,
|
||||
.error-placeholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
background: #f5f5f5;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.error-placeholder {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.loading-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f0f0f0;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, .06), rgba(0, 0, 0, .12), rgba(0, 0, 0, .06));
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.2s infinite linear;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
|
||||
.error-placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
z-index: 2;
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
|
||||
image {
|
||||
display: block;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user