意见箱
恒创运营部门将仔细参阅您的意见和建议,必要时将通过预留邮箱与您保持联络。感谢您的支持!
意见/建议
提交建议

你该知道的浏览器请求与Header

来源:恒创科技 编辑:恒创科技编辑部
2024-02-03 13:33:59


从url到页面展示过程中发生了什么

让我们从一道“经典前端面试题” —— 从url到页面展示过程中发生了什么说起:

url解析,根据dns系统进行ip查找;

① 网络标准规定了 url 只能是数字和字母,还有一小些特殊符号。url 可以携带参数。如果不对 url 转义可能会出现歧义。比如 ​​?key=value​​​ 中可能key本身就包括​​=​​​符号;
url 编码是以 utf-8 为标准,但不是所有浏览器对所有情况都是这样。像JS的话就有 ​​​encodeURIComponent​​​ 和 ​​encodeURI​​​ 来保证以 utf-8 编码。
② dns解析流程是 hosts(映射ip) -> 本地dns解析器(缓存) -> 计算机上配置的dns服务器 -> 全球根dns服务器
前端的dns优化就是在 head 里添加:​​​<meta http-equiv="x-dns-prefetch-control" content="on" />​​​ 和 ​​<link rel="dns-prefetch" href="xxx" />​


你该知道的浏览器请求与Header

查找到 ip 之后,进行 TCP 三次握手发送http请求;建立完链接,请求HTML文件/资源,如果在缓存中有就直接拿,否则向后端要;

浏览器首次加载资源成功时,服务器返回200,此时浏览器不仅把资源下载了,而且把 response 的 header 一并缓存;
下一次加载资源时,首先会经过强缓存处理,​​​cache-control​​​ 优先级最高,比如 ​​cache-control:no-cache​​​ 就直接进入协商缓存步骤;如果是 ​​max-age=xxx​​​,就会先比较当前时间和上一次返回200时的时间差(比较date属性),如果没超过​​max-age​​​,命中强缓存,不发请求直接从本地缓存中读取该文件(这里注意:如果无 ​​cache-control​​​ 字段,会去比较 ​​expires​​​),过期的话就进入下一阶段:协商缓存;
协商缓存阶段会比较两个字段:​​​If-Modified-Since​​​ 和 ​​If-None-Match​​​,他们会在 header 中跟随请求到达服务端。首先会比较​​If-None-Match​​​- ​​Etag​​​,如果相同则命中协商缓存,返回304;如果不一致返回新资源 & 200.然后是 ​​If-Modified-Since​​​ -​​last-modified​​(跟服务端获取的文件最近改动的时间比较),如果一致命中协商缓存,返回304;否则返回新的 last-modified 值和文件以及200;

服务器处理完会返回HTML;TCP 四次挥手结束请求;浏览器解析HTML,有“构建 DOM 树”->“构建 CSSOM 树”->“执行JS”->“根据 DOM 和 CSSOM 合并生成 render 树”->“渲染”->“布局”->“绘制”

一次完整的请求正式结束。其实这和 img 的 src、script 的 src 一样,可以看做一次get请求!


请求与响应中的Header

上面缓存那里提到了一些头字段,我们来看一下:

你该知道的浏览器请求与Header_跨域

可以看到,一次请求包括“请求”和“响应”两个部分。他们具体是:

请求行请求头请求体状态行(响应)响应头(响应)响应正文(响应)响应头

其中“请求头”和“响应头”使我们比较关注的。先来看响应头 —— 他们都是由服务端控制的,用来实现不同的功能。比较常见的有:

etag:资源唯一标识last-modified:请求资源的最后修改时间cache-controlexpiresDateAccess-Control-Allow系Set-Cookie: 服务端设置管理状态所用的cookie信息(这里面也有expires指令,它仅控制cookie的存活时间,如果不指定则默认到浏览器关闭)(在里面还可以通过 Secure 和 HttpOnly 来进行web安全防御)

