如何从Chrome扩展程序传递一个blob到Chrome应用程序

16

一些背景信息

我已经花了几天时间开发一个Chrome扩展程序,可以每天多次对给定的网页进行截屏。我使用这个作为指南,一切都按照预期运行。

然而,扩展程序有一个小要求无法满足。用户必须能够访问保存图像(截屏)的文件夹,但Chrome扩展程序无法访问文件系统。相反,Chrome应用程序可以。因此,经过多方查找,我得出结论必须创建Chrome扩展程序和Chrome应用程序两者。我的想法是:扩展程序将创建截屏的blob,然后将该blob发送到应用程序,应用程序将其保存为用户指定位置的图像。这正是我正在做的——在扩展程序上创建截屏的blob,然后将其发送到应用程序,在那里用户被要求选择保存图像的位置。

问题

在保存部分之前,一切都按照预期工作。blob在扩展程序上创建,发送到应用程序,应用程序接收到,用户被要求选择保存位置,图像被保存……这就是问题所在。 生成的图像无法使用。 当我尝试打开它时,会出现一个消息,上面写着“无法确定类型”。下面是我正在使用的代码:

  1. First ON THE EXTENSION side, I create a blob and send it over, like this:

     chrome.runtime.sendMessage(
        APP_ID, /* I got this from the app */
        {myMessage: blob}, /* Blob created previously; it's correct */
        function(response) {
          appendLog("response: "+JSON.stringify(response));
        }
     );
    
  2. Then, ON THE APP side, I receive the blob and attempt to save it like this:

    // listen for external messages
    chrome.runtime.onMessageExternal.addListener(
      function(request, sender, sendResponse) {
        if (sender.id in blacklistedIds) {
          sendResponse({"result":"sorry, could not process your message"});
          return;  // don't allow this extension access
        } else if (request.incomingBlob) {
          appendLog("from "+sender.id+": " + request.incomingBlob);
    
          // attempt to save blob to choosen location
          if (_folderEntry == null) {
             // get a directory to save in if not yet chosen
             openDirectory();
          }
          saveBlobToFile(request.incomingBlob, "screenshot.png");
    
          /*
          // inspect object to try to see what's wrong
          var keys = Object.keys(request.incomingBlob);
          var keyString = "";
          for (var key in keys) {
             keyString += " " + key;
          }
          appendLog("Blob object keys:" + keyString);
          */
    
          sendResponse({"result":"Ok, got your message"});
        } else {
          sendResponse({"result":"Ops, I don't understand this message"});
        }
      }
    );
    

    Here's the function ON THE APP that performs the actual save:

    function saveBlobToFile(blob, fileName) {
      appendLog('entering saveBlobToFile function...');
      chrome.fileSystem.getWritableEntry(_folderEntry, function(entry) {         
        entry.getFile(fileName, {create: true}, function(entry) {         
          entry.createWriter(function(writer) {
            //writer.onwrite = function() {
            //   writer.onwrite = null;
            //   writer.truncate(writer.position);
            //};
            appendLog('calling writer.write...');
            writer.write(blob);                       
            // Also tried writer.write(new Blob([blob], {type: 'image/png'}));
          });
        });
      });
    }
    
没有错误,也没有任何问题。代码可以运行,但图像是无用的。我到底缺少了什么?出了什么问题?我们只能在扩展/应用程序之间传递字符串吗?blob在传输过程中被破坏了吗?我的应用程序无法访问blob,因为它是在扩展中创建的吗?请有人能帮忙解答一下吗?
更新(9/23/14): 抱歉更新晚了,因为我被分配到了一个不同的项目上,所以直到两天前才回来。
经过长时间的搜索,我决定采用@Danniel Herr的建议,他建议使用SharedWorker和嵌入到app中的框架中的页面。这个想法是扩展会将blob提供给SharedWorker,然后SharedWorker将blob转发到嵌入在app中的框架中的扩展中的页面。然后,该页面使用parent.postMessage(...)将blob转发到app。虽然有点繁琐,但似乎这是我唯一的选择。
让我放一些代码,这样就更容易理解了:
扩展:
var worker = new SharedWorker(chrome.runtime.getURL('shared-worker.js'));
worker.port.start();
worker.postMessage('hello from extension'); // Can send blob here too
worker.port.addEventListener("message", function(event) {
   $('h1Title').innerHTML = event.data;
});

proxy.js

var worker = new SharedWorker(chrome.runtime.getURL('shared-worker.js'));
worker.port.start();

