JSONP 方式实现跨域请求数据

前言

在了解 浏览器的同源政策(SOP) 后知道了浏览器的同源政策对保护用户信息安全的重要性。但是有些时候我们确实需要两个网站间的数据共享。例如腾讯天气的天气数据就是从其他服务器上获取到的,并不是腾讯天气服务器上的数据。那它们是怎么做到跨域资源(数据)共享的呢?在这里学习一下 jsonp 方式实现跨域资源共享。

JSONP 实现原理

并不是页面中所有的请求都受同源政策的限制。例如外链外部 js 文件文件时是不受同源政策的限制的,这也是为什么在使用 jQuery 时,我们并不是一定要将库文件下载到项目后才能引入使用。我们也可以直接引入 jQuery 的线上文件,并不影响正常使用。它的原理是虽然此时进行了跨域(线上资源在 jQuery 的官网上),但是 script 标签的 src 属性外链的资源请求不受浏览器同源政策的影响。这也是我们使用 JSONP 实现跨域资源共享的原理。

JSONP 的基础原理如下示例代码所示(注意 A 服务器监听 3000 端口,B 服务器监听 3001 端口,下面的所有示例都在同源政策的基础上改进,所以不会再做具体说明。):


<body>
    <h2>A服务器的页面h2>
    
    <script>
        function fn1(data) {
      
            console.log(data);
        }
    script>
    
    <script src="http://localhost:3001/jsonp1">script>
body>

如下图是响应的结果,想必从图中也你可以猜出了服务器端的代码。

JSONP 方式实现跨域请求数据_第1张图片
JSONP 方式实现跨域请求数据_第2张图片

// B服务器响应路由的代码
B.get('/jsonp1', (req, res) => {
     
    res.send('fn1({name: "tkop", age: 18})');
});

结合上面的代码示例总结 JSONP 的原理:

script 标签可以向服务器发送请求,最常见的是请求 js 脚本文件,客户端接收到服务器响应的 js 脚本文件后会立即执行。但是这个请求响应过程不受同源政策的限制,即使请求头信息中的 Sec-Fetch-Site 字段值不是 same-origin(同源),即使响应头中没有 Access-Control-Allow-Origin 字段,浏览器依旧会将服务响应的数据视为 js 脚本并在客户端运行。

JSOPN 正是利用了这一点,在客户端接收到响应前提前在全局作用域下声明一个用于接收处理响应数据的函数。再利用 script 标签发送请求,此时服务器端返回的不再是一个脚本文件,而是返回该函数的调用(函数调用的字符串)。并且将客户端需要的数据作为实参传递给函数。在客户端接收到响应后会将返回结果作为 js 脚本执行(调用返回的函数),这样就达到了获取并处理数据的目的。为了更加清楚原理我们再继续看看下面的示例代码。


<body>
    <h2>A服务器的页面h2>
    <script src="http://localhost:3001/jsonp1">script>
    <script>
        console.log(data);
    script>
body>

这次我不想直接在全局定义数据处理函数了,我想直接得到一个全局变量作为响应得到的数据(这个前后端交流成本非常高,而且还有后端开发者给你定义变量。。。。这是不可能的,纯粹是为了演示原理而已)。

// B服务器响应路由的代码
B.get('/jsonp1', (req, res) => {
     
    res.send('var data = "这就是响应数据"');
})

这就是上面的客户端代码即使没有声明 data 也不会报错的原因,返回的数据就是 data 声明

JSONP 模仿 ajax 请求

上面已经介绍了 jsonp 的基本原理,可以看出这种方案实际上已经不属于 ajax 请求的范围了。但是它可以模仿 ajax 请求,使用其原理 jsonp 是怎么模仿 ajax 请求的呢?

  1. 上面示例中的请求都是在页面加载过程就发送出去的,不像 ajax 根据客户端需要去发送。为实现这一点,我们可以在只有需要发送请求时在页面动态追加 script 标签。请求会在 script 标签被追加到页面时发送

  2. 发送请求接收到响应后在页面追加的 script 标签需要删除。如果不进行删除,每发送一次请求页面就会增加一个 script 标签。但是怎么判断 script 标签已经追加到页面且请求已经发送了呢?可以监听 script 标签的 onload 事件实现

  3. 另一个是函数名的问题。服务器返回的某函数的调用时,函数名必须与客户端声明的函数名称保持一致。这增加了前后端的沟通成本,不利于高效开发。为什么不在前端将函数名作为参数传递到后端呢?后端在获取到该参数后作为函数名并返回函数调用。

客户端代码如下:

<body>
    <h2>A服务器的页面h2>
    <button id="btn">向B发送ajax请求button>
    
    <script>
        function fn2(data) {
      
            console.log(data);
        }
    script>
    <script type="text/javascript">
        var btn = document.getElementById('btn');
        btn.onclick = function() {
      
            // 创建script标签
            var script = document.createElement('script');
            // 设置script标签的src属性
            script.src = 'http://localhost:3001/jsonp2?callback=fn2';
            // 将script标签追加到页面中
            document.body.appendChild(script);
            // 在script标签追加完成请求发送后将其删除
            script.onload = function() {
      
                document.body.removeChild(script);
            }
        }
    script>
body>

服务器端代码如下:

B.get('/jsonp2', (req, res) => {
     
    var callback = req.query.callback;
    res.send(callback + '({ name: "tkop", age: 18 })')
})

上面的代码个人亲自敲完后运行没有出错,可以发送请求和获得响应数据,并达到预期结果。