先说 ​​Access-Control-Allow​​系,他们是用来跨域和设置服务端允许接收什么形式的请求;

你该知道的浏览器请求与Header_http_02

然后是 ​​Date​​​:服务端消息发出的时间。它的值应该是上一次请求为200时的时间,用来在下一次请求强缓存阶段和 ​​max-age​​​ 属性做比较,它是一个 GMT 时间;
然后是 ​​​expires​​​:它是一个绝对时间,规定了缓存在什么时间后过期。因为绝对时间对应用场景的限制,才出现了 ​​max-age​​ :相对时间(相对资源上一次被请求的时间);

最重要的便是这个 ​​cache-control​​ 了。它包含几个值:

public:表明响应可以被任何对象缓存;private:响应只能被客户端缓存;no-store:表示当前请求资源不准被缓存;no-cache:资源有缓存,但必须向服务端发一次请求(跳过强缓存,直接进入协商缓存,比较 Etag 和 last-modified)max-ages-maxage…几个经典问题

1、这里 ​​no-store​​​ 和 ​​no-cache​​​ 引起了我的注意:他们有什么用呢?真的如定义所说么?
经过验证,对于 ​​​no-store​​​ 来说,正如它定义的那样,资源不会被以任何形式的缓存,在下一次请求时依然会被服务器认为是“第一次请求”;而对于 ​​no-cache​​​来说,它更偏重比较两次资源有没有变化,它和 ​​max-age=0​​​ 表现一致,在下一次请求时,虽然浏览器缓存中有,但仍然向服务端发一次请求确认(会携带​​Etag​​),如果资源没有变化,返回304(只返回)表示资源还可以继续使用,浏览器才会从缓存中拿到资源;否则按照新的请求返回并重新缓存。

2、为什么 ​​no-cache​​​ 优先级比 ​​no-store​​​ 高?
这个问题笔者翻遍资料也没找到,但是根据上面的描述我可以猜测:是因为确保文件的时效性的同时兼顾性能。如果请求发现文件并没有改动,这时候实际上也并不需要重新传输资源并拿到页面上,直接从缓存中取的话更快一些。

3、为什么​​cache-control​​​(有指定 max-age 时)的优先级比​​expires​​​高?
原因在于Expires控制缓存的原理是使用客户端的时间与服务端返回的时间做对比,那么如果客户端与服务端的时间因为某些原因(例如时区不同;客户端和服务端有一方的时间不准确)发生误差,那么强制缓存则会直接失效,这样的话强制缓存的存在则毫无意义;而且前面说了,expires控制的绝对时间,也就是说你指定的是哪一年几月几日几时几分几秒,大多数场景下这样非常不方便。

哦对了,上面不断提到了协商缓存阶段用到的几个头字段 ​​Etag​​​ 和 ​​If-None-Match​​​、​​last-modified​​​ 和 ​​If-Modified-Since​​,前面说会首先比较Etag,那为什么 Etag 优先级要更高一些呢?

首先,If-Modified-Since 只能检查秒级别的时间(或者说 Unix 记录 MTIME 只能精确到秒),对于文件修改非常频繁的情况,它就无能为力了。其次,last-modified 日期并不可靠。有时开发人员会在修复某些内容后将所有文件上传到服务器,即使内容仅在子集上修改,也会重置所有文件的 last-modified 日期。而 Etag 不一样,它是由开发者生成的对应资源在服务端的唯一标识,它本质上是使用像 SHA256 这样的散列函数生成的一段 hash值。请求头

http请求中的“请求头”同样优秀,这里列举一些常见的:

