前端通讯进阶

在几年前,天空一声巨响,ajax 闪亮登场. 前端宝宝们如获至宝~ 已经表单提交神马的, 真的太 心累了. 有了ajax以后, 网页的性能可大幅提高,告别刷新,告别如水的流量. 不过,长江后浪推前浪,一代更比一代强. 因为ajax被同域限制着, 致使, 多服务器配置,云服务资源的存储 没办法充分利用. 因此,业界想到另一种方法--JSONP. JSONP实际上和ajax没有半点关系,惟一相同的就是都是异步执行,并且JSONP完美解决了CD(cross domain)问题.
科技就是第一辈子产力, web发展so fast. 之前追求就是静态网页,显示信息而已。 如今,正朝着web2.0,webapp前进。 之前的单向交流 已经不能知足 需求了。 怎么办呢?
改呗~
因此,紧接着SSE,websocket 诞生了. 至今为止, 前端通讯方式算是告一段落。 这里咱们将围绕上述的几种通讯方式进行,简单的介绍.
如下是几个技术的顺序.javascript

  • ajaxhtml

  • JSOP前端

  • SSEjava

  • websocketnode

ok~ 进入主题吧~jquery

AJAX

相信这个应该不用过多的讲解了吧.
差很少就4步:git

  • 建立xhr对象程序员

  • 监听请求github

  • 设置回调web

  • 设置参数

  • 发送xhr

  • 得到数据执行回调

这里,我就直接上代码了.

var sendAjax = (function() {
    var getXHR = (function() {
        var xhr;
        if(window.XHRHttpRequest){
            xhr = new XMLHttpRequest();
        }else{
            xhr = new ActiveObject("Microsoft.XMLHTTP");
        }
        return xhr;
    })();
    return function(url,opts){ //url为目标地址
        var xhr = getXHR(),
        data;
        xhr.onreadystatechange = function(){
            if(xhr.readyState===4||xhr.status===200){
                data = JSON.parse(xhr.responseText);  //将data解析为json对象
                opts.callback(data);
            }
        }
        xhr.setRequestHeader('Content-Type','application/json');
        xhr.open(opts.method,url);  //写入参数
        xhr.send(JSON.stringify(opts.data));  //将参数json字符化
    }
})();
//调用执行
sendAjax('www.example.com',{
    callback:function(data){
        //...
    },
    data:{
        name:'JIMMY',
        age:18
    }
})

这样差很少就完成了一个ajax的简单模型。固然,咱们也可使用jquery提供的$.ajax函数, 只是他里面作了更多的兼容性和功能性.

JSONP

JSONP 就是 JSON with Padding... 我真的不知道这个名字的含义到时有什么卵用...
一开始在使用JSONP时, 就是使用jquery的$.ajax函数就能够了. 但,这形成了一个很很差的impression. 老是让咱们觉得,JSONP 和 ajax有什么关联似的. 而,事实上,他们两个是彻底不一样的机制. xhr原理你们已经很清楚了,就是完彻底全的异步操做. 但JSONP的原理是什么呢?

JSONP原理

JSONP 实际上是和< script> 标签 有很大的关系. JSONP最大的优点就是实现异步跨域的做用, 他究竟是怎么作到的呢?
其实, JSONP就是利用script 的 src属性,实现跨域的功能.

talk is cheap, show the code

<script>
function processJSON (json) {
  // Do something with the JSON response
};
</script>

<script src='http://www.girls.hustonline.net?
callback=processJSON&name=jimmy&age=18'></script>

上面的写法有点不符合前端风味. 说明一下, 其实processJSON,其实就至关于一个回调函数而已. 在script--src里面的内容咱们来瞧一瞧. 使用jsoncallback 来指定回调函数名字, 而且传入一些参数

  • name = jimmy

  • age = 18

