img标签请求浏览器资源携带请求头

186 阅读3分钟

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()

});