worker.port.addEventListener("message",
   function(event) {      
      parent.postMessage(event.data, 'chrome-extension://[extension id]');
   }
);

proxy.html

<script src='proxy.js'></script>

shared-worker.js

var ports = [];
var count = 0;
onconnect = function(event) {
    count++;
    var port = event.ports[0];
    ports.push(port);
    port.start(); 

    /* 
    On both the extension and the app, I get count = 1 and ports.length = 1
    I'm running them side by side. This is so maddening!!!
    What am I missing?
    */
    var msg = 'Hi, you are connection #' + count + ". ";
    msg += " There are " + ports.length + " ports open so far."
    port.postMessage(msg);

    port.addEventListener("message",       
      function(event) {
        for (var i = 0; i < ports.length; ++i) {
            //if (ports[i] != port) {
                ports[i].postMessage(event.data);
            //}
        }
    });
};

在应用程序中
context.addEventListener("message", 
    function(event) {
        appendLog("message from proxy: " + event.data);
    } 
);

所以这是执行流程...在扩展中,我创建了一个共享工作者并向其发送了一条消息。共享工作者应该能够接收blob,但出于测试目的,我只发送了一个简单的字符串。
接下来,共享工作者接收到消息并将其转发给所有已连接的人。此时,在应用程序中的框架中的proxy.html/js确实连接,并且应该接收由共享工作者转发的任何内容。
接下来,proxy.js [应]从共享工作者接收到消息,并使用parent.postMessage(...)将其发送到应用程序。应用程序通过window.addEventListener("message",...)监听。
为了测试这个流程,我首先打开了应用程序,然后点击了扩展按钮。我没有在应用程序上收到任何消息。我也没有收到任何错误。
扩展可以与共享工作者往返通信。应用程序可以与共享工作者往返通信。然而,我从扩展->proxy->app发送的消息未到达应用程序。我错过了什么?
对不起,帖子很长,但我希望有人能给我一些提示,因为这让我发疯了。
谢谢
4个回答

10

感谢大家的帮助。我发现解决方法是在扩展程序中将 Blob 转换为二进制字符串,然后使用 Chrome 的消息传递 API 将字符串发送到应用程序。在应用程序中,我按照 Francois 的建议将二进制字符串转换回 Blob。之前我尝试过这种解决方法,但是因为我在应用程序上使用了以下代码,所以它没有起作用:

blob = new Blob([blobAsBinString], {type: mimeType});

那段代码对于文本文件或简单字符串可能有效,但对于图像则失败了(可能是由于字符编码问题)。那就让我疯狂了。解决方法是使用Francois一开始提供的方法:

var bytes = new Uint8Array(blobAsBinString.length);
for (var i=0; i<bytes.length; i++) {
   bytes[i] = blobAsBinString.charCodeAt(i);            
}             
blob = new Blob([bytes], {type: mimeString});

这段代码保持了二进制字符串的完整性,因此应用程序可以正确地重新创建blob。

现在我还添加了一些在这里和RobW建议的内容,即将blob分成块并像这样发送它,以防blob太大。整个解决方案如下:

在扩展中:

function sendBlobToApp() {  

  // read the blob in chunks/chunks and send it to the app
  // Note: I crashed the app using 1 KB chunks. 1 MB chunks work just fine. 
  // I decided to use 256 KB as that seems neither too big nor too small
  var CHUNK_SIZE = 256 * 1024;
  var start = 0;
  var stop = CHUNK_SIZE;      

  var remainder = blob.size % CHUNK_SIZE;
  var chunks = Math.floor(blob.size / CHUNK_SIZE);      

  var chunkIndex = 0;

  if (remainder != 0) chunks = chunks + 1;           

  var fr = new FileReader();
  fr.onload = function() {
      var message = {
          blobAsText: fr.result,
          mimeString: mimeString,                 
          chunks: chunks 
      };          
      // APP_ID was obtained elsewhere
      chrome.runtime.sendMessage(APP_ID, message, function(result) {
          if (chrome.runtime.lastError) {
              // Handle error, e.g. app not installed
              // appendLog is defined elsewhere
              appendLog("could not send message to app");
          } 
      });

      // read the next chunk of bytes
      processChunk();
  };
  fr.onerror = function() { appendLog("An error ocurred while reading file"); };
  processChunk();

  function processChunk() {
     chunkIndex++;         

     // exit if there are no more chunks
     if (chunkIndex > chunks) {
        return;
     }

     if (chunkIndex == chunks && remainder != 0) {
        stop = start + remainder;
     }                           

     var blobChunk = blob.slice(start, stop);

     // prepare for next chunk
     start = stop;
     stop = stop + CHUNK_SIZE;

     // convert chunk as binary string
     fr.readAsBinaryString(blobChunk);
  } 
}