这就是前端发送JSONP的所有. 那应该怎么执行呢?或者说,返回的内容是什么呢?
很简单, 根据jsoncallback里面指定的函数名--processJSON. 在返回的js里面使用processJSON(data); 来执行.
服务器端返回的js内容.

processJSON({
    message:"I've already received"
});

而后,浏览器收到后,直接执行便可. 这里,咱们来模拟一下服务器端盖怎样执行一个JSONP的函数.

const util = require('util'),
    http = require('http'),
    url = require('url');
let data = JSON.stringify({
    message:"I've already received"
});
http.createServer(function(req, res) {
    req = url.parse(req.url, true);
    if (!req.query.callback) res.end();
    console.log(`name is  ${req.query.name} and his age is ${req.query.age}`);
    res.writeHead(200, { 'Content-Type': 'application/javascript' })
    res.end(req.query.callback + "('" + data + "')")
}).listen(80)

ok~ 上面基本上就能够完成一个简单的JSONP函数执行。 固然,express 4.x 里面也有相关的JSONP 操做。 有兴趣的同窗能够看一看.
then, 咱们能够模拟一下实在的JSONP请求.上面是直接将script 写死在html内部, 这样形成的结果可能会阻塞页面的加载. 因此,咱们须要以另一种方式进行,使用异步添加script方法.

var sendJSONP = function(url,callbackName){
    var script = docuemnt.createELement('script');
    script.src = `${url}&callback=${callbackName}`;
    document.head.appendChild(script);
}
var sayName = function(name){
    console.log(`your name is ${name}`);
}
sendJSONP('http://girls.hustonline.net?name=jimmy','sayName');

上面就是一个精简版的JSONP了。 另外,也推荐使用jquery的getJSON和$.ajax进行请求.
先看一下getJSON

$.getJSON("http://girls.hustonline.net?callback=?", function(result){
  console.log(result);
});

这里,咱们须要关注一下url里面中callback=?里的?的内涵. jquery使用自动生成数的方式, 省去了咱们给回调命名的困扰。 其实,最后?会被一串字符代替,好比: json23153123. 这个就表明你的回到函数名.
不过,仍是推荐使用$.ajax,由于你一不当心就有可能忘掉最后的?.
使用$.ajax发送jsonp

$.ajax({
    url: 'http://girls.hustonline.net?name=jimmy',
    dataType: 'jsonp',
    success: function(name){
            console.log(name);
        }
    });

这样,咱们就能够利用jquery很简单的发送jsonp了.

SSE

ajax和JSONP 都是 client-fetch的操做. 可是有时候, 咱们更须要服务器主动给咱们发信息. 好比,如今的APP应用,彻底能够实现服务器发送, 而后Client再处理. 而,SSE就是帮助咱们向webapp靠近.
SSE 全称就是 Server-Sent Events. 中译 为 服务器推送.
他的技术并非很难,和websocket不一样,他依赖原生的HTTP,因此对于开发者来讲更好理解。 好比,在nodeJS, 只要我不执行res.end(),而且必定时间持续发送信息的话,那么该链接就会持续打开(keep-alive). 其实通俗来讲,就是一个长链接. 因此,之前咱们一般使用ajax,iframe长轮询来代替他.可是这样有个缺点就是, 可操控性弱, 错误率高。 因此,正对于这点W3C, 以为须要在客户端另外指定一个机制--可以保证服务器推送, 实现链接的keep-alive,操做简单... 在这样背景下SSE诞生了.
但SSE和AJAX具体的区别在什么地方呢?

  • 数据类型不一样: SSE 只能接受 type/event-stream 类型. AJAX 能够接受任意类型

  • 结束机制不一样: 虽然使用AJAX长轮询也能够实现这样的效果, 可是, 服务器端(nodeJS)必须在必定时间内执行res.end()才行. 而SSE, 只须要执行res.write() 便可.

简单demo

先看一个client端, 一个比较简单的demo

