一、问题起因
虽然谷歌在官方文档中声称 content_script 允许无限制的跨域访问,但实际上经过版本更新该特性已经被禁用,因此我们只能通过间接的方式实现跨域访问。
content_script 的跨域问题https://blog.csdn.net/NXY666/article/details/124309832
二、源代码
./manifest.json(插件清单v3)
{
"manifest_version": 3,
"name": "Expample",
"version": "0.0.1",
"description": "示例",
"permissions": [],
"host_permissions": [
"https://www.example.com/"
],
"icons": {
"256": "icon.png"
},
"author": "NXY666",
"background": {
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["https://www.example.com/*"],
"js": [
"./script.js"
],
"all_frames": true,
"run_at": "document_idle"
}
]
}
./js/http.js(基于fetch的http工具)
export const http = {
request: function (options) {
// Post请求选项并入默认选项
let requestOptions = {
method: null,
url: null,
param: {},
data: {},
headers: {}
};
this.mergeOptions(requestOptions, options);
// 格式化参数
requestOptions.param = this.formatParams(requestOptions.param);
let _url = requestOptions.url + (requestOptions.param ? ('?' + requestOptions.param) : '');
let _data = requestOptions.data;
if (typeof _data == "string") {
requestOptions.headers["Content-type"] = "text/plain;charset=utf-8";
_data = requestOptions.data;
} else if (requestOptions.data instanceof FormData) {
_data = requestOptions.data;
} else if (typeof requestOptions.data == "object") {
let formData = new FormData();
if (Object.keys(requestOptions.data).some(key => {
formData.append(key, requestOptions.data[key]);
return requestOptions.data.hasOwnProperty(key) && requestOptions.data[key] instanceof File;
})) {
_data = formData;
} else {
requestOptions.headers["Content-type"] = "application/json;charset=utf-8";
_data = JSON.stringify(requestOptions.data);
}
}
// 监听状态
let fetchOptions = {
method: requestOptions.method, // *GET, POST, PUT, DELETE, etc.
// mode: 'no-cors', // no-cors, *cors, same-origin
cache: 'default', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'include', // include, *same-origin, omit
headers: requestOptions.headers,
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'no-referrer-when-downgrade' // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
};
if (requestOptions.method.toUpperCase() !== "GET" && requestOptions.method.toUpperCase() !== "HEAD") {
fetchOptions.body = _data;
}
return fetch(_url, fetchOptions);
},
get: function (options) {
options.method = "GET";
return this.request(options);
},
post: function (options) {
options.method = "POST";
return this.request(options);
},
formatParams: function (data) {
const arr = [];
for (let name in data) {
arr.push(encodeURIComponent(name) + "=" + encodeURIComponent(data[name]));
}
return arr.join("&");
},
// 原则:如果有默认值,则使用默认值,否则使用传入的值。
mergeOptions: function (targetOption, newOption) {
if (!newOption) {
return targetOption;
}
Object.keys(targetOption).forEach(function (key) {
if (newOption[key] === undefined) {
return;
}
targetOption[key] = newOption[key];
});
return targetOption;
}
};
./background.js(后台脚本)
import {http} from './js/http.js';
console.log('background.js');
function packMsgRep(state, data, message) {
return {
state,
uuid: message.uuid,
data,
timestamp: Date.now()
};
}
async function parseHttpResponse(response) {
if (response == null) {
return {
status: -2,
statusText: null,
body: null
};
} else if (response instanceof Error) {
return {
status: -1,
statusText: `${response.name}: ${response.message}`,
body: response.stack
};
} else {
return {
status: response.status,
statusText: response.statusText,
body: await response.text()
};
}
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
new Promise(async (resolve, reject) => {
if (typeof message != 'object' || !message.type) {
console.error("消息格式不符合规范:", message);
reject(`消息 ${JSON.stringify(message)} 格式不符合规范。`);
return;
}
switch (message.type) {
case 'FetchRequest': {
http.request(message.data).then(response => {
resolve(parseHttpResponse(response));
}).catch(error => {
reject(parseHttpResponse(error));
});
break;
}
case 'FetchGet': {
http.get(message.data).then(response => {
resolve(parseHttpResponse(response));
}).catch(error => {
reject(parseHttpResponse(error));
});
break;
}
case 'FetchPost': {
http.post(message.data).then(response => {
resolve(parseHttpResponse(response));
}).catch(error => {
reject(parseHttpResponse(error));
});
break;
}
default: {
console.error("消息类型非法:", message);
reject(`消息 ${message} 类型非法。`);
break;
}
}
}).then((response) => {
sendResponse(packMsgRep(true, response, message));
console.log(`消息 ${JSON.stringify(message)} 处理完成。`);
}).catch(e => {
sendResponse(packMsgRep(false, e, message));
console.error(`消息 ${JSON.stringify(message)} 处理失败:`, e);
});
return true;
});
./script.js(内容脚本)
function packMsgReq(type, data) {
return {
uuid: function () {
return 'generate-uuid-4you-seem-professional'.replace(
/[genratuidyosmpfl]/g, function (c) {
const r = Math.random() * 16 | 0,
v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}(),
type: type,
data: data,
timestamp: Date.now()
};
}
const http = {
request: function (options) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(packMsgReq('FetchRequest', options),
(response) => {
if (response.state) {
resolve(response.data);
} else {
reject(response.data);
}
});
});
},
get: function (options) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(packMsgReq('FetchGet', options),
(response) => {
if (response.state) {
resolve(response.data);
} else {
reject(response.data);
}
});
});
},
post: function (options) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(packMsgReq('FetchPost', options),
(response) => {
if (response.state) {
resolve(response.data);
} else {
reject(response.data);
}
});
});
}
};
// 发送get请求
http.get({url: "https://www.example.com/"});
// 发送Delete请求
http.request({
method: "DELETE",
url: "https://www.example.com/",
headers: {
cookie: "test=true;"
}
})
三、操作流程
1. 在 manifest.json 的 host_permissions 中添加须解除限制的网站。
2. 由内容脚本调用,通过 chrome.runtime.sendMessage() 向后台脚本发送请求消息。
3. 后台脚本接收到消息,发送指定请求。然后将处理后的请求结果(消息不支持复杂对象传输)以回调形式发送回内容脚本。