2017.02.09 跨域

来源:互联网 发布:淘宝传感器 编辑:程序博客网 时间:2024/05/03 20:14

1、什么是跨域及产生原因

1.1 什么是跨域

跨域是指从一个域名的网页去请求另一个域名的资源。比如从www.baidu.com 页面去请求 www.google.com 的资源。

跨域的严格一点的定义是:协议(http&https)、端口(:80&:81)、域名(baidu&google)、二级域名(news&sports)不相同,都为跨域。

受到跨域的限制,浏览器不能用ajax获取不同源的数据,也不能在同一个页面但是处于不同域的框架之间的进行数据传递。

1.2 为什么浏览器要限制跨域访问

原因就是安全问题:如果一个网页可以随意地访问另外一个网站的资源,那么就有可能在客户完全不知情的情况下出现安全问题。比如下面的操作就有安全问题:

用户访问 www.mybank.com,登陆并进行网银操作,这时cookie啥的都生成并存放在浏览器。用户突然想起件事,并迷迷糊糊地访问了一个邪恶的网站 www.xiee.com ,这时该网站就可以在它的页面中,拿到银行的cookie,比如用户名,登陆token等,然后发起对www.mybank.com的操作。如果这时浏览器不予限制,并且银行也没有做响应的安全处理的话,那么用户的信息有可能就这么泄露了。

1.3 为什么要跨域

有时公司内部有多个不同的子域,比如一个是 location.company.com, 而应用是放在 app.company.com, 这时想从 app.company.com 去访问 location.company.com 的资源就属于跨域。

2、跨域的方法

2.1 document.domain+iframe方法

2.1.1 适用范围

适用于主域相同,子域不同的情况,并且需要对跨域的两个网页的js脚本都进行修改。例如在http://www.a.com/a.html和http://script.a.com/b.html之间的跨域就可以使用这种方法。

主域名是不带www的域名,比如 a.com, 主域名带前缀的通常是二级域名或者多级域名,比如 www.a.com。

2.1.2 如何实现

首先在a.html中创建一个iframe,将b.html通过iframe添加到a.html页面下,然后在 www.a.com/a.html 和 http://script.a.com/b.html 中都增加js脚本,脚本中加入代码:

document.domain="a.com"

这样就将两个原本跨域的网页的域统一了,此时就和平时同一个域镶嵌iframe一样,通过iframe的contentDocument中就可以实现数据交互。

页面默认的domain是window.loaction.hostname,document.domain只能设置成自身或更高一级的父域,比如 a.b.example.com 中的某个页面的document.domain可以设置为 a.b.example.com、 b.example.com、 example.com, 但是不能是设置 为c.a.b.example.com 或 baidu.com。
document.domain只能设置

因此为保证两个主域相同,子域不同的页面跨域,二者的document.domain只能设置为二者的共同的主域,即a.com

iframe元素就是文档中的文档,拥有自己的事件,拥有自己的窗口对象(contentWindow)。浏览器会在打开一个 HTML 文档时创建一个对应的 window 对象。如果一个文档定义了一个或多个框架(即包含一个或多个frame或iframe 标签),浏览器就会为原始文档创建一个window对象,再为每个框架创建额外的window对象。这些额外的对象是原始窗口的子窗口,可能被原始窗口中发生的事件所影响。contentWindow属性是指指定的frame或者iframe所在的window对象,contentWindow兼容各个浏览器,可取得子窗口的 window 对象[2]。

2.1.3 限制

1 要实现跨域,a、b两个页面都必须由你自己来开发,对不受控制的对方进行跨域时无能为力;

2 依赖iframe,iframe的缺点也是一对,现在的大部分网站避免使用iframe。

历史上,iframe 常被用于复用部分界面,但是多数情况下并不合适。现在,应该使用 iframe 的例子如:
1. 沙箱隔离。
2. 引用第三方内容。
3. 独立的带有交互的内容,比如幻灯片。
4. 需要保持独立焦点和历史管理的子窗口,如复杂的Web应用。

缺点也很明显:大量使用,打开一个网页加载过多iframe体验很不友好而且影响网页加载速度,对爬虫不够友好。[3]

2.1.4 代码示例

www.a.com上的a.html增加脚本:

<script type="text/javascript">    document.domain="a.com";    var ifr=document.createElement("iframe");    ifr.src="ttp://script.a.com/b.html";    ifr.style.display="no`ne";    document.body.appendChild(irf);    irf.onload=function(){        var doc=irf.contentDocument|| ifr.contentWindow.document;        //在这里操作b.html的contentWindow    }</script>

script.a.com上的b.html增加脚本:

<script type="text/javascript">    document.domain="a.com";</script>

2.2 window.name方法

2.2.1 适用范围

适用于控制跨域双方代码的情况,也就是说需要对方的支持。

2.2.2 如何实现

将json格式的数据写入到window.name中,通过共享一个窗口的两个页面的共同的window.name实现跨域的数据传递

window.name属性在一个窗口(window)生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写权限。

window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而重置。window.name只能是字符串,最大允许2M左右数据,可以传递json格式的数据。

a.html和b.html可以通过window.location实现同一个窗口下的跳转,这样a就可以取得b页面中window.name中写入的数据。

如果想要不同源的a页面在不由b页面跳转而来的情况下获得数据,就不能直接使用window.location实现,需要利用一个隐藏的iframe来充当跳板,由iframe获取b页面的数据(需要将iframe的src设置为b页面的地址),然后需要将iframe的src再次设置为与a页面同源,a页面再去获取iframe的数据。

2.2.3 限制

1 同样的,a、b两个页面都必须由你自己来开发,因为需要将数据写入到window.name中,因此对不受控制的对方进行跨域时无能为力;

2 window.name只能是字符串,最大允许2M左右数据,还是有一定限制的。

2.2.4 代码示例

第一种情况就是发生跳转的情况。
如页面b.html的代码如下:

<script>window.name="jay111";setTimeout(function(){    window.location="../test10/test10.html"},3000)</script>

页面a.htm的代码如下:

<script>    function doSomething(jsonDate){        alert(window.name)    }    doSomething()</script>

当b页面跳转a页面后,a会取得在b页面中写入window.name的值jay111。

第二种情况就是不发生跳转,使用iframe作为跳板的情况。
b页面代码如下:

<script>    window.name="jay111";</script>

a页面的代码如下:

<body><iframe id="test" src="b.html"></iframe><script>    var iframe=document.getElementById("test");    iframe.onload=function() {        iframe.src = "c.html";//这里c.html为任意的页面,只需要与a.html同源既可,设置成about:blank也可以        iframe.onload = function () {            var data = iframe.contentWindow.name;//获取a.html中的window.name数据            alert(data);        }    }</script></body>

注意,更改iframe的src需要在iframe载入后执行,所以添加到onload事件里。

2.3 postMessage方法

2.3.1 适用范围

页面和其打开的新窗口的数据传递、多窗口之间消息传递、页面与嵌套的iframe消息传递都可以使用postMessage方法跨域或同源传递数据。[4]

postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。[5]

2.3.2 如何实现

window. postMessage(message,targetOrigin)方法是HTML5引入的新特性,用来向其他的window对象发送消息,无论其他的window对象是否跨域。语法如下:

window.postMessage(message,targetOrigin);

message是要发送的string数据,HTML5规范中提到该参数可以是JavaScript的任意基本类型或可复制的对象,然而并不是所有浏览器都做到了这点儿,部分浏览器只能处理字符串参数,所以我们在传递参数的时候需要使用JSON.stringify()方法对对象参数序列化。

targetOrigin用来限制otherWindow对象所在的域,字符串参数,指明目标窗口的源,协议+主机+端口号[+URL],这个参数是为了安全考虑,postMessage()方法只会将message传递给指定窗口,当然如果愿意也可以建参数设置为”*”,这样可以传递给任意窗口,如果要指定和当前窗口同源的话设置为”/”。

接受消息的window对象otherWindow是通过监听自身的message事件获取传过来的消息,消息内容存储在message事件对象的data属性中。

2.3.3 限制

对不受控制的对方进行跨域时无能为力;

2.3.4 代码示例

a.com上的a.html的代码:

<iframe id="ifr" src="b.com/b.html"></iframe><!--对接受信息页面的引用--><script type="text/javascript">    window.onload=function(){        var ifr=document.getElementById("ifr").contentWindow; //获取框架的window对象        var targetOrigin="http://b.com"//对目标的限定        ifr.postMessage("i love you",targetOrigin);    }</script>

b.com上的b.html的代码:

<script type="text/javascript">    window.addEventListener("message",function(event){//注册message事件用来接收消息        if(event.origin==="http://a.com"){//通过origin判断消息来源地址            alert(event.data);//通过data属性获取"i love you"            alert(event.source);//对a.com/a.html中window对象的引用            alert(event.origin);//发送消息窗口的源(协议+主机+端口号)        }    },false)</script>

上面的集中方法都主要侧重于前端通讯。

2.4 动态创建script

2.4.1 适用范围

两个域开发者都需要控制,被访问的域需要返回一段JS代码。

2.4.2 如何实现

浏览器可以在页面中引用其他域的JS文件,并且执行引入的JS的文件中的function,所以可以通过创建script的方法实现跨域通信。

原理就是在本域内页面a的

2.4.3 限制

两个域开发者都需要控制。

2.4.4 代码示例

详细代码以前曾经总结过,详见《2016.12.05 JSON和JSONP》(http://note.youdao.com/noteshare?id=89adc08bbc61d9735f324dd68da83425&sub=7C193C83D0454923B7D41ABE9961C69D)

2.5 JSONP方法

2.5.1 适用范围

使用返回与上一个方法相同,开发者都需要控制两个域。

2.5.2 如何实现

利用

2.5.3 限制

1 服务器端需要部署相应的代码,响应客户端的请求。

2 只支持GET,不能很好的支持POST。

2.5.4 代码示例

本地服务器上同时运行了2个project,端口分别是8080和8081,在8081的客户端上面请求8080端口的数据,端口不同也是跨域。[6]

//getJOSN方法$.getJSON("http://localhost:8080/msg/front/jcj/t1.jsp?callback=?",function(result) {});//ajax方法$.ajax({type: "get",    url: "http://localhost:8080/msg/front/jcj/t1.jsp",//也可以直接将callback写在url中//url: "http://localhost:8080/msg/front/jcj/t1.jsp?callback=m1",    dataType: "jsonp",    jsonpCallback: "m1",    success:function(data){        //处理返回的数据    }});function m1(data) {    alert(data);}

服务器端需要部署相应的代码,响应客户端的请求。服务器端部署的代码可以是JAVA代码:

String str = "{[\"name1\": \"json\",\"name2\": \"json\",\"name3\": \"json\"]}";ObjectMapper mapper = new ObjectMapper();String json = mapper.writeValueAsString(str);json = "m1(" + json + ")";response.getWriter().print(json);

也可以是PHP代码:

<?php$callback=$_GET['callback'];//得到回调函数名$data=array('a','b','c');//要返回的数据echo $callback.'('.json_encode($data).')';//echo输出?>

说明:第一种方式的callback=?回调函数标志着Ajax请求是以jsonp的方式发送请求,客户端请求会自动在问号处增加一个方法名,方法名是一jquery开头的一串数字,而第二种方式的是参数dataType=”jsonp”来说明是以jsonp的方式发送请求,所以可以直接在callback后面直接指定方法名callback=m1,而不需要jsonpCallback: “m1”这一样代码。

AngularJS的http也提供了对JSONP的支持,可以直接调用jsonp进行跨域访问;

知道了一个免费的API的实例(http://www.opencai.net/apifree/),
可以使用下面的代码获取响应的数据:

$.getJSON("http://f.apiplus.cn/ssq-12.json?callback=?",            function(result){                alert(result.data[1].opencode)            }        );

结果:

这里写图片描述

从这个里也可以开出,服务器端要布置响应的代码,并且把API放出来给使用者,最好配有相应的接口说明文档

2.6 XHR2(XMLHTTPREQUEST LEVEL2)方法

2.6.1 适用范围

在服务端进行处理,客户端支持H5

2.6.2 如何实现

HTML5中提供的XMLHTTPREQUEST Level2实现了跨域访问,只需要在服务端填上响应头[7]:

header(“Access-Control-Allow-Origin:*”);/*星号表示接受来自所有域的访问*/header(“Access-Control-Allow-Methods:GET,POST”);

2.6.3 限制

1 服务器端需要部署相应的代码,响应客户端的请求。

2 IE10一下不支持这种方式。

2.7 nginx反向代理跨域

上面的所有的方法都有一个共同的限制,那就是必须在对方的网页(或者服务器)上进行相应的部署,不论是JS代码还是服务器端代码。

但是有一种最常遇到的情形,就是想要获取的数据的网站是不受自己控制的,开发者只能控制一个域。

这时候只能依靠nginx进行反向代理跨域。

3 nginx

3.1 关于nginx

nginx(音同engine x)是一个强大的轻量级的高性能网页服务器、反向代理服务器和电子邮件代理服务器。作为负载均衡服务器,nginx可以在内部直接支持Rails和PHP程序对外进行服务,也可以支持作为HTTP代理服务器对外进行服务。

3.2 nginx的正向代理

正向代理的工作原理就像一个跳板,简单的说,我是一个用户,我访问不了某网站,但是我能访问一个代理服务器这个代理服务器呢,他能访问那个我不能访问的网站。

于是我先连上代理服务器,告诉他我需要那个无法访问网站的内容,代理服务器去取回来,然后返回给我。从网站的角度,只在代理服务器来取内容的时候有一次记录。有时候并不知道是用户的请求,也隐藏了用户的资料,这取决于代理告不告诉网站。

结论就是正向代理是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端必须要进行一些特别的设置才能使用正向代理。

3.3 nginx的反向代理

3.3.1 什么是反向代理

所谓反向代理(Reverse Proxy)方式是指以代理服务器来接受Internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给Internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器[8]。
这里写图片描述

从上图可以看出,反向代理服务器位于网站机房,代理网站web服务器接受Http请求,对请求进行转发。

例用户访问 http://ooxx.me/readme, 但ooxx.me上并不存在readme页面,他是偷偷从另外一台服务器上取回来,然后作为自己的内容吐给用户,但用户并不知情。这里所提到的ooxx.me这个域名对应的服务器就设置了反向代理功能。

结论就是反向代理正好相反,对于客户端而言它就像是原始服务器,并且客户端不需要进行任何特别的设置。客户端向反向代理发送普通请求,接着反向代理将判断向何处(原始服务器)转交请求,并将获得的内容返回给客户端,就像这些内容原本就是它自己的一样。

3.3.2 反向代理的作用

1 保护网站安全:任何来自Internet的请求都必须先经过代理服务器;
这里写图片描述

2 通过配置缓存功能加速web请求:可以缓存真实Web服务器上的某些静态资源,减轻真实Web服务器的负载压力;
这里写图片描述

3 实现负载均衡:充当负载均衡服务器均衡的奋发请求,平衡集群中各个服务器的负载压力;

这里写图片描述

3.4 利用nginx的反向代理实现跨域

利用nginx反向代理实现跨域,不需要目标服务器配合,但需要搭建中转nginx服务器,用于转发请求。

3.4.1 实现原理

这里写图片描述
黑色的过程是nginx作为反向代理服务器,隐藏了真实地址的过程,紫色的过程就是用nginx实现跨域的过程。

3.4.2 具体操作

首先找到nginx.conf中的下面这部分内容:

 server {        listen       80;        server_name  localhost;        location / {            root   ../Project;            index  index.html index.htm;        }

其中server代表启动的一个服务,location是一个定位规则,是nginx用来跨域的入口。
location /的意思是所有以/开头的地址,实际上是所有请求,后面的地址可以是绝对地址,也可以是相对地址(相对nginx.exe的地址);root的意思是去请求相对nginx.exe上一层中的Project文件夹里的文件,index是去指定首页。

在location /{}中添加一下代码:

add_header’ Access-Control-Allow-Origin’’*’;add_header’ Access-Control-Allow-Credentials’’true’;add_header’ Access-Control-Allow-Methods’’GET,POST,OPTIONS’;rewrite regex replacement [flag]proxy_pass http://ip:port/;

其中:

第一条指令:授权从other.subdomain.co的请求;是W3C标准里用来检查该跨域请求是否可以被通过;

第二条指令:当该标志为true时,响应于该请求可以被暴露;

第三条指令:制定请求的方式,可以使GET/POST等;

第四条指令:对域名(根路径)后面除去传递的参数外的字符串的url进行重写及重定向;

URL 是 URI 的子集。任何东西,只要能够唯一地标识出来,都可以说这个标识是 URI 。如果这个标识是一个可获取到上述对象的路径,那么同时它也可以是一个 URL ;但如果这个标识不提供获取到对象的路径,那么它就必然不是 URL[9]。

第五条指令:实际要访问的请求地址;

前三条时用来指定自己的服务器是否可以被跨域访问的指令,是可以没有的。

第四条利用nginx提供的变量,结合正则表示和标志位来实现比较复杂的重定向,简单的情况也是可以没有的。

3.4.2.1 location+proxy_pass

适用于将location直接转发至proxy_pass地址的情况,这种情况一般比较简单,例子

<script type="text/javascript">    var button=document.getElementById("button");    $(button).click(function(){        $.get("students",function(data){            alert(data.data[0].id)        },"json");        $.getJSON("students",function(data){            alert(data.data[0].id)        });        $.ajax({            url:"students",            type:"GET",            dataType:"json",            success:function(data){                alert(data.data[0].id)}        })    })</script>

上面的三种方法都可以实现跨域,要注意的是使用get方法获取json数据时一定要声明第四个参数dataType为JSON,否则总是无法获得正确的对象。

跨域的部分,url地址是一个相对地址”students”,转化为绝对地址就是:

http://localhost/Project2/task5/students/

而在实际本机的地址中,并没有这样的资源,所以需要利用nginx进行转发处理。

location ^~/Project2/task5/students/{    proxy_pass http://115.29.203.53:10013/students/;}

配置完成后重启nginx服务

nginx.exe -s reload

nginx的启动和停止需要在命令行中定位到nginx.exe所在的文件夹,然后:

启动:start nginx

停止:nginx -s stop或nginx -s quit

注:stop是快速停止nginx,可能并不保存相关信息;quit是完整有序的停止nginx,并保存相关信息。
重新载入Nginx:nginx -s reload

当配置信息修改,需要重新载入这些配置时使用此命令。

重新打开日志文件:nginx -s reopen

查看Nginx版本:nginx -v[11]

这个是比较蠢的做法,直接将全部的url替换为最终的真实url,这样就将地址转发出去了,实现了跨域。

3.4.2.2 location+proxy_pass+rewrite

适用于直接将location转发至proxy_pass不能满足需要,需要对虚拟根路径及后面的路径使用rewrite进行改写的情况,例子:

我们主机的地址是 www.a.com/html/index.html, 想请求 www.b.com/api/msg?method=1&para=2, ajax请求如下:

$.ajax({    type:"get",    url:"www.b.com/api/msg?method=1&para=2",    success:function(res){        alert("success")    }})

这样必然因为跨域问题发生错误,无法获得b网站的数据。将ajax请求更改为:

$.ajax({    type:"get",    url:"api/msg?method=1&para=2",    //真实地址是www.c.com/proxy/html/api/msg?method=1&para=2,www.c.com是nginx主机地址    success:function(res){        alert("success")    }})

这是url就是相对于a网站的本地的相对地址了,但是在本地并不存在相应的数据,所以需要将这个地址通过nginx转发出去。

在刚才的路径中匹配到这个请求,在location下面再添加一个location:

location ^~/proxy/html/{//#匹配任何以/proxy/html/开头的地址,匹配符合以后不继续往下搜索    rewrite ^/proxy/html/(.*)$ /$1 break;    proxy_pass http://www.b.com/}

location ^~/proxy/html/用于拦截请求,是一个匹配规则,匹配任何以/proxy/html/开头的地址,这里匹配到的就是:

www.c.com/proxy/html/api/msg?method=1&para=2

rewrite ^/proxy/html/(.*)/1 break用来重写拦截的请求,并且只对域名后面除去传递参数外的字符起作用,即重写上面匹配到的地址的这一部分进行重写:/proxy/html/api/msg

rewrite后面是一个正则表达式,表示匹配以/proxy/html/开头的任何字符至结尾,并且将proxy/html/后面的任意字符存到第一个捕获组$1之中,break表示匹配一个后停止。

重写的结果是:/api/msg

proxy_pass http://www.b.com/ 用来把请求代理到其他主机,即 www.c.com 代理到 www.b.com, 请求路径最终变为

http://www.b.com/api/msg? method=1&para=2。
配置完成后重启nginx服务

这样就可以更改3.4.2.1中的做法,js文件不变,nginx的location的配置如下:

    location ^~/Project2/task5/students/{        rewrite ^/Project2/task5/(.*)$ /$1 break;        proxy_pass http://115.29.203.53:10013/;        }

同样可以实现跨域

3.4.3 关于location[10]

location是主机访问的地址,在nginx服务器做一个代理,转发到location中配置的地址。

location =/{    #精确匹配,主机名后面不能带任何字符串    [configuration]}location /{    #因为所有的地址都以/开头,所以这条规则将匹配到所有请求,通用匹配    #但是正则和最长字符串会优先匹配    [configuration]}location ~/document/abc{    #匹配任何以/document/开头的地址,匹配符合以后,还要继续往下搜索    #只有后面的正则表达式没有匹配到时,这一条才会被采用    [configuration]}location ^~/document/{    #匹配任何以/document/开头的地址,匹配符合以后不继续往下搜索    [configuration]}

3.4.4 关于rewrite

rewrite只能对域名(根地址)后边的除去传递的参数外的字符串起作用,例如:

http://www.baidu.com/a/bb/ccc.php?id=1&uu=str

rewrite只会对a/bb/ccc.php重写。

语法:

rewrite regex replacement [flag]

执行顺序是首先执行location匹配,然后执行location中的rewirte指令。

3.4.5 关于proxy_pass

proxy_pass地址后面加上了/则相当于转发到绝对根路径,nginx不会对loaction中匹配的路径部分进行转发,例如[12]:

location ^~/proxy/html/{//#匹配任何以/proxy/html/开头的地址,匹配符合以后不继续往下搜索    proxy_pass http://www.b.com/}

如果请求的url为:http://localhost/proxy/html/test.json, 则转发后的地址是:http://www.b.com/test.json。

如果proxy_pass地址后面不加/,则nginx会对loaction中匹配的路径部分进行转发,例如:

location ^~/proxy/html/{//#匹配任何以/proxy/html/开头的地址,匹配符合以后不继续往下搜索    proxy_pass http://www.b.com}

如果请求的url为:http://localhost/proxy/html/test.json, 则转发后的地址是:http://www.b.com/proxy/html/test.json。

3.4.6 又一个例子[13]

$.ajax({    type:"get",    url:"sohu/api/msg?method=1&para=2",    //真实地址是www.c.com/proxy/html/api/msg?method=1&para=2,www.c.com是nginx主机地址    success:function(res){        alert("success")    }})
location ^~/sohu{//#匹配任何以/proxy/html/开头的地址,匹配符合以后不继续往下搜索    rewrite ^.+sohu/?(.*)$ /$1 break;    proxy_pass http://www.sohu.com/}

location定位到:sohu/api/msg?method=1&para=2

rewrite的结果:api/msg

proxy_pass后的结果:http://www.sohu.com/api/msg?method=1&para=2

3.4.7 最后一个例子

这里写图片描述

终于把这个总结写完了,从2016年写到了2017年,拖了很长时间,不过写完了自己也明白了,任务可以做了。
写这些东西,也是怕以后自己如果长时间不用后再用的时候会忘记,好可悲,不过自己写的东西符合自己的理解思路,捡起来会跨一点。
还是好可悲。

5、参考

  1. http://blog.csdn.net/notechsolution/article/details/50394391
  2. http://blog.csdn.net/dongzhiquan/article/details/585120
  3. https://www.zhihu.com/question/20653055
  4. http://www.qdfuns.com/notes/13989/cab4fcdd13851f75bb8af349d5596baf.html
  5. http://www.cnblogs.com/dolphinX/p/3464056.html
  6. http://www.cnblogs.com/dreamroute/p/3613563.html
  7. http://www.jb51.net/article/77470.htm
  8. http://blog.csdn.net/ywl570717586/article/details/51556912
  9. https://www.zhihu.com/question/19557151
  10. https://segmentfault.com/a/1190000002797606
  11. http://blog.csdn.net/ppby2002/article/details/38681345
  12. http://stevenlee87.blog.51cto.com/996623/1188295
  13. http://blog.csdn.net/shendl/article/details/48443299
0 0
原创粉丝点击