var source = new EventSource('/dates');  //指定路由发送
source.onmessage = function(e) {  //监听信息的传输
    var data = JSON.parse(e.data),
        origin = e.origin;
};
source.onerror = function(e) { //当链接发生error时触发
    console.log(e);
};
source.onopen = function(e) { //当链接正式创建时触发
    console.log(e);
};

SSE主要就是建立一个EventSource对象. 里面的参数就是发送的路由, 不过目前还不支持CORS,因此也被限制在同源策略下.
在返回的source里面包含了,须要处理的一切信息.SSE也是经过事件驱动的,如上面demo所述. 这里,SSE一般有一下几类重要的事件.

eventName effect
open 当链接打开时触发
message 当有数据发送时触发, 在event对象内包含了相关数据
error 当发生错误时触发

上面几个方法比较重要的仍是message方法. message主要用来进行信息的接受, 回调中的event 包含了返回的相关数据.
event包含的内容

property effect
data 服务器端传回的数据
origin 服务器端URL的域名部分,有protocol,hostname,port
lastEventId 用来指定当前数据的序号.主要用来断线重连时数据的有效性

服务器返回数据格式

上文说过,SSE 是以event-stream格式进行传输的. 但具体内容是怎样的呢?

data: hi

data: second event
id: 100

event: myevent
data: third event
id: 101

: this is a comment
data: fourth event
data: fourth event continue

上面就是一个简单的demo. 每一段数据咱们称之为事件, 每个事件通过空行分隔. :前面是数据类型,后面是数据. 一般的类型有:

  • 空类型: 表示注释,在处理是会默认被删除.好比: this is a comment.

  • event: 声明该事件类型,好比message.

  • data: 最重要的一个类型, 表示传输的数据。能够为string格式或者JSON格式. 好比: data: {"username": "bobby"}

  • id: 其实就是lastEventId. 用来代表该次事件在整个流中的序号

  • retry: 用来代表浏览器断开再次链接以前等待的事件(不经常使用)

其实上面最重要的两个字段就是data,id. 因此,咱们通常获取的话就可使用 event.dataevent.lastEventId.
上文说道, 每一段内容是经过换行实现的, 那服务器端应该怎么实现, 写入的操做呢?
一样, 这里以nodeJS 为例:

res.write("id: " + i + "\n");
res.write("data: " + i + "\n\n");

经过使用'nn'进行两次换行操做--即,产生空行便可.

使用自定义事件

服务器端不只能够返回指定数据,还能够返回指定事件.不过默认状况下都是message事件, 但咱们也能够指定事件. 好比

event: myevent
data: third event
id: 101

这里出发的就是 myevent事件。 即, 这就是触发自定义事件的方式.
在front-end 咱们可使用addEventListener 来进行监听.

var source = new EventSource('/someEvents');
source.addEventListener('myevent', function(event){
    //doSth
}, false);

服务端使用SSE

因为使用的是HTTP协议,因此对于服务端基本上没什么太大的改变. 惟一注意的就是, 发送数据使用res.write()便可,断开的时候使用res.end();

res.writeHead(200, {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      "Access-Control-Allow-Origin": "*" //容许跨域
    });
var num =0;
var f = function(){
   if(num===10){
      res.end();
   }else{
    res.write("id: " + num + "\n");
    res.write("data: " + num + "\n\n");
    num++;
   }
   setTimeout(f,1000);
}
f();

Ok~ 这里有一个demo, 你们能够打开控制台看一下. 会发现,有一个链接一直处于Content-Download状态. 该链接就是一个SSE。
兼容性
目前SSE,在市面上大受欢迎, 不过总有一个SB, 离经叛道... 竟然连edge都不支持. 偶尔去翻了一下,还在underConsideration. 结果底下的评论基本都是xxxx. 有空能够去看看, 逼逼MS程序员.

websocket

