【轻聊前端】网页动态化,前后通信的发展历程

现今各种框架、工具‘横行’,到处在讲原理和源码,更有跨端技术需要我们去探索,但如果基本功不好,学什么都是事倍功半,效果很不好,花费时间的同时打击自信心。此篇文章,为我所计划的【轻聊前端】系列第(十五)篇,旨在系统地、逻辑性地把原生JavaScript知识分享给大家,帮助各位较为轻松地理清知识体系,更好地理解和记忆,我尽力而为,望不负期待。

前言

前面的文章,我们聊了数据,操作数据的方法,网页载体,宿主,交互,工具箱已经很丰富。

但是,需要思考一个重要问题,我们一直在讨论语言本身,而网页,首先是用来呈现信息的——“信息从哪来?”

可以在html里写,但如果这样,它会一直不变,而且每个人都一样。

故而,信息“动态化”,是Web的重要特点之一,毫不夸张地说,它改变和引领了一个时代。

如果你是新手,从手敲代码输入内容,到从数据库拉取内容,是一种突破和跨越,即便老手,仍旧每天在和数据打交道,拿到准确无误的信息依然令人兴奋。

网页只有具备了动态化的内容,才是丰富的,和用户产生联系的。

本文就聊聊前后端通信的发展历程。

发展历程

CGI(Common Gateway Interface)

CGI,出现于1993年,距今快30年历史,它定义了Web服务器与外部程序之间的通信接⼝标准,Web服务器可以通过CGI执⾏外部程序,让外部程序根据请求⽣成动态内容。可以使用不同的编程语言编写。

缺点是效率低,编程困难。

ASP/PHP/JSP

它们出现的时间、特点,不尽相同,但较CGI更简单,效率更高。它们有一个共同点,页面是由后端处理完之后生成返回给前端,前后端有相当一部分代码耦合在一起。

通过这些方式同样能做到内容动态刷新,但有几个缺点。

(1)刷新时网页一片空白,影响用户体验。

(2)多数情况下,需要更新的只是网页中很小一部分,但这种方式会刷新整个页面,网络传输中传送了额外的信息,使速度变慢,增加了传输负担。

这种方式已经显得陈旧,虽然仍会有公司,包括大公司,由于历史原因仍有使用,但不作为讨论的重点,知道即可。

“无破坏”刷新、前后端分离

iframe——模拟异步传输

什么是异步传输?它如何模拟?上面提到了页面的整体传输,一片空白。异步传输就是局部更新,iframe可以用来实现局部化。

iframe是一种HTML标记,它会创建包含另外一个文档的内联框架,通过iframe框架可以在当前页面显示其他页面的信息。

具体操作,是将iframe的src属性设置为对另外一个页面的连接请求,并在当前页面通过JavaScript动态更新iframe的内容,就可以将服务端的数据响应到客户端,这样就不会出现主页面空白,等待刷新的现象,减少了刷新的内容,提高了速度。

至此,在当前时间节点,能被称为”老历史“的已经回顾完成,接下来的内容,才是当下需要学习和掌握的重点。

Ajax——真正的异步交互

在Web发展历程中,Ajax具有转折性意义。

2005年,Jesse James Garrett撰写了一篇文章,“Ajax—A New Approach toWeb Applications”。文中描绘了一个被他称作Ajax(AsynchronousJavaScript+XML,即异步JavaScript加XML)的方案。

它不是一门独立的技术,而是使用JavaScript对象——XMLHttpRequest跟XML相结合得出的实现方案。现在我们知道,它不局限于XML,只是名字被保留下来。

Ajax通过异步通信和响应完成页面的局部刷新,以此改善传统Web中大量不必要的整页刷新。

Ajax出现之后,Google就在Google地图、Google搜索建议、Gmail等投入使用,效果很棒,随后在全世界逐渐流行开来。

Ajax请求

创建

1
2
3
4
5
6
7
8
9
10
11
function requestMethod(type,url,data,contentType){
let XHR = new XMLHttpRequest()
XHR.open(type,url,true)
XHR.setRequestHeader('Content-type',contentType)
XHR.send(data)
XHR.onreadystatechange = function(){
if(XHR.readyState == 4 && XHR.status == 200){
// 请求成功返回内容
}
}
}

过程略显繁琐,不论是以前还是现在,都会有第三方工具提供更加简洁和强大的封装,后面会聊,现在先让我们深入细节。

type——请求类型

小tips:请求类型跟JavaScript无关,属于HTTP协议的范畴。

常见的有以下几种:

  • get:用于获取数据
  • post:用于创建数据
  • put:用于更新数据
  • delete:用于删除数据
  • options:判定服务器内容是否允许客户端访问

不常见的有:head、trace、connect、patch,感兴趣的可自行了解。

请求类型看起来很丰富,能充分应对各种需求,但你可能在很多项目中只看到get和post,这就是它”灵活“的地方,虽各有用途,但你用或者不用,并没有强制性,不会被阻止。建议还是应用合适的方法。

url——请求地址

前端发送请求是向服务端地址发起的,需要服务端提供。

data——请求传参

