使用 Manifest V3 在 chrome 扩展中将 FormData/File 对象从内容脚本传递到后台脚本
Passing FormData/File Object from content script to background script in chrome extension with Manifest V3
我正在构建一个 chrome 扩展,我从用户那里获取一个文件作为输入并将其传递给我的 background.js(如果是清单 v3,则为服务工作者)以将其保存到我的后端。由于内容脚本阻止了跨源请求,因此我必须将其传递给我的 background.js
并使用 FETCH API 来保存文件。当我将 FormData
或 File
对象传递给 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
,也会发生同样的情况。
我参考了旧 Whosebug 问题建议的多个解决方案,使用 URL.createObjectURL(myfile);
并将 URL 传递给我的 background.js
并获取相同的文件。而 FETCH API 不支持 blob URL 来获取,并且 XMLHttpRequest
在 service worker 中也不支持 here 所推荐的。有人可以帮我解决这个问题吗?这种行为让我很受阻。
目前只有Firefox可以直接传输这些类型。 Chrome 或许可以在 future.
中完成
解决方法 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。
使用 iframe 指向通过 web_accessible_resources 公开的扩展 中的 html 文件。 iframe 将能够执行扩展程序可以执行的所有操作,例如发出 CORS 请求。 File/Blob 和 other cloneable types can be transferred directly from the content script via postMessage。这些消息会暴露给页面中的任何脚本 运行,因此我们必须使用 chrome.runtime 消息添加请求的授权,这是安全的(直到有人找到通过 side 破坏内容脚本的方法-通道攻击,如 Spectre)。
警告!该站点(或其他扩展程序)可以随时删除 iframe。
manifest.json:
{
"web_accessible_resources": [{
"resources": ["sender.html"],
"matches": ["<all_urls>"],
"use_dynamic_url": true
}],
"host_permissions": [
"https://your.backend.api.host/"
],
请注意 use_dynamic_url
尚未实施。
content.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);
}
});
});
}
sender.html:
<script src="sender.js"></script>
sender.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] : []);
}
}
};
我有一个更好的解决方案:您实际上可以将 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!
}
}
});
我找到了另一种将文件从内容页面(或弹出页面)传递给服务工作者的方法。
但是,可能,它并不适合所有情况,
您可以 intercept a fetch request 从服务工作者的内容或弹出页面发送。然后你可以通过service-worker发送这个请求,也可以修改
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;
});
我正在构建一个 chrome 扩展,我从用户那里获取一个文件作为输入并将其传递给我的 background.js(如果是清单 v3,则为服务工作者)以将其保存到我的后端。由于内容脚本阻止了跨源请求,因此我必须将其传递给我的 background.js
并使用 FETCH API 来保存文件。当我将 FormData
或 File
对象传递给 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
,也会发生同样的情况。
我参考了旧 Whosebug 问题建议的多个解决方案,使用 URL.createObjectURL(myfile);
并将 URL 传递给我的 background.js
并获取相同的文件。而 FETCH API 不支持 blob URL 来获取,并且 XMLHttpRequest
在 service worker 中也不支持 here 所推荐的。有人可以帮我解决这个问题吗?这种行为让我很受阻。
目前只有Firefox可以直接传输这些类型。 Chrome 或许可以在 future.
中完成解决方法 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。
使用 iframe 指向通过 web_accessible_resources 公开的扩展 中的 html 文件。 iframe 将能够执行扩展程序可以执行的所有操作,例如发出 CORS 请求。 File/Blob 和 other cloneable types can be transferred directly from the content script via postMessage。这些消息会暴露给页面中的任何脚本 运行,因此我们必须使用 chrome.runtime 消息添加请求的授权,这是安全的(直到有人找到通过 side 破坏内容脚本的方法-通道攻击,如 Spectre)。
警告!该站点(或其他扩展程序)可以随时删除 iframe。
manifest.json:
{ "web_accessible_resources": [{ "resources": ["sender.html"], "matches": ["<all_urls>"], "use_dynamic_url": true }], "host_permissions": [ "https://your.backend.api.host/" ],
请注意
use_dynamic_url
尚未实施。content.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); } }); }); }
sender.html:
<script src="sender.js"></script>
sender.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] : []); } } };
我有一个更好的解决方案:您实际上可以将 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!
}
}
});
我找到了另一种将文件从内容页面(或弹出页面)传递给服务工作者的方法。 但是,可能,它并不适合所有情况,
您可以 intercept a fetch request 从服务工作者的内容或弹出页面发送。然后你可以通过service-worker发送这个请求,也可以修改
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;
});