websocket 不一样于其余的HTTP协议,他是独立于HTTP存在的另一种通讯协议。好比,像这样的一个路径ws://websocket.example.com/,就是一个websocket 通讯. 一般的实时通讯并不会传输大量的内容, 因此,对于HTTP协议那种,进行链接时须要传递,cookie和request Headers来讲, 这种方式的通讯协议,会形成必定的时延(latency). websocket通讯协议就是在这样的背景下诞生了, 他与SSE,ajax polling不一样的是--双向通讯.

talk is cheap, show the code

咱们来看一个简单的websocket demo

var socket = new WebSocket('ws://localhost:8080/');
  socket.onopen = function () {
      console.log('Connected!');
  };
  socket.onmessage = function (event) {
      console.log('Received data: ' + event.data);
      socket.close();
  };
  socket.onclose = function () {
      console.log('Lost connection!');
  };
  socket.onerror = function () {
      console.log('Error!');
  };
  socket.send('hello, world!');

能够说上面就是一个健全的websocket 通讯了. 和SSE同样,咱们须要建立一个WebSocket对象, 里面的参数指定链接的路由. 并且,他也是事件驱动的.
常见的事件监听有.

event effect
open 当ws链接创建时触发
message 当有信息到来时触发
error 当链接发生错误时触发
close 当链接断开时触发

websocket 发送数据

另外,websocket 最大的特色就是能够双向通讯。这里可使用.
ws.send()方法发送数据, 不过只能发送String和二进制. 这里,咱们一般call 数据叫作 Frames. 他是数据发送的最小单元.包含数据的长度和数据内容.
下面就是几种经常使用的发送方式

socket.send("Hello server!"); 
 socket.send(JSON.stringify({'msg': 'payload'})); 

  var buffer = new ArrayBuffer(128);
  socket.send(buffer); 

  var intview = new Uint32Array(buffer);
  socket.send(intview); 

  var blob = new Blob([buffer]);
  socket.send(blob);

另外还可使用binaryType指定传输的数据格式,不过通常都用不上,就不说了.
不过须要提醒的是, send方法,通常在open和message的回调函数中调用.

websocket 接受数据

同理,和SSE差很少, 经过监听message事件,来接受server发送回来的数据. 接受其实就是经过event.data来获取. 不过, 须要和server端商量好data的类型.

ws.onmessage = function(msg) { 
  if(msg.data instanceof Blob) { 
    processBlob(msg.data);
  } else {
    processText(JSON.parse(msg.data)); //接受JSON数据
  }
}

那server端应该怎样处理websocket通讯呢?
websocket虽然是另一种协议,不过底层仍是封装了TCP通讯, 因此使用nodeJS的net模块,基本就能够知足,不过里面须要设置不少的头. 这里推荐使用ws模块.

NodeJS 发送websocket数据

简单的websocket demo

var WebSocketServer = require('ws').Server
  , wss = new WebSocketServer({ port: 8080 });

//经过ws+ssl的方式通讯. 和HTTPS相似
wss.on('connection', function connection(ws) {
  ws.on('message', function incoming(message) {
    console.log('received: %s', message);
  });

  ws.send('something');
});

能够参考treeHouse 编写的WSdemo

为何websocket会有子协议

因为websocket 自己的协议对于数据格式来讲,不是特别的清晰明了,ws能够传输text,blob,binary等等其余格式. 这样对于安全性和开发性能来讲,友好度很低。因此,为了解决这个问题, subprotocols 出现了. 在使用时,client和server都须要配置同样的subprotocols. 例如:

var ws = new WebSocket('wss://example.com/socket',
                       ['appProtocol', 'appProtocol-v2']);

服务端须要将subprotocols发送过去, 在handshakes的过程当中,server 会识别subprotocols. 若是,server端也有相同的子协议存在, 那么链接成功. 若是不存在则会触发error, 链接就被断开了.

websocket 协议内容

websocket 是有HyBi Working Group 提议并建立的。 主要的内容就是 一张表.