传参是需要注意的一点,不同的请求方式和后端处理不同的时候,都会不同。

主要有两种方式,一种是作为查询字符串(query),另一种放在消息体(body)。查询通常对应get,消息体通常对应post。

传输的数据类型即contentType,常见的有以下三种:

(1)application/x-www-form-urlencoded:原生form表单,放在body,数据按照key1=val1&key2=val2的方式进行编码,key 和 val 都进行了URL转码。

(2)multipart/form-data:通常表单上传文件时使用该种方式。

(3)application/json:消息主体是序列化后的JSON字符串。

请求回调

请求发出之后,我们需要知道什么时候返回结果,状态是什么,内容是什么。

成功了是一种处理,失败又是另一种。怎么判断呢?——onreadystatechange。

在这个回调中,有两个属性值可用于判断:readyState 和 status。

readyState代表请求的状态,跟结果无关,每当readyState的值发生变化,都会触发readystatechange事件。可以借此机会检查readyState的值。一般来说,我们需要关心的readyState值是4,表示数据已就绪。为保证跨浏览器兼容,onreadystatechange事件处理程序应该在调用open()之前赋值

如果readyState值为4,就需要判断status,它保存着HTTP状态码,当它的值是2xx或3xx,表示请求顺利完成,完成后,我们就拿到返回的数据进行后续处理和展示。

还有一些小工具,在某些场景很有用。

  • loadstart:接收到第一个字节时触发
  • progress:获取响应进度
  • error:请求出错时触发
  • abort:用来取消发出的请求
  • load:成功接收完响应时触发,和readyState值为4时类似
  • loadend:在通信完成时,且在error、abort或load之后触发

请求头(Request Headers)

每个请求在发送时都会携带请求头,用于传递除数据之外的信息。常见的有:请求方法、协议、接收的数据类型、请求来源、缓存策略等,数量繁多,每种都有其特定的作用,这里不展开。

开发者需要额外关注的,是自定义的请求头,通常是前后端约定用于接口鉴权,如果需要,可以使用setRequestHeader()方法。它接收两个参数:字段的名称和值。

为保证请求头被发送,须在open()之后、send()之前调用setRequestHeader()。

状态码

上面提到过状态码,状态码能够帮助我们判断请求结果是否可用,如果不可用,发生了什么类型的异常,从而更合理地进行后续处理。

主要分类如下:

  • 1XX 表示消息
  • 2XX 表示成功
  • 3XX 表示重定向
  • 4XX 表示客户端错误
  • 5XX 表示服务端错误

这就是为什么说status是2xx或者3xx代表请求顺利完成,因为这两种是没有出错且和数据相关的。状态码种类同样繁多,且每个项目规范不一,酌情使用,这里不再展开。

Ajax为Web发展立了大功,现在仍占据主流,但随着时间推移,它终将会被替代,替代者可能就是下面要登场的这位。

Fetch API

顾名思义,”获取“。

Fetch API能够执行XMLHttpRequest对象的所有任务,且更容易使用,接口更现代化。

它在使用上和Ajax有很大不同,主要是:

  • fetch()暴露在全局,可直接调用
  • XMLHttpRequest可以通过open的第三个参数选择是否异步(true为异步,false为同步),Fetch必须异步
  • 请求完成返回一个Promise,回调里包含返回对象
  • 使用这个对象的属性和方法获取资源并进行转换
  • 能够在包括主页面执行线程、模块和工作线程中使用

看个示例:

1
2
3
4
5
6
7
fetch(url).then((response)=>{
if(response.status == 200){
response.json().then(data=>{
console.log(data)
})
}
})

看代码就一目了然,直接调用fetch,参数url、响应response、属性status、方法json()、有用的数据data。

其中,方法有多种选择:blob、formData、json、text。

只使用URL时,fetch()会发送GET请求,只包含最低限度的请求头。要进一步配置如何发送请求,需要传入可选的第二个参数——init对象。init中包含但不限于:

  • method:请求的类型
  • cache:控制浏览器和HTTP缓存的交互
  • headers:指定请求头部
  • mode:指定请求模式,决定来自跨源请求的响应是否有效
  • signal:支持通过AbortController中断进行中的fetch请求

比如可以像这样发送请求:

1
2
3
4
5
6
7
8
9
10
11
let payload = JSON.stringify({
name:'idea'
})
let jsonHeader = {
'Content-Type':'application/json'
}
fetch('send-json',{
method:'POST',
body:payload,
headers:jsonHeader
})

需要注意的是,fetch请求不会帮你判断状态是否真正可用,只要响应完成,就会返回为resolve,所以,要在回调中用response.status和response.ok来判断,如果因为服务器没有响应而导致浏览器超时,这样的失败会导致reject。

同样,fetch支持通过AbortController/AbortSignal中断请求。调用AbortController. abort()会中断所有网络传输,适合希望停止传输大型负载的情况。中断进行中的fetch()请求会导致包含错误的拒绝。

fetch的大体使用就是这样,还有很多细节,可自行查阅。

实时交互

