0

我正在构建一个 chrome 扩展,在其中我从用户那里获取一个文件作为输入,并将其传递给我的 background.js(在清单 v3 的情况下为服务工作者)以将其保存到我的后端。由于内容脚本阻止了跨域请求,因此我必须将其传递给我background.js并使用 FETCH API 来保存文件。当我将FormDataorFile对象传递给chrome.runtime.sendMessageAPI 时,它使用 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推荐的 service worker 也不支持。有人可以帮我解决这个问题吗?我被这种行为挡住了。

4

1 回答 1

1

目前只有 Firefox 可以直接传输此类类型。Chrome将来可能会做到这一点。

解决方法 1。

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

  • 内容脚本:

    async function serialize(src) {
      const cls = Object.prototype.toString.call(src).slice(8, -1);
      switch (cls) {
        case 'FormData': {
          return {
            cls,
            value: await Promise.all(Array.from(src.keys(), async key => [
              key,
              await Promise.all(src.getAll(key).map(serialize)),
            ])),
          };
        }
        case 'Blob':
        case 'File':
          return new Promise(resolve => {
            const { name, type, lastModified } = src;
            const reader = new FileReader();
            reader.onload = () => resolve({
              cls, name, type, lastModified,
              value: reader.result.slice(reader.result.indexOf(',') + 1),
            });
            reader.readAsDataURL(src);
          });
        default:
          return src == null ? undefined : {
            cls: 'json',
            value: JSON.stringify(src),
          };
      }
    }
    
  • 背景脚本:

    function deserialize(src) {
      switch (src.cls) {
        case 'FormData': {
          const fd = new FormData();
          for (const [key, items] of src.value) {
            for (const item of items) {
              fd.append(key, deserialize(item));
            }
          }
          return fd;
        }
        case 'Blob':
        case 'File': {
          const { type, name, lastModified } = src;
          const binStr = atob(src.value);
          const arr = new Uint8Array(binStr.length);
          for (let i = 0; i < binStr.length; i++) arr[i] = binStr.charCodeAt(i);
          const data = [arr.buffer];
          return src.cls === 'file'
            ? new File(data, name, {type, lastModified})
            : new Blob(data, {type});
        }
        case 'json':
          return JSON.parse(src.value);
      }
    }
    

解决方法 2。

使用指向通过 web_accessible_resources 公开的扩展中的 html 文件的 iframe。iframe 将能够做任何扩展可以做的事情,比如发出 CORS 请求。File/Blob 和其他可克隆类型可以通过postMessage直接从内容脚本传输。这些消息会暴露给页面中运行的任何脚本,因此我们必须使用 chrome.runtime 消息添加请求的授权,这是安全的(直到有人找到通过诸如 Spectre 之类的旁道攻击破坏内容脚本的方法) .

警告!该站点(或其他扩展程序)可以随时删除 iframe。

  • 清单.json:

    {
      "web_accessible_resources": [{
        "resources": ["sender.html"],
        "matches": ["<all_urls>"],
        "use_dynamic_url": true
      }],
      "host_permissions": [
        "https://your.backend.api.host/"
      ],
    

    请注意,use_dynamic_url目前尚未实施。

  • 内容.js:

    var iframe;
    /**
     * @param {string} url
     * @param {'text'|'blob'|'json'|'arrayBuffer'|'formData'} [type]
     * @param {FetchEventInit} [init]
     */
    async function makeRequest(url, type = 'text', init) {
      if (!iframe || !document.contains(iframe)) {
        iframe = document.createElement('iframe');
        iframe.src = chrome.runtime.getURL('sender.html');
        iframe.style.cssText = 'display: none !important';
        document.body.appendChild(iframe);
        await new Promise(resolve => (iframe.onload = resolve));
      }
      const id = `${Math.random}.${performance.now()}`;
      const fWnd = iframe.contentWindow;
      const fOrigin = new URL(iframe.src).origin;
      fWnd.postMessage('authorize', fOrigin);
      await new Promise(resolve => {
        chrome.runtime.onMessage.addListener(function _(msg, sender, respond) {
          if (msg === 'authorizeRequest') {
            chrome.runtime.onMessage.removeListener(_);
            respond({id, url});
            resolve();
          }
        });
      });
      fWnd.postMessage({id, type, init}, fOrigin);
      return new Promise(resolve => {
        window.addEventListener('message', function onMessage(e) {
          if (e.source === fWnd && e.data?.id === id) {
            window.removeEventListener('message', onMessage);
            resolve(e.data.result);
          }
        });
      });
    }
    
  • 发件人.html:

    <script src="sender.js"></script>
    
  • 发件人.js:

    const authorizedRequests = new Map();
    window.onmessage = async e => {
      if (e.source !== parent) return;
      if (e.data === 'authorize') {
        chrome.tabs.getCurrent(tab => {
          chrome.tabs.sendMessage(tab.id, 'authorizeRequest', r => {
            authorizedRequests.set(r.id, r.url);
            setTimeout(() => authorizedRequests.delete(r.id), 60e3);
          });
        });
      } else if (e.data?.id) {
        const {id, type, init} = e.data;
        const url = authorizedRequests.get(id);
        if (url) {
          authorizedRequests.delete(id);
          const result = await (await fetch(url, init))[type];
          parent.postMessage({id, result}, '*', type === 'arrayBuffer' ? [result] : []);
        }
      }
    };
    
于 2021-08-11T05:57:45.577 回答