Files
fitness-coach/miniprogram/components/gif-player/gif-player.js
wm 8d584129ec fix: gif-player 加 cache-bust 击穿微信 GET 缓存
- config.js 新增 MEDIA_CACHE_BUST 常量,换媒体后改此值即可强制客户端重下
- gif-player _load 给 src 追加 cb 参数,避免覆盖 720 动图后仍显示微信缓存的旧 180 图
2026-07-24 16:31:12 +08:00

125 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 原始分辨率(已超分到 720CSS 负责缩放到容器
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));
}
}
});