前端面试热题:Script 跨域与 JSONP 全解析,一文吃透

44 阅读6分钟

大前端的 “跨域之困”

如今的大前端世界,JavaScript 可谓是 “全家桶” 级别的存在 —— 从 React、Vue 这类 MVVM 前端框架,到 Node.js 搭建的后端服务,再到移动端(iOS/Android)的混合开发、桌面端 exe 应用,甚至嵌入式和 AI 领域,都能看到它的身影。

但在前后端分离成为主流开发模式的今天,一个绕不开的 “拦路虎” 就是跨域问题。比如前端项目跑在localhost:5175,后端接口部署在localhost:8080,当前端美滋滋地想调用后端接口时,控制台可能就会跳出一串刺眼的错误:“Access to fetch at '...' from origin '...' has been blocked by CORS policy”。这就是跨域在 “作祟” 啦~

跨域究竟是什么?

同源策略:浏览器的 “安全红线”

浏览器的同源策略(Same-Origin Policy)是跨域问题的 “始作俑者”,它规定:只有当两个 URL 的协议(protocol)、域名(domain)、端口(port)三者完全一致时,才被视为 “同源”,否则就是跨域。

举几个例子直观感受下(以http://localhost:5175为基准):

  • http://www.baidu.com/api/user:域名不同,跨域

  • https://localhost:5175/api/user:协议不同(http→https),跨域

  • http://localhost:8080/api/test:端口不同(5175→8080),跨域

浏览器为啥要搞这么严格的限制?本质是为了安全!想象一下,如果没有同源策略,恶意网站可以随意读取你在银行网站的 Cookie,后果不堪设想。这就像小区的门禁系统,不是 “自己人”(同源)就得多加防备~

跨域的表现:请求到了,响应却 “丢了”

很多同学会疑惑:跨域请求到底发出去了吗?答案是发出去了,而且服务器也会正常处理并返回响应。但问题出在浏览器 —— 它会拦截这个跨域响应,不让前端 JavaScript 拿到数据,这就是控制台报错的原因。

简单说就是:服务器 “发货” 了,但浏览器这个 “快递员” 因为 “安全规定”,把包裹扣下了。

为什么要学习跨域?

在前后端分离的日常开发中,前端和后端几乎不可能永远跑在同源环境下(端口、域名大概率不同)。如果解决不了跨域,前端就拿不到后端数据,项目根本没法推进。

而且,理解跨域背后的CORS(Cross-Origin Resource Sharing,跨域资源共享)机制,能帮你更深入地掌握浏览器安全原理,在面试中也能轻松应对相关问题 —— 毕竟这可是前端面试的高频考点哦!

使用 script 的跨域解决方案:JSONP

既然浏览器对fetchXMLHttpRequest(axios 基于此封装)这类请求卡得严,那有没有 “漏网之鱼” 能绕过同源策略呢?还真有!<script><img><link>这些标签的src属性,天生就支持跨域请求。其中,<script>标签被开发者们玩出了花样,催生了 JSONP 解决方案。

JSONP 的原理:“借鸡生蛋” 的跨域技巧

JSONP 的全称是 “JSON with Padding”,核心思路是:

  1. 前端提前定义一个处理数据的函数(比如handleData),并挂载到window上。

  2. 动态创建一个<script>标签,把请求 URL 设置为src,同时在 URL 参数里带上这个函数名(比如callback=handleData)。

  3. 后端接收到请求后,解析出callback参数的值,然后返回一段 JavaScript 代码 —— 形式是handleData(实际数据),也就是调用前端定义的函数,并把数据当作参数传进去。

  4. <script>标签加载并执行这段代码,相当于自动调用了handleData函数,前端也就拿到了数据。

整个过程就像前端 “埋好一个函数”,后端 “填进数据并触发执行”,完美利用了<script>的跨域特性~

JSONP 的实现步骤

1. 前端:创建请求并定义回调

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JSONP示例</title>
</head>
<body>
<script>
// 1. 定义处理数据的回调函数
function handleResponse(data) {
    console.log('拿到跨域数据啦:', data);
}

// 2. 动态创建script标签发起请求
const script = document.createElement('script');
// 请求URL中带上callback参数,值为回调函数名
script.src = 'http://localhost:3000/say?wd=Iloveyou&callback=handleResponse';
document.body.appendChild(script);
</script>
</body>
</html>

2. 后端:配合返回带函数包裹的数据

以 Node.js 为例,后端需要解析callback参数,并返回函数名(数据)格式的响应:

// server.js - 原生Node.js实现
const http = require('http');

const server = http.createServer((req, res) => {
    // 匹配GET请求的/say接口
    if (req.url.startsWith('/say')) {
        // 解析URL中的查询参数
        const url = new URL(req.url, `http://${req.headers.host}`);
        const wd = url.searchParams.get('wd'); // 前端传的参数(Iloveyou)
        const callback = url.searchParams.get('callback'); // 回调函数名(handleResponse)

        // 准备要返回的数据
        const data = {
            code: 0,
            msg: '收到你的爱意啦~',
            received: wd
        };

        // 返回JSONP格式:回调函数包裹数据
        res.writeHead(200, { 'Content-Type': 'application/javascript' });
        res.end(`${callback}(${JSON.stringify(data)})`); 
        // 实际返回:handleResponse({"code":0,"msg":"收到你的爱意啦~","received":"Iloveyou"})
    } else {
        res.writeHead(404);
        res.end('Not Found');
    }
});

server.listen(3000, () => {
    console.log('服务器运行在 http://localhost:3000');
});

运行前端页面和后端服务后,控制台就能成功打印跨域数据,是不是很神奇?

手写 JSONP:封装成通用工具

上面的示例比较基础,实际开发中我们可以封装一个 JSONP 工具函数,让它支持 Promise、自动处理参数拼接,更易用:

/**
 * 封装JSONP请求工具
 * @param {Object} options - 配置项
 * @param {string} options.url - 请求地址
 * @param {Object} [options.params={}] - 请求参数
 * @param {string} options.callback - 回调函数名
 * @returns {Promise} - 返回Promise对象,成功时 resolve 数据
 */
function getJSONP({ url, params = {}, callback }) {
    return new Promise((resolve, reject) => {
        // 1. 创建script标签
        const script = document.createElement('script');
        
        // 2. 拼接参数:把callback加入参数列表
        const paramsWithCallback = { ...params, callback };
        const paramStr = Object.entries(paramsWithCallback)
            .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
            .join('&');
        
        // 3. 设置script的src属性
        script.src = `${url}?${paramStr}`;
        
        // 4. 定义全局回调函数,接收数据后resolve
        window[callback] = function(data) {
            resolve(data);
            // 清理工作:移除script标签和全局函数,避免污染
            document.body.removeChild(script);
            delete window[callback];
        };
        
        // 5. 处理错误
        script.onerror = function() {
            reject(new Error('JSONP请求失败'));
            document.body.removeChild(script);
            delete window[callback];
        };
        
        // 6. 把script加到页面中,触发请求
        document.body.appendChild(script);
    });
}

// 使用示例
getJSONP({
    url: 'http://localhost:3000/say',
    params: { wd: 'I love you' },
    callback: 'show'
}).then(data => {
    console.log('JSONP返回数据:', data);
}).catch(err => {
    console.error('请求失败:', err);
});

这样封装后,JSONP 的使用体验就接近axios了,是不是方便多了?

封装的 JSONP:优点与局限

优点:

  • 兼容性好:甚至支持 IE 这类老浏览器,因为<script>标签是所有浏览器都支持的基础特性。
  • 实现简单:不需要复杂的配置,前端后端少量代码就能搞定。

局限:

  • 只能发 GET 请求:因为<script>标签的src属性只能发起 GET 请求,无法满足 POST、PUT 等需求。

  • 依赖后端配合:后端必须按照 JSONP 的格式返回数据,否则前端无法处理。

  • 安全风险

    • 全局回调函数可能被恶意覆盖(比如被其他脚本篡改)。
    • 后端如果返回恶意代码,<script>会直接执行,可能导致 XSS 攻击。
  • 无法捕获 HTTP 错误状态码:即使后端返回 404、500,<script>也只会触发onerror,无法拿到具体的状态码。

所以,JSONP 更适合一些简单的跨域场景(比如调用第三方公开 API),复杂场景下还是推荐使用 CORS 方案哦~

总结

跨域问题是前端开发的 “必修课”,而 JSONP 作为利用<script>标签特性的经典解决方案,虽然有局限,但理解它的原理能帮你更深刻地掌握浏览器的同源策略。

希望这篇文章能让你对 “script 跨域” 和 JSONP 了如指掌,下次面试再遇到相关问题,就能胸有成竹啦~ 如果你有其他跨域解决方案的心得,欢迎在评论区交流哦!