Accept系:客户端可接受的一些编码、内容类型等方面的设置Cache-Control:本资源的缓存机制Connection:是否需要长久连接(http1.1默认开启)cookie:发送请求时,会把保存在该请求域名下的所有cookie一起发送过去content系:请求的信息,比如:请求体的字节长度(请求体是指在HTTP头结束后,两个CR-LF字符组之后的内容,常见的有POST提交的表单数据,并不包含请求行和HTTP头的数据长度)、是否基于MD5算法对请求体内容进行Base64二进制编码、设置请求体中的内容的 mime 类型(适用于post和put请求)Date:发送请求的日期Origin:发送请求的资源的“协议名+域名”referer(referrer):发送请求的资源的地址Upgrade:传输协议(学过websocket的应该不怎么陌生)User-agent:用户信息If-None-Match:值为服务器先前响应的Etag,用来比较判断是否资源发生了改变If-Modified-Since:请求资源是否在指定时间后才被修改…

这里发现:请求头中有 ​​cache-control​​​ 字段,而响应头中也有这个字段!他们是怎么发挥作用的?
事实上,它们之间的关系是:响应头(后端设置的)中控制缓存的开启,而请求头(前端ajax设置)可控制不缓存。 我们用 node做服务器来看一下:

// 前端代码
const ajax=new XMLHttpRequest();
ajax.open('get','http://localhost:8083/assets');
// ajax.setRequestHeader('cache-control','max-age=0');
ajax.onreadystatechange=function(){
if(ajax.readyState==4 && ajax.status==200){
console.log(ajax.responseText);
}
}
ajax.send()

这段代码中有一行注释,打开则表示前端设置不进行缓存,如果打开,则请求时的请求头与响应头是这样的(此时每次请求都不会再走强缓存,而是直接向服务端发送请求):

你该知道的浏览器请求与Header_request header_03

但若关闭注释,表示一切以后端设置的为主。(这种情况 Request Header 里面是没有 cache-control 这个字段的)

// koa代码
const Koa=require("koa");
const Router=require("koa-router");
const cors=require('koa2-cors');
//引入静态服务模块
const staticFiles = require('koa-static');
const router=new Router();

const app=new Koa();

app.use(cors({
origin:'*',
credentials:true // koa中控制允许接收cookie的方式
}))


app.use(staticFiles(__dirname + '/试验的HTML页面都在这'))
app.use(async(ctx,next)=>{
console.log('这是全局测试',Date.now())
await next()
})

router.get('/assets',async (ctx,next)=>{
console.log(ctx);
ctx.set('cache-control', 'max-age=30');
ctx.body='这是一次test'
})

app.use(router.routes())
app.use(router.allowedMethods())

app.listen(8083);

我们可以看下这时候的效果:

你该知道的浏览器请求与Header_request header_04

请求头中还有两个字段是需要重点提到的:
首先是cookie —— 可能前端都听过“cookie会被自动携带到 header 中发送到服务端”。有时候我们也会配合后端使用token(JWT)。这里涉及到“简单请求”和“非简单请求”的概念:

简单请求:请求方法是GET、POST、HEAD之一,且HTTP头信息只有/少于“​​Accept​​​”、“​​Accept-Language​​​”、“​​Content-Language​​​”、“​​Last-Event-ID​​​”、“​​Content-Type​​​”,并且​​Cnotent-Type​​​的值仅限于“​​application/x-www-form-urlencoded​​​”、“​​multipart/form-data​​​”、“​​text/plain​​”;非简单请求:对服务器有特殊要求,比如请求方式是PUT、DELETE,或者​​Content-Type​​​字段类型是​​application/json​​,或者HTTP头信息中加了自定义header(比如token);

他们通常发生在“跨域请求”中的 cors 场景中(这属于Ajax请求的范畴,也是浏览器请求的一种,故而放到本文)。
其次是 origin ——它也和跨域密切相关,我们一起聊聊。


跨域的解决

说起跨域,简单来说就是浏览器对两个不同源的资源之间资源共享的限制。前端js对跨域的解决方法有很多,这里说下最主要的两种:cors和jsonp。

cors原理

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在请求头信息中增加一个 origin 字段(这是浏览器自动添加的)。如果 origin 指定的源不在许可范围内,服务端会返回一个正常的HTTP响应。浏览器发现响应头信息中没有包含 ​​Access-Control-Allow-Origin​​​字段,就知道出错了,从而抛出一个错误,这个错误被​​XMLHttpRequest​​的 onerror 回调捕获。