关于APP

chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id in blacklistedIds) {
      return;  // don't allow this extension access
    } else if (request.blobAsText) {                  
       //new chunk received  
      _chunkIndex++;                   

      var bytes = new Uint8Array(request.blobAsText.length);                     
      for (var i=0; i<bytes.length; i++) {
         bytes[i] = request.blobAsText.charCodeAt(i);            
      }         
      // store blob
      _blobs[_chunkIndex-1] = new Blob([bytes], {type: request.mimeString});           

      if (_chunkIndex == request.chunks) {                      
         // merge all blob chunks
         for (j=0; j<_blobs.length; j++) {
            var mergedBlob;
            if (j>0) {                  
               // append blob
               mergedBlob = new Blob([mergedBlob, _blobs[j]], {type: request.mimeString});
            }
            else {                  
               mergedBlob = new Blob([_blobs[j]], {type: request.mimeString});
            }
         }                         

         saveBlobToFile(mergedBlob, "myImage.png", request.mimeString);
      }
    }
 }
);

感谢分享这个非常全面的解决方案。我喜欢这种方法,目前看来它运行得很好。 - kzahel
不起作用。块不同步。 需要一种方法以正确的顺序接收所有内容。 - Микола Мірчук

7

我的应用程序没有访问blob,因为它是在扩展上创建的吗?有人可以帮我解决一下吗?

没错!你可能想传递一个dataUrl而不是一个blob。类似下面这样的代码可能会起作用:

/* Chrome Extension */

var blobToDataURL = function(blob, cb) {
  var reader = new FileReader();
  reader.onload = function() {
    var dataUrl = reader.result;
    var base64 = dataUrl.split(',')[1];
    cb(base64);
  };
  reader.readAsDataURL(blob);
  };

blobToDataUrl(blob, function(dataUrl) {
  chrome.runtime.sendMessage(APP_ID, {databUrl: dataUrl}, function() {});
});

/* Chrome App */

function dataURLtoBlob(dataURL) {
    var byteString = atob(dataURL.split(',')[1]),
        mimeString = dataURL.split(',')[0].split(':')[1].split(';')[0];

    var ab = new ArrayBuffer(byteString.length);
    var ia = new Uint8Array(ab);
    for (var i = 0; i < byteString.length; i++) {
        ia[i] = byteString.charCodeAt(i);
    }

    var blob = new Blob([ia], {type: mimeString});
    return blob;
}

chrome.runtime.onMessageExternal.addListener(
    function(request) {  
  var blob =  dataURLtoBlob(request.dataUrl); 
  saveBlobToFile(blob, "screenshot.png");
});

dataURL 是我尝试的第一种方法。但是它不起作用,因为我拥有的 dataURL 看起来像这样:filesystem:chrome-extension://' + chrome.i18n.getMessage('@@extension_id') + '/persistent/' + fileName。应用程序无法访问创建的图像,因为它处于扩展程序的沙箱中。至少当我无法使用 dataURL 在应用程序上显示图像时,我认为问题就在这里。 - CJ Alpha
1
这可能会有帮助。如果有效,请告诉我。http://stackoverflow.com/questions/24582573/implement-cross-extension-message-passing-in-chrome-extension-and-app - Daniel Herr
@LeonAlpha 如果你要将 blob 发送到应用程序,为什么不能发送 dataURL? - user3307259
谢谢Daniel Herr。我明天会看看那个选项,并回报是否有效。@vux777 我在将blob发送到应用程序时遇到了问题。似乎所有内容都传输过去了,但生成的图像无法使用。然后,当我尝试使用dataURL时,它也不起作用。dataURL中缺少预期的';' ':'和':',提供的代码无法将其解码为blob。 - CJ Alpha
@DanielHerr 嘿,丹尼尔,我已经更新了我的问题,以反映我迄今为止所做的事情。我已经编写好了代码,但是我发送的消息在中途丢失了,我无法弄清楚它到底在哪里。你能否看一下我的编辑并想想有什么办法?这真的让我发疯了。我已经花了10个小时了,还是无法解决它。再次感谢。 - CJ Alpha
你可能想在原始问题中询问,我只是想知道那是否有效。 - Daniel Herr

3

我对这个问题非常感兴趣,因为我正在尝试完成类似的事情。