相比TCP来讲, 真的是简单~
其实一句话就能够说完.

Figure 17-1. WebSocket frame: 2–14 bytes + payload

具体内容是:

  • 第一个比特(FIN) 代表, 该frame 是否信息的最后一个. 由于信息能够分多个frame包传送. 但最终客户端接收的是整个数据

  • opcode(4bit)--操做码, 表示传送frame的类型 好比text(1)|| binary(2)

  • Mask 比特位表示该数据是不是从 client => server.

  • Extended length 用来表示payload 的长度

  • Masking key 用来加密有效值

  • Payload 就是传输的数据

websocket 可否跨域?

首先,答案是。 但,网上有两部份内容:

WebSocket is subject to the same-origin policy
WebSocket is not subject to the same-origin policy

看到这里我也是醉了. 事实上websocket 是能够跨域的。 可是为了安全起见, 咱们一般利用CORS 进行 域名保护.
即,设置以下的相应头:
Access-Control-Allow-Origin: http://example.com
这时, 只有http://example.com 可以进行跨域请求. 其余的都会deny.
那什么是CORS呢?

how does CORS work

CORS 是Cross-Origin Resource Sharing--跨域资源分享. CORS 是W3C 规范中 一项很重要的spec. 一开始,ajax 收到 the same origin policy 的限制 奈何不得。 结果出来了JSONP 等 阿猫阿狗. 这让ajax很不安呀~ 可是,W3C 大手一挥, 亲, 我给你开个buff. 结果CORS 就出来了。
CORS 就是用来帮助AJAX 进行跨域的。 并且支持性也超级好. IE8+啊,亲~ 可是IE 是使用XDomainRequest 发送的.(真丑的一逼)
因此,这里安利一下Nicholas Zakas大神写的一个函数.(我把英文改成中文了)

function createCORSRequest(method, url) {
  var xhr = new XMLHttpRequest();
  if ("withCredentials" in xhr) {

    // 检查xhr是否含有withCredentials属性
    //withCredentials 只存在于XHR2对象中.
    xhr.open(method, url, true);

  } else if (typeof XDomainRequest != "undefined") {

    // 检查是不是IE,而且使用IE的XDomainRequest
    xhr = new XDomainRequest();
    xhr.open(method, url);

  } else {

    // 不然..基本上就不能跨域了
    xhr = null;

  }
  return xhr;
}

而后, 就能够直接,xhr.send(body). 那CORS其实就完成了.
但,withCredentials是什么意思呢?

CORS中的withCredentials

该属性就是用来代表,你的request的时候,是否带上你的cookie. 默认状况下是不带的. 若是你要发送cookie给server的话, 就须要将withCredentials设置为true了.
xhr.withCredentials = true;
可是,server并非随便就能接受并返回新的cookie给你的。 在server端,还须要设置.
Access-Control-Allow-Credentials: true
这样server才能返回新的cookie给你. 不过,这还有一个问题,就是cookie仍是遵循same-origin policy的。 因此, 你没法使用document.cookie去访问他. 他的CRUD(增删查改)只能由 server控制.

CORS 的preflight 验证

CORS的preflight request, 应该算是CORS中里面 巨坑的一个。 由于在使用CORS 的时候, 有时候我命名只发送一次请求,可是,结果出来了两个。 有时候又只有一个, 这时候, 我就想问,还有谁能不懵逼.
这里,咱们就须要区分一下. preflight request的做用究竟是什么。
preflight request 是为了, 更好节省宽带而设计的. 由于CORS 要求的网络质量更高, 并且 花费的时间也更多. 万一, 你发送一个PUT 请求(这个不常见吧). 可是服务端又不支持, 那么你此次的 请求是失败了, 浪费资源还不说,关键用户不能忍呀~
因此, 这里咱们就须要区分,什么是简单请求, 什么是比较复杂的请求
简单请求
简单请求的内容其实就两块, 一块是method 一块是Header

  • Method

    • GET

    • POST

  • Header

    • Accept

    • Accept-Language

    • Content-Language

    • Last-Event-ID //这是SSE的请求头

    • Content-Type ,但只有一下头才能算简单

      • application/x-www-form-urlencoded

      • multipart/form-data

      • text/plain

