1.问题背景
项目中图片上传的功能,需要在el-table中展示,并且点击编辑需要在el-upload中重新展示上传的图片; 图片并不是保存在数据库中,而是保存在miniIO中(miniIO是一个文件存储服务器,文件上传后存储在根据日期划分的文件夹中,比如/20250331/abc123_fhgjiouyyAdf-ffeegkki.jpg),文件上传成功后,接口返回的是类似这样的数据
{
code:'0000',
data:{
fileName:'1.jpg',
url:'/20250331/abc123_fhgjiouyyAdf-ffeegkki.jpg'
}
}
意思就是接口只返回文件在miniIO中地址; 现在图片上传成功在表格中创建一条数据后,在表格中无法展示,并且点击编辑,el-upload组件汇总无法回显图片;
2.原因分析
(1)表格中无法展示图片 表格中使用el-image标签展示图片
<template slot-scope="scope">
<el-image :src="scope.row.url" style="width:50px;height:50px;" fit="contain"/>
</template>
在js代码中打印url的格式是
http://localhost/data/jjjj/20250331/abc123_fhgjiouyyAdf-ffeegkki.jpg
明显请求的路径地址不对,本地调试的时候需要代理到服务器地址 (2)图片请求需要携带headers 由于后端表格数据返回的只是图片在miniIO上的文件地址,而不是一个blob类型的数据,所以想要获取图片,其实还需要再次请求后端的地址来获取blob类型的数据; el-image的底层使用的还是img标签,当img标签的src属性中是一个图片地址的时候(网络地址),它会通过浏览器向这个网络地址发起一个请求,请求图片的资源; 但是后端为了安全,对每个请求做了限制,需要在headers中携带Authorization,但是img标签请求图片资源是浏览器控制的,它不会经过axios,所以在axios的请求拦截器中添加的headers操作不会实现在img请求中; (3)el-upload无法复现图片 el-upload组件有一个属性header,但是它只能在发起上传图片请求的时候携带在请求中,复现图片主要依靠的是将file放入file-list中,这样可以在el-ipload中展示图片; el-upload底层也是使用的img标签,这样和上一个原因一样,也是因为再次请求图片资源的时候没有携带Authorization导致图片资源没有下载下来导致无法展示;
3.解决办法
(1)后端修改对图片资源请求接口的限制
可以做,但是不能做,原因都懂的
(2)使用serviceWorker来代理img请求
有限制:serviceWorker只能在 http://localhost 和 https:// 中使用,这意味着当你的项目线上环境的访问ip是http开头的时候,这种方式就不能使用; 1)新建serviceWorker文件:public/sw.js
// sw.js
let authToken = null;
self.addEventListener('message', (event) => {
if (event.data.type === 'SET_TOKEN' || event.data.type === 'UPDATE_TOKEN') {
authToken = event.data.token;
}
});
self.addEventListener('fetch', (event) => {
if (authToken && event.request.url.endsWith('.jpg')) {
const newHeaders = new Headers(event.request.headers);
newHeaders.set('Authorization', `Bearer ${authToken}`);
event.respondWith(
fetch(new Request(event.request, { headers: newHeaders }))
);
}
});
2)在msin.js中注册serviceWorker并且传递token
if ('serviceWorker' in navigator) {
// 注册时传递 Token
navigator.serviceWorker.register('/sw.js').then(registration => {
// 从存储中获取 Token
const token = localStorage.getItem('auth_token') ||
getCookie('auth_token');
// 发送给 Service Worker
registration.active?.postMessage({
type: 'SET_TOKEN',
token
});
});
// 监听存储变化实时更新
window.addEventListener('storage', () => {
navigator.serviceWorker.controller?.postMessage({
type: 'UPDATE_TOKEN',
token: localStorage.getItem('auth_token')
});
});
}
(3)笨办法:在url放入img标签前重新请求图片资源
1)在el-table中,获取到表格数据后,取出每条数据中的url字段,使用axios、fetch、XMLHttpRequest进行请求,封装一个requestImgUrl方法
async function requestImgUrl(url){
const response =await axios.get(
url:url,
{
responseType:'blob',
headers:{
Authorization:"bearer"+getToken(),//getToken是封装的获取Cookies或者sessionStorage中存储的token的方法
}
}
)
return URL.createObjectURL(response.data)
}
在获取表格数据时进行遍历
for(let item of res.data)
item.url = await requestImgUrl(item.url)
}
2)el-upload中也是同样的方法,这是需要放入file-list中
<el-upload
...
:file-list="fileList"
...
/>
async function editData(row){
...
if(row.url){
let blobData = await requestImgUrl(row.url)
fileList.vlaue = [{
url:blobData
}]
}
...
}
(4)聪明办法:MutationObserver监听img标签的加载
可以全局使用,避免每段代码都需要写请求逻辑,屏蔽了细节,但是在页面节点过多的时候,可能会有性能问题(节点>1000)
import { getToken } from "@/utils/auth";
// 初始化MutationObserver
const initObserver = () =>{
const observer = new MutationObserver(handleMutations);
observer.observe(document, {
childList: true,
subtree: true,
});
}
// 防抖处理Mutation回调
const handleMutations = (mutations, observer) =>{
// 监听所有的img标签
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === 'IMG') {
processImage(node)
}
if (node.nodeType === Node.ELEMENT_NODE && node.querySelectorAll) {
node.querySelectorAll('img').forEach((item) => {
processImage(item)
})
}
});
}
});
}
// 核心图片处理逻辑
const processImage = async (img)=> {
const originalSrc = img.src;
if (!originalSrc || originalSrc.startsWith('blob:') || originalSrc.endsWith('/login')) return;
try {
// 发起带认证的请求
const blob = await fetchImageWithAuth(originalSrc);
// 替换为Blob URL
const blobUrl = URL.createObjectURL(blob);
img.src = blobUrl;
} catch (error) {
console.error('图片加载失败:', originalSrc, error);
}
}
// 带认证的图片请求
const fetchImageWithAuth = async (url)=> {
const response = await fetch(url, {
headers: {
'Authorization': 'bearer ' + getToken(),
},
mode: 'cors',
credentials: 'include'
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.blob();
}
// 初始化拦截器
document.addEventListener('DOMContentLoaded', () => {
initObserver()
});