我比较喜欢查看大型网站的前端代码,了解一下最新的技术,看看大公司都是怎么实现某个功能的。比如打开百度主页搜索数字 1 的时候,发现百度的前后端数据交互形式并不是我们大家习以为常的 JSON。而是类似这种的数据格式:

JSONP 的工作原理

这让我感到很奇怪,为什么不直接使用 JSON 去处理?

  • 一、什么是跨域

直到最近在工作中遇到了 AJAX 跨域的问题,接触到了 JSONP。才明白了这种方式的『奥秘』。所以抽空总结一下关于 JSONP 的知识,加深自己的理解。首先要知道什么是 跨域:跨域是指 HTTP 请求中只要域名、协议、端口号,之中有一个是不相同的都被认为是跨域。以 http://www.wuchengkai.com 为例,以下域名被认为都是跨域了:

http://statics.wuchengkai.com     // 一级域名相同,子域名不相同  
https://www.wuchengkai.com        // 域名、端口号相同,协议不同  
http://www.wuchengkai.com:8090    // 域名、协议相同,端口号不同  
http://80.90.100.110:8090         // 端口、协议相同,IP 与域名不同  

如果直接去跨域请求会怎么样?服务器会直接 abort 掉这个请求,所以你在 chrome 控制台能够看到浏览器给出的错误提示。

JSONP 的工作原理

  • 二、JSONP 的原理

有时我们在工作中真的会需要遇到跨域请求的情况,比如这次我遇到的就是一个 app.域名.com 去请求 www.域名.com 的案例。同样的很多网站也存在跨域的需求,以百度为例:百度的主域名是 www.baidu.com, 请求的对象却是 https://www.baidu.com/home/msg/data/personalcontent。显然存在了跨域的行为,那么我们如何实现这样的跨域行为?

聪明的人马上就想到了 html 当中的 link、script 等标签是不存在有跨域的限制的,能够很方便的使用别人服务器的资源,而不用受到兼容性的干扰。而 AJAX 的数据总是要和 JavaScript 打交道,所以我们可以利用 script 标签的特性,做到跨域请求。方法其实很简单:我们动态生成一个 script 标签,标签的路径上加上我们想要查询的参数,以域名 127.0.0.1:8080 为例:

<script src="127.0.0.1:2344/?name=tom&age=18">  

JSONP 的实现需要后端的支持,后端接受到了这条请求,拿到参数 {name: 'tom', age: 18} 执行相关业务代码,并且将数据返回。而 script 标签直接引用了返回的数据,所以我们就能按照常规的流程去执行数据渲染等操作。但是如果返回的是单纯的 JSON 数据,那么我们又能怎么知道当前的这条数据是我本次跨域请求得到的那? jQuery 给了我们一个很好很简单的方法,点击此链接查看 百度 JSONP 返回值

JSONP 的工作原理

jQuery 在实现 JSONP 的时候实例化了一个临时函数,函数以 jQuery 加上时间戳或 uuid 命名,内部调用的实际上是类似 ajax 的 success 回调函数。

  • 三、如何去实现 JSONP

了解了 JSONP 的实现原理,那么现在开始实战吧。

前端代码:

'use strict';

const JSONP = function( params ){

    // default setting
    // 默认的参数
    const defaults = {complete: function(){}, callback: function(){}, timeout: 4000, timeoutFunc: function(){}};

    // fill setting
    // 填充参数
    const options = Object.assign({}, defaults, params);

    // current time
    // 时间戳
    const timestamp = Date.now();

    // temp script DOM
    // 临时的 script 标签
    const script = document.createElement('script');

    // format param
    // 格式化参数
    JSONP.prototype.param = function(){

        // get jsonp data
        // 获取 JSONP 参数
        const data = options['data'];

        // judge data type
        // 判断数据类型
        const type = Object.prototype.toString.call( data ).toLowerCase().slice(8,-1) === 'object';

        // return
        // 如果不是对象, 直接 return
        if( type !== true ) return;

        // add callback func
        // 增加回调函数
        let emptyString = '', extendData = Object.assign({}, data, {callback: `JSONP_${timestamp}`});

        // convert params
        // 转换参数形式
        for(let i in extendData) { emptyString += `${encodeURIComponent( i )}=${encodeURIComponent( extendData[i] )}&`; }

        // return 
        return '?' + emptyString.slice(0, -1);
    }

    // init
    // 初始化
    JSONP.prototype.init = function(){

        // get head tag
        // 获取 head 标签
        const head = document.getElementsByTagName('head')[0];

        // add jsonp url
        // 增加 jsonp URL 与参数
        script.src = options.url + JSONP.prototype.param();

        // register complete event
        // 注册完成事件
        script.onload = options.complete.call( script );

        // append script
        // 添加 script 标签
        head.appendChild( script );

        // register timeout event
        // 注册超时事件
        setTimeout('options.timeoutFunc()', 4000);
    }

    // callback event
    // 回调事件
    JSONP.prototype.callback = function(){

        // return jsonp data
        // 返回 JSONP 数据
        options.callback( arguments[0], script );
    }

    // register global jsonp event
    // 全局注册 JSONP 回调事件
    window[`JSONP_${timestamp}`] = JSONP.prototype.callback;

    // emit right now
    // 立即触发
    JSONP.prototype.init();
}

new JSONP({url: 'http://127.0.0.1:2344/', data: {name:'tom',age:18}, callback: function( data, script ){  
    let element = document.createElement('div');
    element.innerHTML = `我的名字是 ${data.name} ,我今年 ${data.age} 岁了, 我是 ${data.callback} 的回调函数`;
    document.body.insertBefore(element, document.body.firstChild);
}});

后端语言以 nodejs 为例,为了避免繁琐,我直接在使用了 nodejs 官网的示例代码(注:下面这段代码是不严谨的,仅用于演示使用)。

const http = require('http');

const hostname = '127.0.0.1';  
const port = 2344;

const param = function( query ){

    // handle without no query
    // 处理没有查询数据
    if( query.indexOf('?') <= 0 ) return 'need query string';

    // empty object
    // 空对象, 用于格式化数据
    const emptyObject = {};

    // format params
    // 格式化请求参数
    const params = query.replace('/?', '').split("&");

    // push in object
    // 填充对象
    Array.prototype.forEach.call(params, i => emptyObject[ i.split('=')[0] ] = i.split('=')[1]);

    // output response
    // 输出返回值
    return `${emptyObject.json}(${JSON.stringify( Object.assign( {}, emptyObject, {mask: '假装是 SQL 查出的数据'}) )})`;
}

http.createServer((req, res) => {  
  res.writeHead(200, { 'Content-Type': 'text/plain;charset=UTF-8' });
  res.end( param( req.url ), 'UTF-8' );
}).listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

代码比较简陋,但是也能实现 ajax 的 success 、timeout 等事件。实际的使用效果是这种样子的:

JSONP 的工作原理

  • 四、HTML5 的新方式

这种 JSONP 的方法虽然能够实现跨域请求,但是缺点也是很明显,只能支持 GET 请求,不支持其他的 http 请求方式。HTML5 找到了痛点所在提供了一种新的 API postMessage

完。