好比, 我使用上面定义好的函数createCORSRequest. 来发送一个简单请求

var url = 'http://example.com/cors';
var xhr = createCORSRequest('GET', url);
xhr.send();

咱们来看一下,只发送一次简单请求时,请求头和相应头各是什么.(剔除无关的Headers)

//Request Headers
POST  HTTP/1.1
Origin: http://example.com
Host: api.bob.com
//Response Headers
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Vary
Content-Type: text/html; charset=utf-8

上面就是一个简单的CORS 头的交互。 另外,说明一个Access-Control-Allow-Origin,该头是必不可少的.
原本在XHR中, 通常能够经过xhr.getResponseHeader()来获取相关的相应头。 可是 在CORS中通常能够得到以下几个简单的Header:

  • Cache-Control

  • Content-Language

  • Content-Type

  • Expires

  • ETag

  • Last-Modified

  • Pragma

若是你想暴露更多的头给用户的话就可使用,Access-Control-Expose-Headers 来进行设置. 多个值用','分隔.
那发送两次请求是什么状况呢?
咱们若是请求的数据是application/json的话,就会发送两次请求.

var url = 'http://example.com/cors';
var xhr = createCORSRequest('POST', url);
xhr.setRequestHeader('Content-Type','application/json');
xhr.send();

第一次,咱们一般叫作preflight req. 他其实并无发送任何 data过去. 只是将本次须要发送的请求头发送过去, 用来验证该次CORS请求是否有效.
上面的请求头就有:

OPTIONS HTTP/1.1
Origin: http://example.com
Content-Type: application/json
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive

Access-Control-Request-Method就是用来代表,该次请求的方法.
请求内没有任何附加的数据.
若是该次preflight req 服务器能够处理,那么服务器就会正常返回, 以下的几个头.

//Response Header
<= HTTP/1.1 204 No Content
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Max-Age: 86400
Access-Control-Allow-Headers: Custom-Header
Access-Control-Allow-Origin: http://foo.com
Content-Length: 0

说明一下里面的头

  • Access-Control-Allow-Methods: 指明服务器支持的方法

  • Access-Control-Max-Age: 代表该次preflight req 最长的生存周期

  • Access-Control-Allow-Headers: 是否支持你自定义的头. 好比: Custom-Header

这里,主要要看一下Access-Control-Max-Age. 这和preflight另一个机制有很大的关系. 由于preflight 已经多发了一次请求, 若是每次发送json格式的ajax的话, 那我不是每次都须要验证一次吗?
固然不是. preflight req 有本身的一套机制. 经过设置Max-Age 来表示该次prefilght req 的有效时间。 在该有效时间以内, 后面若是有其余复杂ajax 的跨域请求的话,就不须要进行两次发送验证了.
并且,第二次的请求头和相应头 还能够减小很多重复的Header.
第二次继续验证

=> POST 
- HEADERS -
Origin: http://example.com
Access-Control-Request-Method: POST
Content-Type: application/json; charset=UTF-8

<= HTTP/1.1 200 OK
- RESPONSE HEADERS -
Access-Control-Allow-Origin: http://example.com
Content-Type: application/json
Content-Length: 58

ok~
最后上一张 Monsur Hossain大神话的CORS server 的运做流程图=>
此处输入图片的描述
看不清的话,请新建一个标签页看,放大就能看见了.

发展图谱

很少说了, 上图~

fetch 补充

fetch 相关补充,能够查阅览 前端 fetch 通讯

转载请注明做者和原文地址:https://segmentfault.com/a/11...

相关文章
相关标签/搜索