前阵子做后台的一个小需求:配置列表每行几张图片,点「编辑」传张新图、保存,列表就地刷新。功能看着平平无奇,结果测的时候发现传完图、保存成功,列表数据也重新请求过了,可那张图还是原来的图,有时候干脆裂了。但我只要刷新页面,新图立马就出来。
数据我翻来覆去看了,接口返回的 URL 一个不差;把那个 URL 单独贴到浏览器地址栏打开,图也好好的。但是列表还是原来的图。
先别急着看代码,有没有可能是资源缓存了?
这种「局部更新不行、整页刷新就行」的毛病,我第一反应是缓存。因为这两种刷新对图片资源根本不是一回事:
AJAX 局部更新,Vue 重渲染,<img> 的 src 变了才会去重新请求;没变的话,浏览器直接拿内存里那份,连请求都不发。而 F5 会把页面上的资源整个重新请求一遍。
顺着「是不是缓存」这条线,我打开 DevTools 的 Network 面板,保存之后盯着那张图的请求看------好家伙,压根没发新请求 ,那一行写着 (from memory cache)。到这儿基本实锤了:浏览器用的是缓存。
那为什么会命中缓存?再去对比保存前后的图片 URL,问题的根子就浮出来了------后端是把新图覆盖上传到了同一个固定地址 ,比如永远叫 tab1.png。前后 URL 一模一样。
这么一看整件事就顺了:
- 页面第一次加载,浏览器请求
tab1.png,把旧图缓存下来。 - 我换图保存,后端把存储上的
tab1.png覆盖成了新图,可地址没变。 - 前端重新拉列表,
<img>的src还是tab1.png,跟之前完全一样。 - 浏览器一看这地址有缓存,直接用旧的,不重新请求。
- 我按 F5,资源全部重新拉,新图才露面。
顺手也可以用 curl 看一眼这张图的响应头,确认它确实是可被缓存的:
arduino
curl -sI "https://xxx-oss.example.com/static/icons/tab1.png"
返回里如果带着 Cache-Control: max-age=...、Last-Modified、ETag 这类,就说明浏览器有充分理由把它缓存住、下次不再问服务器。配合 Network 面板里那句 (from memory cache),缓存这个结论就很稳了。
解法:让 URL 变一下
问题既然是「URL 没变、命中旧缓存」,思路就很直接:给图片地址拼一个会变的参数,骗浏览器这是张新图。
ini
https://xxx-oss.example.com/static/icons/tab1.png?t=1720857600000
对象存储(OSS / COS 之类)不认识 ?t= 这个参数,会忽略它、照样把图返回来;但浏览器看到 query 变了,就当成一个新 URL 去重新请求,自然就绕开了旧缓存。
具体到 Vue 2 + Element UI,就是加一个会变的时间戳,拉列表时刷新它,再用一个方法给图片地址拼上去:
kotlin
data() {
return {
list: [],
imgTs: Date.now(), // 图片缓存戳
}
},
methods: {
getList() {
this.loading = true
getList(this.query).then(res => {
if (res.success) {
this.list = res.value
this.imgTs = Date.now() // 每次拿到数据就换个戳
}
this.loading = false
})
},
imgSrc(url) {
if (!url) return url
return url + (url.includes('?') ? '&' : '?') + 't=' + this.imgTs
},
},
模板里用 imgSrc() 包一层就行:
ini
<img v-if="scope.row.iconUrl" :src="imgSrc(scope.row.iconUrl)" />
保存成功会走 getList(),imgTs 一变,图片 URL 就带上新的 ?t=...,浏览器乖乖重新请求,不用再按 F5 了。
imgSrc 是在模板里当方法调的,每次重渲染都会跑一遍;但只有 imgTs 真的变了、返回的 URL 才会变。所以效果就是「拉列表的时候强制重取,平时不折腾」。
几个可以多想一步的地方
为什么不干脆让后端换个随机文件名? 那样 URL 每次都变,前端确实不用加戳。但覆盖同名也有它的道理------地址稳定、不会攒一堆历史垃圾文件,挺常见的做法。所以前端这个戳更像是个通用兜底,不指望后端改约定,成本也低。
每次拉列表都重新下图,会不会浪费? 会。加了会变的 ?t= 之后,这些图就不走缓存了,每次刷列表都重新下。对这种图少、图小的配置页无所谓;要是大列表大图,就别每次都换戳,改成只在保存成功之后才换:
kotlin
save(data).then(res => {
if (res.success) {
this.imgTs = Date.now() // 只在更新后强制重取,平时走缓存
this.getList()
}
})
万一加了戳还是偶尔裂图? 那可能就不是缓存的锅,而是对象存储上传有延迟------保存接口在图还没完全写进存储时就返回了,前端立刻去请求,对象还不在。想区分很简单:保存后先别刷新,干等几秒,图会不会自己好?会自己好,就是上传延迟,得让后端「传完再返回」或者前端延后一点再拉列表;不会好,那才是缓存。
最后
这次卡壳,真正费时间的其实不是改代码,是「意识到该往缓存方向查」。后面再遇到这种莫名其妙的显示问题,我基本都会先手贱按个 F5------能好,那八成就是缓存或者资源加载层面的事,排查范围一下子就窄下来了。