JSONP 函数封装

JSONP 也可以像 ajax 那样将发送请求处理响应的过程封装成一个函数。在需要发送请求时,只需要调用并传入指定参数即可。参考 ajax 函数的封装过程,按照思考的步骤进行 JSONP 函数的封装。

  1. 请求地址、请求参数和响应处理函数可以作为函数的参数传入。但是由于 JSONP 方式只能发送 get 方式的请求,所以也就没有请求方式判断和请求参数格式不同等问题的处理(相比较于以前 ajax 函数的封装简单了不少)

  2. 响应处理函数作为参数传入 jsonp 请求函数,理想做法是以匿名函数的方式传入。如果是在全局作用域声明一个命名函数在将其传入 jsonp 请求函数,这是 jsonp 的封装性非常低

  3. 这样又会出现两个问题。问题 1:函数必须要挂载在全局作用域内,在后端返回函数调用时才可以找到相应的函数。问题 2:在发送请求时必须将函数名作为参数传递给后端

  4. 针对以上两个问题,可以直接在函数内部封装给函数命名和将其挂载在全局作用域下的代码

  5. 但是还是需要解决一个问题,就是不能在内部为所有请求的函数起同一个函数名,这样会造成函数覆盖的问题。例如给按钮 1 和按钮 2 都绑定点击事件,点击后均是发送 jsonp 请求,如果它们的响应处理函数命名相同,那无疑都会执行后面定义的那个函数

  6. 所以需要给每个函数的命名是随机而不同的

服务器端 jsonp 函数封装的过程和发送 jsonp 请求的代码

<body>
    <h2>A服务器的页面h2>
    <button id="btn">向B发送jsonp请求button>
    <script type="text/javascript">
        var btn = document.getElementById('btn');

        function jsonp(options) {
      
            // 处理其他参数的拼接
            var params = '';
            for (var k in options.data) {
      
                params += '&' + k + '=' + options.data[k];
            }
            // 响应处理函数
            var fnName = 'myJsonp' + Math.random().toString().replace('.', '');
            window[fnName] = options.success;
            // 请求完整地址
            var url = options.url + '?callback=' + fnName + params;
            // 创建script标签
            var script = document.createElement('script');
            // 设置script标签的src属性
            script.src = url;
            // 将script标签追加到页面中
            document.body.appendChild(script);
            // 在script标签追加完成请求发送后将其删除
            script.onload = function() {
      
                document.body.removeChild(script);
                window[fnName] = null;
            }
        }
        btn.onclick = function() {
      
            jsonp({
      
                // 地址
                url: 'http://localhost:3001/jsonp3',
                // 其他请求参数
                data: {
      
                    name: 'tkop',
                    age: 18
                },
                // 响应处理函数
                success: function(data) {
      
                    console.log(data);
                }
            })
        }
    script>
body>

服务器端的响应代码

B.get('/jsonp3', (req, res) => {
     
    var callback = req.query.callback;
    var data = JSON.stringify({
      name: 'TKOP', password: 123456 });
    var result = callback + '(' + data + ')';
    res.send(result)
});

对服务器端的响应代码可以做一下优化,响应对象下有一个 jsonp() 方法封装了回调函数名的获取和与数据字符串拼接为函数调用的过程。所以使用这个方法的后端实现示例代码如下:

B.get('/jsonp3', (req, res) => {
     
    res.jsonp({
      name: 'TKOP', password: 123456 });
});
// 响应结果
//typeof myJsonp07452599508118731 === 'function' && myJsonp07452599508118731({"name":"TKOP","password":123456});

写完验证没有问题后,我在想:这样岂不是每发送一个请求就会在全局挂载一个全局函数吗?需不需要在接收到响应并调用处理函数后将它删了?我觉得有必要,所以处理需要删除 script 标签外我另外添加了清除全局函数的代码。

JSONP 方式实现跨域请求数据_第3张图片
封装结果如上图所示,发送几次请求后没有问题,如果试一下给其他按钮绑定点击事件发送 jsonp 请求的也没有发现问题。

其他方式

实现跨域的方式还有 CORS 和 WebSocket ,但是在此记录的是利用服务器作为中间人向其他服务器请求数据的方式。因为同源政策只是浏览器为保护用户信息安全而在客户端执行的策略,而服务器是没有同源政策的。浏览器不跨域向服务器请求数据时,服务器转而向其他服务器请求数据,在得到其他服务器响应的数据后转手响应给浏览器。这也隐式实现了跨域的数据请求。原理如下:

aaa: A 哥我希望得到这些数据。
A : 这些数据在 B 上,我去跟他要吧,你自己去要的话,他不会给你。
A : B 哥我要这些数据。
B : 给你。
A :aaa 这是你要的数据。

具体的实现服务器端需要使用到第三方库(request)向另一个服务器发送请求和接收处理响应。

// aaa 页面脚本向自己的服务器发送ajax请求
ajax({
     
	type: 'get',
	url: 'http://localhost:3000/server',
	success: function (data) {
     
		console.log(data);
	}
})

// A服务器的响应路由
app.get('/server', (req, res) => {
     
	// 向B服务器发送请求
	request('http://localhost:3001/cross', (err, response, body) => {
     
		res.send(body);
	})
});

// B服务器响应路由
app.get('/cross', (req, res) => {
     
    res.send('ok')
});

有关 CORS 和 WebSocket 有空再总结。。。。

你可能感兴趣的:(javaScript,javascript)