在开发浏览器插件时,我们经常会遇到跨域资源共享(CORS)的问题。特别是当插件需要与外部API进行通信,而后端服务器又因安全考虑无法配置CORS时,这个问题就变得尤为棘手。本文将介绍一个巧妙的解决方案,通过利用Chrome扩展的特性来绕过跨域限制。
问题背景
我们开发了一个浏览器插件,插件中嵌入了一个系统项目,在项目中需要调用后端API,但是在外部网页中使用插件时,由于跨域限制,API调用会失败。传统的解决方案是在后端配置CORS,但出于安全考虑,这种方法并不适用于我们的情况。
解决方案概述
我们的解决方案主要包含三个部分:
- 前端请求拦截(
request.js
) - 后台脚本(
background.js
) - 内容脚本(
content-script.js
)
这个方案的核心思想是利用Chrome扩展的跨域能力,通过消息传递机制来代理API请求。
具体实现
1. 前端请求拦截(request.js)
现在的项目基本都会基于axios做一个请求的封装,在封装文件request.js
中,我们通过环境变量判断,如果是插件环境,则走新的请求逻辑。关键点在于handlePluginRequest
方法:
javascript
async handlePluginRequest(options) {
//每一个请求创建一个唯一的id。
const uniqueId = Date.now() + Math.random().toString(36).substr(2, 9)
const config = await this.requestInterceptor({ headers: {}, ...options })
const headers = {
"Content-Type": "application/json;charset=utf-8",
"token":''
}
return new Promise((resolve, reject) => {
const responseEventName = `ReceiveResponse-${uniqueId}`;
const event = new CustomEvent('MakeRequest', {
detail: {
...options,
headers,
url: this.service.defaults.baseURL + options.url,
data: options.data || options.params || {},
uniqueId: uniqueId,
}
});
document.dispatchEvent(event);
const handleResponse = (e) => {
const { data, error } = e.detail;
if (error) {
//axios拦截器中的错误处理函数,这里不做实现,对实现没有影响
this.errorInterceptor({ response: error });
reject(error);
} else {
const response = { data: data };
//axios的响应拦截器,对实现没有影响
this.responseInterceptor(response);
resolve(data);
}
document.removeEventListener(responseEventName, handleResponse);
};
document.addEventListener(responseEventName, handleResponse);
});
}
这个方法创建了一个自定义事件,将API请求的详细信息作为事件数据发送出去。
2.请求的唯一性:避免响应混淆
在我们的解决方案中,每次API请求都会创建一个唯一的事件名。这个设计决策看似简单,实则解决了一个潜在的严重问题:多个并发请求导致的响应混淆。
为什么需要唯一事件名?
当插件同时发起多个API请求时,如果所有请求都使用相同的事件名,就会出现响应内容串扰的情况。具体来说:
- 响应匹配问题:如果多个请求使用相同的事件名,当响应返回时,前端无法确定哪个响应对应哪个请求。
- 数据完整性:错误的响应匹配可能导致数据被错误地处理,影响应用的正确性。
- 并发处理:在高并发情况下,使用相同的事件名会导致后来的响应覆盖先前的响应,造成数据丢失。
实现唯一事件名
在我们的代码中,唯一事件名的实现如下:
javascript
const uniqueId = getUniqueId()
const responseEventName = `ReceiveResponse-${uniqueId}`;
getUniqueId()
函数,这可以通过多种方式实现,例如使用时间戳加随机数,或者使用UUID库。
工作流程
- 每次发起请求时,生成一个唯一的
uniqueId
。 - 使用这个
uniqueId
创建特定的响应事件名(ReceiveResponse-${uniqueId}
)。 - 请求通过内容脚本发送到后台脚本时,携带这个
uniqueId
。 - 后台脚本处理完请求后,响应带着相同的
uniqueId
返回。 - 内容脚本使用对应的响应事件名分发响应。
3. 后台脚本(background.js)
background.js
作为中间人,负责接收来自内容脚本的消息,并执行实际的API请求:
javascript
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "fetchData") {
// 构建fetch请求选项
const fetchOptions = {
method: request.method.toUpperCase(),
headers: request.headers,
};
// 构建URL和处理请求数据
let url = request.url;
if (request.method.toUpperCase() === 'GET' && request.data) {
const queryParams = new URLSearchParams(request.data).toString();
url += (url.includes('?') ? '&' : '?') + queryParams;
} else if (request.data) {
fetchOptions.body = JSON.stringify(request.data);
}
// 执行fetch请求
fetch(url, fetchOptions)
.then(response => response.json())
.then(data => sendResponse({ data: data }))
.catch(error => sendResponse({ error: error.toString() }));
return true; // 保持消息通道开放
}
});
4. 内容脚本(content-script.js)
content-script.js
是连接前端和后台脚本的桥梁:
javascript
function handleMakeRequest(e) {
const { uniqueId } = e.detail;
const responseEventName = `ReceiveResponse-${uniqueId}`;
chrome.runtime.sendMessage({ action: "fetchData", ...e.detail }, function (response) {
const event = new CustomEvent(responseEventName, { detail: response });
document.dispatchEvent(event);
});
}
document.addEventListener('MakeRequest', handleMakeRequest);
这个脚本监听来自前端的MakeRequest
事件,将请求转发给后台脚本,然后将响应通过自定义事件传回前端。
工作流程
- 前端发起API请求时,
request.js
拦截请求并创建MakeRequest
事件。 content-script.js
监听到这个事件,将请求信息发送给background.js
。background.js
接收请求信息,使用fetch
执行实际的API调用。background.js
将API响应发送回content-script.js
。content-script.js
创建ReceiveResponse
事件,将响应数据传回前端。- 前端接收响应数据,完成API调用过程。
总结
这个解决方案利用了Chrome扩展的特性,成功绕过了跨域限制。通过在不同脚本之间传递消息,我们实现了一个无需后端配置CORS的API代理机制。这种方法不仅解决了跨域问题,还保持了良好的代码组织结构,使得整个过程清晰可控。
这个方案特别适用于那些无法修改后端CORS配置,又需要在插件中进行API调用的场景。利用浏览器扩展的能力来解决web开发中的常见问题。