以上介绍的内容,在页面的生命周期,或者响应用户操作层面足够了,但有一些情况难以胜任,即实时交互。比如,你发出一个请求,后端处理结果的时机是不确定的,需要一个过程,这时候该怎么响应呢?

主流方案

有4种主流方案:

  • 轮询:客户端定时发送请求,就像普通请求一样。
  • 长轮询:客户端发送请求,服务端接收到请求后进行阻塞,并保持连接,当服务端有数据需要响应时,使用保持住的连接进行响应,并关闭连接
  • 长连接:和上一种类似,但最后不关闭连接,而是继续保持
  • 推送:客户端与服务端建立连接后,服务端可以直接向客户端推送数据

这几种都可以满足实时更新服务端信息的目的,下面介绍的属于最后一种。

Web Socket

iframe、AJAX都是基于HTTP协议进行Web交互。HTTP协议的工作模式对于构建实时Web应用存在诸多的限制,只能先由客户端提交请求,服务器端响应请求,并非是由服务器向客户端进行主动推送。

随着HTML 5标准的推出,提出了一种新的浏览器服务器通信协议—WebSocket协议。通过该协议可以在浏览器和服务器之间构建一条全双工的通信连接,支持服务器端向客户端主动推送信息,实现实时刷新页面的功能。

为了使用Web Socket,需要在Web服务器上运行一个程序(也叫Web Socket服务器)。这个程序负责协调各方通信,而且启动后就会不间断地运行下去。

前端使用示例:

1
2
3
4
5
6
7
const ws = new WebSocket('ws://localhost:8080')
ws.onopen = ()=>{
ws.send()
}
ws.onmessage = ()=>{}
ws.onerror = ()=>{}
ws.onclose = ()=>{}

ws开头的url,是一个表示Web Socket连接的新协议,还可以是wss表示安全加密。

创建WebSocket对象后,页面就会尝试连接服务器。然后就是使用WebSocket对象的4个事件:onOpen、onMessage、onError、onClose。分别对应”建立连接、收到消息、发生异常、连接关闭“。

连接成功后,需要向服务器发送一条消息。发送消息要使用WebSocket对象的send()方法,这个方法接收纯文本内容作为参数。

Web Socket在前端的使用就是这样,但使用之前要理解两点。

第一,Web Socket是一种专用手段,比较适合开发聊天室、大型多人游戏,或者端到端的协作工具,而不是每一种类型。在适合的场景使用才好。

第二,Web Socket方案做起来可能比较复杂。JavaScript代码很简单,服务端代码不好写,要对多线程和网络模型有深刻理解。

到这里,关于前后端数据通信基本介绍完了,但是,还有一个角色,虽然跟业务数据无关,也属于这个范畴。

Beacon API

介绍之前,先聊一个话题——数据上报

一般的请求是用来拉取动态数据的,但还有一类请求,它不是来展示数据,而是帮助统计数据,什么数据呢?跟用户行为相关的数据。这个动作称为”数据上报“。

上报的对象是服务器,至于是自有,还是第三方,看情况定。

过去,解决这个问题有如下几种方案:

  • 发起一个同步 XMLHttpRequest
  • 使用<img>元素
  • 创建一个几秒的 no-op 循环

一个奇怪的角色出现了,Image对象,你可能会问,这两者似乎完全不搭,是怎么产生联系的。大体有以下几点:

  • 数据上报不需要回馈,对页面没有、也不应该有实际的展示或者交互影响。
  • 方法足够轻量,性能消耗小。
  • 图片作为替换元素,有src属性可用来传递url
  • 兼容性极好

有优点也有缺点,它的缺点:

  • 请求方式只能是get
  • url长度受限
  • 响应数据类型受限,状态处理受限
  • 用户代理会延迟文档卸载,以完成挂起的图片加载

既然以上几种方案都互有优劣,有没有一种结合它们优点的方式?

Beacon API就是。

语法如下:

1
2
navigator.sendBeacon(url);
navigator.sendBeacon(url, data);

sendBeacon有两个参数:URL和要在请求中发送的数据data。data参数是可选的,其类型可以是ArrayBufferView、Blob、DOMString 或FormData。如果浏览器成功的以队列形式排列了用于传递的请求,则该方法返回“true”,否则返回“false”。

当我们写了这些代码,它会尝试在卸载文档之前将数据发送到服务器。时机过早可能错失收集数据的机会。所以时机控制很重要,恰恰这是开发人员难以做到的,API帮我们解决了。

有几点要说明:

  • sendBeacon()并不是只能在页面生命周期末尾使用,而是任何时候都可以使用。
  • 浏览器保证在原始页面已经关闭的情况下也会发送请求。
  • 状态码、超时和其他网络原因造成的失败是不透明的,不能通过编程方式处理。

嗯,这次真结束了~

工具

通信方法部分聊完了,简单介绍几个工具,

第三方封装的请求Jquery.ajax()、axios。

接口调试工具mock、postman、apipost、apifox等。

结语

前后通信,接口调试,是现代Web必有的环节,当我们写好页面结构和布局,就是它了。

本文需要重点关注的是Ajax、Fetch、其次Websocket,最后Beacon。

文章够长,希望你有收获,下篇见。