- config.js 新增 MEDIA_CACHE_BUST 常量,换媒体后改此值即可强制客户端重下 - gif-player _load 给 src 追加 cb 参数,避免覆盖 720 动图后仍显示微信缓存的旧 180 图
125 lines
4.7 KiB
JavaScript
125 lines
4.7 KiB
JavaScript
// gif-player:微信 <image> 组件不播放 GIF 动画(只渲染首帧),
|
||
// 这里用 Canvas 2D + 纯 JS 解码器(omggif)逐帧绘制,实现真正的动图播放。
|
||
const { GifReader } = require('../../utils/vendor/omggif.js');
|
||
const config = require('../../config.js');
|
||
|
||
Component({
|
||
properties: {
|
||
// GIF 地址(通常是 ex.gifUrl,超分后的清晰动图)。
|
||
// 组件可能在 attached 之后才拿到 src(父页面接口异步返回),用 observer 兜底重新加载。
|
||
src: {
|
||
type: String,
|
||
value: '',
|
||
observer(value) {
|
||
if (value) this._load();
|
||
}
|
||
}
|
||
},
|
||
data: {
|
||
failed: false,
|
||
loading: true
|
||
},
|
||
lifetimes: {
|
||
attached() {
|
||
this._aborted = false;
|
||
this._timer = null;
|
||
this._reader = null;
|
||
this._ctx = null;
|
||
this._rgba = null;
|
||
this._frame = 0;
|
||
this._loadedSrc = '';
|
||
if (this.data.src) this._load();
|
||
},
|
||
detached() {
|
||
// 仅 detached 时彻底停止;不要把 _aborted 留给异步回调误判
|
||
this._aborted = true;
|
||
if (this._timer) { clearTimeout(this._timer); this._timer = null; }
|
||
}
|
||
},
|
||
methods: {
|
||
// 清除上一次可能的循环定时器(不动 _aborted 标志,否则会误杀正在进行的异步请求)
|
||
_clearTimer() {
|
||
if (this._timer) { clearTimeout(this._timer); this._timer = null; }
|
||
},
|
||
_load() {
|
||
const raw = this.data.src;
|
||
if (!raw || this._aborted) return;
|
||
// 击穿微信 GET 缓存:服务器静态文件被覆盖新版本(如重做超分动图)后,
|
||
// 旧图仍可能被微信请求缓存导致客户端显示旧图。加固定 bust 参数强制重新下载。
|
||
const sep = raw.indexOf('?') >= 0 ? '&' : '?';
|
||
const src = raw + sep + 'cb=' + config.MEDIA_CACHE_BUST;
|
||
// 同一 src(含 bust)且已解码完成则跳过,避免 attached 与 observer 重复触发
|
||
if (this._loadedSrc === src && this._reader) return;
|
||
this._loadedSrc = src;
|
||
this._clearTimer();
|
||
this.setData({ loading: true, failed: false });
|
||
wx.request({
|
||
url: src,
|
||
responseType: 'arraybuffer',
|
||
success: (res) => {
|
||
// detached 之后才允许放弃
|
||
if (this._aborted) return;
|
||
try {
|
||
this._reader = new GifReader(new Uint8Array(res.data));
|
||
this._start();
|
||
} catch (e) {
|
||
console.error('[gif-player] decode fail', e);
|
||
this.setData({ failed: true, loading: false });
|
||
}
|
||
},
|
||
fail: () => {
|
||
if (!this._aborted) this.setData({ failed: true, loading: false });
|
||
}
|
||
});
|
||
},
|
||
// 等待 Canvas 2D 节点就绪(自定义组件 attached 时可能还没布局好,最多重试 15 次)
|
||
_start(attempt = 0) {
|
||
if (this._aborted || !this._reader) return;
|
||
const q = wx.createSelectorQuery().in(this);
|
||
q.select('#gifCanvas').fields({ node: true, size: true }).exec((rs) => {
|
||
if (this._aborted) return;
|
||
if (!rs || !rs[0] || !rs[0].node) {
|
||
if (attempt < 15) setTimeout(() => this._start(attempt + 1), 60);
|
||
return;
|
||
}
|
||
const canvas = rs[0].node;
|
||
const ctx = canvas.getContext('2d');
|
||
const w = this._reader.width;
|
||
const h = this._reader.height;
|
||
// 画布内部分辨率 = GIF 原始分辨率(已超分到 720);CSS 负责缩放到容器
|
||
canvas.width = w;
|
||
canvas.height = h;
|
||
this._ctx = ctx;
|
||
this._rgba = new Uint8ClampedArray(w * h * 4);
|
||
this._frame = 0;
|
||
this._render();
|
||
});
|
||
},
|
||
_render() {
|
||
if (this._aborted || !this._reader || !this._ctx) return;
|
||
const r = this._reader;
|
||
const n = r.numFrames();
|
||
if (!n) { this.setData({ failed: true, loading: false }); return; }
|
||
try {
|
||
// decodeAndBlitFrameRGBA 按 GIF 的 disposal 规则在 rgba 上累积绘制
|
||
r.decodeAndBlitFrameRGBA(this._frame, this._rgba);
|
||
const img = this._ctx.createImageData(r.width, r.height);
|
||
img.data.set(this._rgba);
|
||
this._ctx.putImageData(img, 0, 0);
|
||
} catch (e) {
|
||
console.error('[gif-player] frame fail', e);
|
||
this.setData({ failed: true, loading: false });
|
||
return;
|
||
}
|
||
// 首帧渲染完成,关闭加载提示
|
||
if (this.data.loading) this.setData({ loading: false });
|
||
const info = r.frameInfo(this._frame);
|
||
// omggif 的 delay 单位是 1/100 秒,需 ×10 转毫秒;缺失时给 100ms
|
||
const delay = (info && info.delay) ? info.delay * 10 : 100;
|
||
this._frame = (this._frame + 1) % n;
|
||
// 单帧 GIF 只画一次;多帧才循环。最小 40ms 避免极端快帧
|
||
if (n > 1) this._timer = setTimeout(() => this._render(), Math.max(40, delay));
|
||
}
|
||
}
|
||
});
|