需求场景:上传图片后接口返回文件id
,展示图片时链接地址为/file/preview?id=xxx
。但系统中的接口都需要鉴权(即在请求header
中添加authCode
),而img
标签的请求是浏览器的默认行为,并不受我们的接口拦截(如axios
请求拦截)约束。
通常的解决方式:
手动发请求+处理流
以Vue
为例,我们可以封装一个自定义图片组件authImg
<template>
<img ref="img">
</template>
<script>
export default {
props:{
src:{
type: String,
default:''
}
},
watch:{
src:{
handler(n){
if(n){
this.getImage(n);
}
},
immediate:true
}
},
methods:{
getImage(url){
let that = this;
that.$nextTick(()=>{
let request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', url, true);
// 添加自定义请求头
request.setRequestHeader('authToken', localStorage.getItem('authToken'));
request.onreadystatechange = e => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
that.$refs.img.src = URL.createObjectURL(request.response);
that.$refs.img.onload = () => {
that.$refs.img && URL.revokeObjectURL(that.$refs.img.src);
}
}
};
request.send(null);
});
}
}
}
</script>
使用:
<template>
<authImg src="/file/preview?id=xxx"></authImg>
</template>
此方式可以处理绝大多数场景,但解决不了类似富文本中的img
问题。一般富文本会保存成字符串,数据回显时,很难处理其中的img
标签
通过自定义元素加载
这种方式就是扩展原有的HTML Element
,实现一些自定义的功能,示例如下
// 扩展原生Image
let requestImage = function (url, element) {
let request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', url, true);
request.setRequestHeader('authToken', localStorage.getItem('authToken'));
request.onreadystatechange = e => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
element.src = URL.createObjectURL(request.response);
element.onload = () => {
element && URL.revokeObjectURL(element.src);
}
}
};
request.send(null);
}
class AuthImg extends HTMLImageElement {
constructor() {
super();
this._lastUrl = '';
}
static get observedAttributes() {
return ['authsrc'];
}
connectedCallback() {
let url = this.getAttribute('authsrc');
if (url !== this._lastUrl) {
this._lastUrl = url;
requestImage(url, this);
}
console.log('connectedCallback() is called.');
}
}
window.customElements.define('auth-img', AuthImg, {extends: 'img'});
使用:
原始(报错401):<img src="/file/preview?id=xxx">
替换后:<img is="auth-img" authsrc="/file/preview?id=xxx"/>
此方式可以解决上述的富文本中的图片问题,以wangEdit
为例
// 自定义上传图片
this.editor.customConfig.uploadImgHooks = {
customInsert: function (insertImg, result, editor) {
let imgUrl = result.data[0];
that.editor.cmd.do('insertHTML', `<img is="auth-img" authsrc="${imgUrl}" alt="图片" /> `);
}
}
serviceWorker
我们真的没办法拦截到浏览器原生的img
请求吗?其实是可以的。serviceWorker
具体使用可自行查找资料,这里我们简单做个示例:
<!--index.html-->
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js');
}
</script>
//sw.js
self.addEventListener('fetch', function (evt) {
// 拦截指定请求前缀
if (evt.request.url.indexOf('image/view?id')>-1) {
// 拷贝原始请求
const request = evt.request.clone();
// 创建headers
const headers = new Headers();
// 添加自定义请求头
headers.append('authToken', '7c5c90e6-bf49-47c0-bda1-bc93255364d8');
// 创建新请求
const newRequest = new Request(request,{
headers,
mode: 'cors'
})
// 返回=>走浏览器原有逻辑
evt.respondWith(fetch(newRequest))
}
});
这样我们就不需要修改img
标签,或者做其它处理。通过serviceWorker
自动拦截对应请求,添加请求头,剩下的交给浏览器就可以了。
后记
第一、二种方式原则上差不多,第二种覆盖场景会多一些,但改动量都比较大。
第三种方式改动较小,基本上是全场景覆盖,如富文本,如css
背景图设置成接口地址;但自定义性较差,比如异常回调,加loading
之类的操作。
当然使用serviceWorker
,同样可以对以window.open
或者window.location.href
形式的下载链接拦截,添加自定义请求头,就不需要前端通过调用接口获取文件流再手动转换了。