注意:这种情况下无法通过状态码识别,因为HTTP回应的状态码有可能也是200。

怎么判断origin指定的源在不在许可范围内?这就是cors解决跨域的方法:后端配置一些响应头,在里面指定域名、请求方法等信息:

// springMVC 代码
@Configuration
public class CrosConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*");
}
}

简单请求中要让浏览器携带cookie必须手动在ajax中设置:

xhr.withCredentials=true;

同时要让服务端同意(接受),通过指定 ​​Access-Control-Expose-Credentials​​ 字段。

对于非简单请求的跨域cors,浏览器会首先发出类型为 OPTIONS 的“预检请求”,请求地址相同,浏览器询问服务端当前网页所在域名是否在服务端的许可名单中,服务端对“预检请求”处理,并对 ​​Response Header​​ 添加验证字段,客户端接收到预检请求的返回值进行一次请求预判断,验证通过后主请求发起。这种情况浏览器只有在第一次请求后得到肯定答复时才发出主请求,否则会直接报错

非简单请求在服务端的配置和简单请求差不多,唯一不同的是:​​Allow-Origin​​字段必须是确定的(不能是“ * ”)!

关于这点,有的说可以有的说不行。笔者在项目中测试时不行的,也可能和项目的其他某些配置有关。

jsonp

jsonp的原理是利用了像script、img这类标签的src属性并不被同源策略约束的特性。它是动态生成script标签的方式。jsonp只允许get请求!

jsonp的实现模式是callback。也就是说,我们必须要接收一个回调函数去拿到返回值:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<script type='text/javascript'>// 后端返回直接执行的方法,相当于执行这个方法,由于后端把返回的数据放在方法的参数里,所以这里能拿到res。
window.showLocation = function (res) {
console.log(res)
//执行ajax回调
}</script>
<script src='http://127.0.0.1:8080?callback=showLocation' type='text/javascript'></script>
</body>
</html>

和这段代码表述的意思相同,jsonp的原理就是:创建一个回调函数,然后在远程服务上调用这个函数并且将JSON数据作为参数传递,完成回调(它的本质是在服务端以结果为参数调用传递过去的函数名,并且将这一切返回给前端,这样就相当于在js中直接执行了函数)。

// node.js服务端代码
const http = require("http");
const server = http.createServer();

server.on("request", (req,) => {
res.setHeader("Content-Type", "text/html;charset=utf-8");
res.end("showLocation('我是返回值')"); // !这里当心
});

server.listen(8080, () => {
console.log("请访问 http://127.0.0.1:8080");
});

为什么jsonp要以这种方式?

一般情况下,我们希望这个 ​​script​​​ 标签能够动态调用,而不是固定在HTML中所以没等页面显示完就执行了,很不灵活。我们可以通过js动态创建 ​​script​​ 标签,然后以回调函数名作为参数传递,回调函数的参数又是服务端调用这个参数后传递进去的结果。这样就可以灵活调用远程服务了。

jsonp的不足?

JSONP 的优势就在于简单易用、能够直接访问响应文本、支持在浏览器与服务器之间双向通信,也就是说,包含 JSON 编码数据的响应体会自动解码(即执行)。
但这也说明JSONP“ 依赖 ”其他域中的代码。如果其他域不安全,很肯能会在响应中夹带一些恶意代码,此时除了放弃 jsonp 调用之外没有任何办法;
其次,要确定 JSONP 请求是否失败并不容易。虽然 HTML5 给 ​​​script​​​ 元素增加了 ​​onerror​​ 事件,但目前支持情况不是很好。为此你必须使用计时器检测指定时间内是否接收到响应。


上一篇: js设计模式在web前端开发中的实践——网站登录 下一篇: 手机怎么远程登录云服务器?