以下是我发现与此相关的问题:

  1. 如何让Chrome扩展程序将多个文件保存到用户指定的目录?
  2. 在Chrome扩展和应用中实现跨扩展消息传递
  3. Chrome.runtime是否支持使用可传输对象发布消息?
  4. 将File对象从内容脚本传递到background.js或传递createObjectURL(并在刷新后保持活动状态)

根据Rob W在第一个链接中的说法:

"Chrome的fileSystem(app)API可以直接写入用户的文件系统(例如~/Documents或%USERPROFILE%\Documents),由用户指定。"

如果您可以写入用户的文件系统,那么应该能够从中读取,对吗?

我还没有机会尝试这个方法,但是您可以使用Chrome扩展下载API将项目保存到下载中,而不是直接将文件blob传递给应用程序。

然后,您可以使用Chrome应用程序文件系统API检索它以访问它。

编辑:

我一直在阅读API可以访问的文件系统是沙盒化的。所以我不知道这种解决方案是否可行。它被沙盒化,而Rob W的“直接写入用户文件系统”的描述听起来像是相反的。

编辑:

Rob W在此处修改了他的答案:在Chrome扩展和应用中实现跨扩展消息传递

它不再使用共享工作器,并将文件数据作为字符串传递给后端,后者可以将字符串转换回blob。

我不确定消息的最大长度是多少,但Rob W还提到了一种将blob分成片段发送的解决方案。

编辑:

我已经发送了43 MB的数据,而不会使我的应用程序崩溃。


这是一个好想法。我之前浏览了一下API,发现扩展只能访问临时存储或永久存储,这两者都对用户和其他应用程序不可访问。也就是说,每个扩展都在沙盒中运行,其他应用程序无法访问它创建的文件(或者至少我记得是这样)。这就是为什么我需要一个应用程序,因为应用程序可以在任何地方写入。我的问题现在是如何将图像的blob从扩展传递到应用程序。我现在唯一的希望是SharedWorker,但正如您在这里看到的那样,它并没有起作用。 - CJ Alpha
假设 Rob W 是正确的,为什么你还需要发送 Blob 呢?你可以使用你的扩展程序将所需文件下载到 Chrome 的默认下载目录中,然后在你的应用程序中使用文件系统 API 访问它们。不需要 Blob,你只需要处理真正需要的文件即可。 - markain
文件系统API仍然需要用户选择一个文件夹来访问。这对于用户来说可能有点奇怪。 - Xan
那听起来不太理想。 - markain
各位,感谢你们的建议!@markain 我必须检查下载API。到目前为止,我读到的所有内容都指向永久或临时存储。但是,如果下载API允许我1-创建目录和2-在没有用户交互的情况下将其保存到这些目录中,那么我就有了解决方案。不过我不确定能否实现。@Xan 用户只需通过应用程序UI选择一次位置。在上面的代码中,“if(_folderEntry!= null){openDirectory(); }”块就完成了这个操作。用户选择一个文件夹来保存图像,这很好地工作。我的问题是这些图像现在毫无用处。 - CJ Alpha
Rob W修改了他关于将Blob发送到应用程序的答案,请参见我上面的编辑。 - markain

2

这是一个非常有趣的问题。从我的角度来看,可以使用以下技术来实现:

  1. 首先,您应该将blob转换为arraybuffer。可以使用FileReader完成此操作,这是异步操作。
  2. 然后,利用编码API的一些魔法,目前在稳定版的Chrome上可用。因此,您可以将arraybuffer转换为字符串。此操作是同步的。
  3. 然后,您可以使用Chrome API 像这样与其他扩展程序/应用程序进行通信。我正在使用这种技术来通过另一个著名的旧版应用程序推广我的新打包应用程序。由于旧版打包应用程序实际上是扩展程序,所以我认为一切都会没问题。

谢谢@Dmitry Sorin。我明天会看一下你的解决方案,并回报告诉你它是否有效。再次感谢。 - CJ Alpha
再次感谢您的回答。尽管我尝试了很多次,但仍然无法使其正常工作。我能够成功发送数组缓冲区,但生成的图像无法使用且无法打开。我已编辑我的问题以展示我现在正在尝试做什么。我正在使用SharedWorker和应用程序中的帧,如此处建议:http://stackoverflow.com/questions/24582573/implement-cross-extension-message-passing-in-chrome-extension-and-app。非常感谢您对此的见解(或者为什么您之前的建议不起作用的原因)。谢谢 - CJ Alpha

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接