背景
H5 中有图片展示+保存按钮的业务场景
点击保存图片会转 base64 以通过协议传给客户端进行保存,因此需要 js 动态载入 img 标签,并加上 crossOrigin=anonymous
,后续在 canvas 中处理才不会因为跨域污染而出问题
现象
在生产环境真机下,图片能正常显示,但点击保存会触发 onload error,导致图片保存失败
(CDN 资源平台已配置允许跨域)
调试
抽离出 getImage
单独尝试:
const getImage = (imageUrl, isCrossDomain) => {
const image = new Image();
isCrossDomain && image.setAttribute('crossOrigin', 'anonymous');
image.onload = () => console.log('ok');
image.onerror = () => console.log('err');
image.src = imageUrl;
}
发现其它图片资源都没问题,只有已经触发过 error 的图片会持续出错,并且重启 App 也是如此
卸载重装 App 后现象才有所变化,在多次重装尝试后终于发现规律:
- 在图片资源没有放到组件中点击“保存”之前,单独
getImage
都是成功的,但只要放到业务组件展示过了,即使重启 App 也不会再getImage
成功 - 卸载重装 App 后,原来不行的图片就又有重新做图的机会了
于是可以明确问题与缓存相关
原因
导致该现象的是浏览器的缓存机制,已经加载过的图片资源,再次请求会直接命中缓存,即使前后的请求有不同的 crossOrigin
设置也无济于事
(早在14年就有人向 chorme 提出该 issue,但官方表示不认为是 bug,不准备解决)
因此若图片先在 <img>
中展示,则已经有一次普通的图片请求发出了,后续点击保存图片时,再发出一次 crossOrigin=anonymous
的请求会命中缓存
而有无 crossOrigin=anonymous
的区别在于请求头的 origin
、sec-fetch-mode
字段不同
其中重点是 origin
,只有请求头中含有该字段时,CDN 服务器返回的响应头中才含有 access-control-allow-origin
等
因此在点击保存图片时,命中缓存中的请求的响应头中并无跨域相关的 header,于是被浏览器视为非法跨域请求拦截
实践发现,去掉组件中的 <img>
图片展示,或者在图片还没加载完之前立刻点击保存图片,确实便可以成功保存
解决方案
每次加上随机数
在 getImage
的 url 后拼上随机数,以避开缓存:
image.src = imageUrl + '?time=' + new Date().valueOf()
加载图片的 img 标签也加上 crossOrigin 字段
只要第一次的图片请求也有跨域相关的响应头,后续命中缓存也不会有问题
另有两个较不切实际的方式
- 配置 CDN 服务永远返回带有 cross-origin 的 headers
- 禁用缓存