一、什么是jsonp?
先别管什么叫jsonp,我们先来看一个小问题,看完这个问题你就知道jsonp是要解决什么问题的,自然也就明白什么是jsonp了。
问题:
之前做的例子如果在同一个域名下运行时是非常正常的,但如果这个数据接口是在A域名下,而使用了AJAX请求的静态页运行在B域名下,我们就会发现点击按钮发出请求时会报错。截个图来看看。
我们看到后台的一般处理程序运行的域名是 : http://localhost:64113
现在使用VS重新创建1个web项目,然后在这个使用AJAX方式发出请求的静态页拷贝到这个新项目中。当然,这时候异步请求的请求url我们需要写成 http://localhost:64113/getTimeHandler.ashx。运行之,我们再来看截图:
猛一看都是localhost,不过仔细看端口号,就可以知道了,现在是运行在不同域名下的2个完全独立的应用程序。如果这个时候点击按钮获取时间,我们在浏览器的控制台中就会看到报错。截图如下:
更极端的情况再试一试。我们这次仍然启动域名为http://localhost:64113 的服务,然后在文件系统下直接打开静态页HtmlPage.html,这时在浏览器的地址栏中看到的url是这样的:
此时点击“获取时间”按钮,使用AJAX发出请求,在控制台中会报出同样的错误。
这个错误不是代码写错了,如果我们的静态页发出的AJAX请求,所请求的目标跟当前页面是同一个域名,代码没有任何问题。
这个错误是因为跨域引起的,简单的说,就是如果使用AJAX发出http请求,是不允许跨域的。说白了就是不允许你自己的网站,使用AJAX直接请求其他网站的服务。
这是硬性规定。
之所以有这种规定,原因是为了网络的安全。如果想了解到底安全问题会出在哪里,原理是如何的,请参考:浅谈CSRF攻击方式
是时候给jsonp下个定义了。首先,jsonp不是json,json只是一种数据描述格式;jsonp只是用来解决AJAX不能跨域问题的一种手段,只不过在这种手段经常会用到json格式的数据,我想着可能就是jsonp名字的来源。
如果你还不明白什么是jsonp,不知道为啥要跨域,那么咱们看这个例子。比如你要开发一个非常NB的网站,这个网站上要有天气预报的信息,你总不能自己建个气象站来观测气象得到天气数据吧,通常的做法是访问其他平台为我们提供的数据服务,那么很显然我们自己的网站是不可能跟那些专业提供数据服务的网站的域名是相同的,如果你使用AJAX的方式直接发出请求,这时候,对于提供数据服务的平台来说,你的http请求就是跨域的,这是非法的。
但是为了完成项目需求我必须要访问,而且这些提供数据服务的平台就是让用户访问的,如何解决之呢?这时候就要是用jsonp的手段,简而言之,jsonp就是当使用异步的方式发出跨域http请求时,为了避开跨域访问限制的一种技术手段。这种技术手段并不复杂,只不过是拐了个弯而已。接下来我们就通过实例来一窥jsonp的究竟。
二、jsonp原理
(1)我不打算直接拿使用jsonp的代码来给大家看,请先从一个最基本的JavaScript的小例子开始,然后一步一步进化到jsonp的形式,我觉得这样更有助于大家理解。
首先我们创建一个html文档,myPage.html,然后在该html文档中写入以下js代码。
<script> function callback(data) { console.log(data); } callback("hello"); </script>
此代码非常简单,声明了一个函数叫callback,然后调用它。在浏览器中打开这个页面,控制台会输出hello字符串。
(2)改造这个案例,callback函数仍然在当前网页中声明,但是把callback("hello")这条调用的语句剪切走,我们另外创建一个单独的js文件,比如叫做test.js,这个文件中就一条js语句:callback("hello")。然后我们在myPage.html页面中引用这个外部的js脚本文件。代码如下:
<script> function callback(data) { console.log(data); } </script> <script src="test.js"></script>
这个页面打开以后,在浏览器控制台中一样可以看到hello字符串。这说明callback仍然被调用了。只不过调用callback的语句是写在外部文件test.js中的。
注意看红色的代码,这个script标签引用了一个外部文件,当浏览器解释到这行html代码时,就会去请求这个外部的js脚本文件,并且把这个文件加载进来,运行该js脚本内的所有JavaScript代码。而这个外部的test.js文件又恰好只有一条语句:callback(“hello”);就是调用上一个script标签中声明的函数。于是该函数就被执行1次,于是我们就看到了控制台中输出hello字符串。
额,题是没跑的。这里要说明的是,在JavaScript中,可以在A文件中声明一个函数,而在另外一个文件B中去调用它。
而接下来要说的jsonp的基本实现原理就是基于下面2个事实。
- 不允许直接在AJAX发出跨域请求,但是,允许<script>标签的src属性引用一个跨域的资源。
- 在JavaScript中,可以在A文件中声明一个函数,而在另外一个文件B中去调用它
(3)再次用获取服务器时间的例子。如果之前的服务端代码如果忘了,可以再去看看 AJAX(一)初识AJAX 。我们这次要是用jsonp的方式,让这个服务能够被跨域请求。
先看服务端代码,我们改造为:
public class getTimeHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType = "text/plain"; string nowTime = DateTime.Now.ToLongTimeString(); context.Response.Write("callback('" + nowTime + "');"); } public bool IsReusable { get { return false; } } }
这个PR方法特殊之处就是不再直接返回时间字符串,而是返回了一个拼接的字符串,该字符串形如:“callback( '20:18:30‘)’”。
再来看浏览器端的js代码:
<head> <title></title> <script> function callback(data) { console.log(data); } </script> <script src="http://localhost:64113/getTimeHandler.ashx"></script> </head>
这里通过一个<script>标签的src属性,去请求一个跨域的资源。而且得到的这个资源就是我们上边的服务端代码返回的字符串:callback( '20:18:30‘),正好是对本页面上声明的函数callback的调用。所以,这个页面打开后,我们就能在浏览器的控制台中看到输出的服务器时间。
这就是jsonp的实现原理:
1.不要直接使用AJAX去请求跨域的资源,而是把这个跨域的请求交给一个<script>标签来做。
2.客户端要声明一个函数,用来处理服务端返回的数据。
3.服务端不能仅仅返回客户端要展示的数据,而且要把数据跟js端声明的方法,两者按照js语法,品写成1个js函数调用的格式的大字符串。
(4)然而还有一个事实我们必须想到,一般情况下都是先有服务端的数据接口,然后才有第三方开发者去跨域访问它,获取服务方提供的数据支持。这样就有一个很尴尬的问题,服务方如何知道第三方调用者的JavaScript代码中定义了一个叫callback的函数,而不是一个叫abc的函数?翻过来也是如此啊,如果服务端定义了返回的字符串中,客户端应该有个方法叫callback,但开发客户端的第三方如何能知道呢?退一步说,如果第三方开发者知道这个函数需要定义为callback,而此时他的代码中已经占用了这个名字,岂不是很尴尬了?有没有更灵活的方式呢?
肯定是有的。看我们来改造代码。还是先看服务端代码:
public void ProcessRequest(HttpContext context) { context.Response.ContentType = "text/plain"; if (context.Request.HttpMethod.ToUpper() == "GET") { string fun = context.Request.QueryString["_jsonp"]; string nowTime = DateTime.Now.ToLongTimeString(); context.Response.Write(fun + "('" + nowTime + "');"); } }
在这段代码中,我们不再把返回的字符串中表示js代码的函数名写死了,而且通过get方式要求客户端请求时传递过来一个key为“_jsonp”的参数,这个参数被用来作为返回的字符串中的,要调用的js函数的名字。这么做的目的在于,在第三方调用我这个后台服务时,具体人家会定义一个叫什么名字的函数,让第三方来灵活声明,你只需要把你声明的函数名以get请求的方式告诉服务端就ok了,服务端会自动使用你传递过来的参数当函数名,来和生成的数据做拼接,拼接成响应报文。
再来看看浏览器端的JavaScript代码。
<head> <title></title> <script> function callback(data) { console.log(data); } </script> <script src="http://localhost:64113/getTimeHandler.ashx?_jsonp=callback"></script> </head>
注意红色标注的url地址,显然这是一种get请求传参的风格。传递的参数key叫_jsonp,value是callback,这个callback正好是定义的那个处理服务端数据的函数名。
这样就灵活多了。
原理就是这么简单。接下来的一期,我会用一个案例来实现一个完整,使用原生的代码的,jsonp跨域请求。