【Chrome扩展程序】利用 background 实现跨域 fetch 访问

一、问题起因

        虽然谷歌在官方文档中声称 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. 后台脚本接收到消息,发送指定请求。然后将处理后的请求结果(消息不支持复杂对象传输)以回调形式发送回内容脚本。

你可能感兴趣的:(问题解决,实用工具,chrome,json,http,https)