javascript 使用Manifest V3将FormData/File对象从内容脚本传递到chrome扩展中的后台脚本

kq4fsx7k  于 2023-01-29  发布在  Java
关注(0)|答案(3)|浏览(222)

我正在构建一个chrome扩展,在其中从用户那里获取一个文件作为输入,并将其传递给background.js(manifest v3中的service worker)将其保存到我的后端。由于内容脚本阻止了跨源请求,我必须将其传递到我的background.js并使用FETCH API保存文件。当我将FormDataFile对象传递给chrome.runtime.sendMessage API时,它使用JSON序列化,而我在background.js中收到的是一个空对象。

//content-script.js

attachFile(event) {
 let file = event.target.files[0];

 // file has `File` object uploaded by the user with required contents. 
 chrome.runtime.sendMessage({ message: 'saveAttachment', attachment: file }); 
}
//background.js

chrome.runtime.onMessage.addListener((request, sender) => {
 if (request.message === 'saveAttachment') {
   let file = request.attachment; //here the value will be a plain object  {}
 }
});

甚至当我们从内容脚本传递FormData时也会发生同样的情况。
我参考了旧StackOverflow问题建议的多个解决方案,使用URL.createObjectURL(myfile);并将URL传递给我的background.js并获取相同的文件。然而FETCH API不支持blob URL获取,并且服务工作器中也不支持XMLHttpRequest(推荐here)。有人能帮助我解决这个问题吗?我被这种行为所阻碍。

mklgxw1f

mklgxw1f1#

目前只有Firefox可以直接传输这些类型,Chrome可能可以在future中实现。

变通方案1。

手动将对象的内容序列化为字符串,发送该字符串(如果长度超过64 MB消息大小限制,则可能分几条消息发送),然后在后台脚本中重新生成对象。下面是一个简化的示例,没有拆分,改编自Violentmonkey,比较慢(50 MB的编码和解码需要几秒钟),因此您可能希望编写自己的版本,在内容脚本中构建multipart/form-data字符串,并在后台脚本中直接发送它。s fetch

  • 内容脚本:
async function serialize(src) {
  const wasBlob = src instanceof Blob;
  const blob = wasBlob ? src : await new Response(src).blob();
  const reader = new FileReader();
  return new Promise(resolve => {
    reader.onload = () => resolve([
      reader.result,
      blob.type,
      wasBlob,
    ]);
    reader.readAsDataURL(blob);
  });
}
  • 后台脚本,在onMessage侦听器内:
const [body, type] = deserialize(message.body);
fetch(message.url, {
  body,
  headers: {
    'Content-Type': type, 
  },
}).then(/*........*/);
function deserialize([base64, type, wasBlob]) {
  const str = atob(base64.slice(base64.indexOf(',') + 1));
  const len = str.length;
  const arr = new Uint8Array(len);
  for (let i = 0; i < len; i += 1) arr[i] = str.charCodeAt(i);
  if (!wasBlob) {
    type = base64.match(/^data:(.+?);base64/)[1].replace(/(boundary=)[^;]+/,
      (_, p1) => p1 + String.fromCharCode(...arr.slice(2, arr.indexOf(13))));
  }
  return [arr, type];
}

解决方法2。

对通过web_accessible_resources公开的扩展中的html文件使用iframe。

iframe将能够做扩展所能做的一切,比如发出CORS请求。
File/Blob和other cloneable types可以通过postMessage从内容脚本直接传输。FormData不可克隆,但可以将其作为[...obj]传递,然后组装到new FormData()对象中。
它还可以通过navigator.serviceWorker消息传递将数据直接传递给后台脚本。
示例:参见that answer中的“Web消息(双向MessagePort)”。

gojuced7

gojuced72#

我有个更好的办法你可以把Blob存储在IndexedDB中。

// client side (browser action or any page)
import { openDB } from 'idb';

const db = await openDB('upload', 1, {
  upgrade(openedDB) {
    openedDB.createObjectStore('files', {
      keyPath: 'id',
      autoIncrement: true,
    });
  },
});
await db.clear('files');

const fileID = await db.add('files', {
  uploadURL: 'https://yours3bucketendpoint',
  blob: file,
});

navigator.serviceWorker.controller.postMessage({
  type: 'UPLOAD_MY_FILE_PLEASE',
  payload: { fileID }
});

// Background Service worker
addEventListener('message', async (messageEvent) => {
  if (messageEvent.data?.type === 'UPLOAD_MY_FILE_PLEASE') {
    const db = await openDB('upload', 1);
    const file = await db.get('files', messageEvent.data?.payload?.fileID);
    const blob = file.blob;
    const uploadURL = file.uploadURL;
    
    // it's important here to use self.fetch
    // so the service worker stays alive as long as the request is not finished
    const response = await self.fetch(uploadURL, {
      method: 'put',
      body: blob,
    });
    if (response.ok) {
      // Bravo!
    }
  }
});
tyky79it

tyky79it3#

我发现了另一种方法来将文件从内容页面(或从弹出页面)传递给服务工作者。但是,可能,它并不适合所有情况,
你可以在服务工作器中拦截从内容或弹出页面发送的获取请求,然后你可以通过服务工作器发送这个请求,它也可以被修改
popup.js:

// simple fetch, but with a header indicating that the request should be intercepted
fetch(url, {
    headers: {
        'Some-Marker': 'true',
    },
});

background.js:

self.addEventListener('fetch', (event) => {
    // You can check that the request should be intercepted in other ways, for example, by the request URL
    if (event.request.headers.get('Some-Marker')) {
        event.respondWith((async () => {
            // event.request contains data from the original fetch that was sent from the content or popup page.
            // Here we make a request already in the background.js (service-worker page) and then we send the response to the content page, if it is still active
            // Also here you can modify the request hoy you want
            const result = await self.fetch(event.request);
            return result;
        })());
    }
    return null;
});

相关问题