编者按:本文由混元霹雳手投稿,原标题 “人生苦短,了解一下前端必须明白的http知识点”
对于http的报文格式就不多细说了,因为做为前端开发,我们需要知道前后端联调时的请求和响应、请求头和返回头之间的关系和每个字段中的含义,静态文件资源在加载时我们可观察到的性能优化点,和一些日常请求报错如何去解决,更重要的是面试的时候如何去从容的应对面试官
以下的讲解纯属于个人理解,肯定会有错误和理解不到位的点,请在下方用你们猿族的语言喷起来
简单跨域的解决方式
跨域是一个老生常谈的话题,面试官问我如何解决跨域,以前只会和面试官说用webpack的 proxy做代理,叫后端大哥给我本地启一个nginx就可以了。在一些特殊的情况下,了解跨域根本性的知识点才能解决根本性的问题。
猿族前端 VS 猿族后端 java
后端说: 前端同志,我们先调一个get请求的一个接口,地址我给你,http://www.pilishou.com/getname/list
前端操作中。。。
fetch('http://www.pilishou.com/getname/list', {
method: 'GET'
})
写了一个这样的请求,听从后端大哥向服务端发送,此时浏览器报了一个这样的错误
Failed to load http://http:www.pilishou.com: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘null’ is therefore not allowed access. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.
小白前端会说: 大哥,你这是什么接口,请求了还报错,你那里什么鬼!
大牛前端会说: 大哥,帮个忙,你那里忘记设跨域头了。
小白后端会说:大哥,你会不会调接口,报错还找我,我这里postman上面调的一点问题都没有
大牛后端会说: 大哥,等一下,我的跨域头忘记设了,稍等
原理讲解:
在本地向不同域请求的时候,浏览器会做一个Origin请求头的验证,如果没有设置,在不同域名下或者本地请求时浏览器会向服务端发送请求,服务端也会向客户端发送对应的值,但是浏览器考虑到安全策略,会进行一个关于头信息的报错,此时对于后端来说,需要在response的返回头中加入 ‘Access-Control-Allow-Origin’: ‘*’, 来告诉浏览器我允许你进行一个跨域请求,不用报错,把值返回给请求者,这样你就可以安然地拿到数据。同时这样也会导致任何一个域名发送过来的请求,都允许跨域。但是可以通过’Access-Control-Allow-Origin’: ‘此处设置指定的域名’设定允许的跨域域名。
复杂跨域的解决方式
此时前端唱起来一首抖音网红歌,我知道我对你不仅仅是喜欢!。。。。。
后端说:小伙,这里有一个接口,需要遵循resutful接口,用PUT方法,http:www.pilishou.com/getname/update
前端操作中。。。。
fetch('http://www.pilishou.com/getname/list', {
method: 'PUT'
})
继续按部就班的写了一个这样的请求,然后又发现浏览器报了这样一个错误
Failed to load http://http:www.pilishou.com: Method PUT is not allowed by Access-Control-Allow-Methods in preflight response
小白前端会说: 大哥,你接口又怎么了,GET,POST都行,PUT怎么不行,肯定是你的问题,我别的什么都没动啊。
大牛前端会说: 大哥,帮个忙,你把请求头中加一些允许跨域的方法。
小白后端会说:大哥,你不会调接口,报错还找我,我这次postman上面调的还是一点问题都没有.
大牛后端会说: 大哥,等一下,我加一些允许跨域的方法,稍等
原理讲解:
在简单的跨域请求中 1. 请求方法是以下三种方法之一:HEAD、 GET、POST
2.HTTP的头信息不超出以下几种字段: Accept、 Accept-Language、Content-Language、 Last-Event-ID、Content-Type
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
如果不超过以上的限制,后端则只需要提供一个允许跨域的Origin就可以了,如果在请求方法超过了以上三种,需要添加 ‘Access-Control-Allow-Methods’: ‘PUT’, 同样浏览器为了安全,不允其它请求方法在前端进行跨域请求
同理复杂请求还包含着别的需要后端设置允许的一些跨域请求的方式,比如通常会出现的:
fetch('http://127.0.0.1:8887', {
method: 'PUT',
headers: {
'x-header-f': '1234',
}
})
报错信息:
Failed to load http://www.pilishou.com: Request header field x-header-f is not allowed by Access-Control-Allow-Headers in preflight response.
解决方案:
需要服务端加上允许那些自定义头进行一个跨域仿问 ‘Access-Control-Allow-Headers’: ‘x-header-f’
fetch('http://127.0.0.1:8887', {
method: 'PUT',
headers: {
'x-header-f': '1234',
'content-type': 'json'
}
})
报错信息
Failed to load http://www.pilishou.com: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
解决方案
需要服务端加上允许那些自定义头进行一个跨域仿问 ‘Access-Control-Allow-Headers’: ‘content-type’这个请求头信息
cache-control 的使用场景和性能优化
cache-control这个东西就是对服务端拉取的静态资源打上一个缓存标志
对于cache-control可以设置几种模式,通常前端工程师需要知道几种模式
max-age = 10000 (以秒为单位,根据需求设定)
no-cache (每次进行请求时都要向服务端进行验证,需要配合etag,Last-Modified使用)
no-store (每次请求都需要向服务端拉取新的资源)
private (私有的,不经过代理缓存)
public (公有的,如果本地失效,代理缓存存在的话可以从代理缓存进行通知用过期的资源)
max-age
当加载完资源时,浏览器会自动给我们存储到内存当中,但是浏览的失效时间是由内部机制控制的,在用nginx做静态资源的时候,在刷新的时候,浏览会向服务端再次发送是否过期的认证,在资源缓存时间确定的情况下,通过max-age指定强缓存后,浏览器再次加载同样的资源文件时,只需要从memory或者disk上面进行拉取复用。
达到以上的功能需要在返回资源的服务端对返回的资源设置’cache-control’: ‘max-age=时间(以秒为单位)’,当再次刷新页面的时候,在设置的时间之内刷新页面,不清除缓存的情况下都会重新拉取内存了中的缓存资源。
no-cache
no-cache 字面的字意是不缓存的意思,很容易迷惑人,但是本质的函意,意味着每次发送请求静态资源时都需要向服务端进行一次过期认证,通常情况下,过期认真证需要配合(etag和Last-Modified)进行一个比较,这个话题后继再展开讨论,如果验证并没有过期,则会发送304的状态码,通知浏览进复用浏览器的缓存
no-store
no-store 代表每次资源请求都拉取资源服务器的最新资源,就算同时设置max-age , no-store, no-store的优先级则最高,此时max-age则不生效,同样的会从服务端拉取最新的资源
private vs. public
在资源请求时,有些情况不会直接到原资源服务器发送请求,中间会经过一些代理服务器,比如说cdn,nginx等一些代理服务器,如果写入public的情况下,所有的代理服务器同样也会进行缓存,比如说s-maxage就是在代理缓存中生效的,如果本地max-age过期了,则会通过代理缓存,代理缓存并没有过期,会告诉浏览器还是可以用本地过期的缓存,但对于private中间代理服务器则不会生效,直接从浏览器端向原服务器进行一个验证。
缓存验证 Last-Modified 和 Etag
Last-Modified
最后修改时间,一般在服务端,对文件的修改都会有一个修改时间的记录,在nginx做静态资源时,nginx会返回一个Last-Modified表示最后修改的时间.在浏览器再次请求的时候,会把对应的 If-Modified-Since和If-UnModified-Since在请求头中再次发送给服务端,告诉服务端上次你给我文件改动的时间,但是Last-Modified只能以秒为单位,在有些情况下,是不够精确的
Etag
Etag 是一个更加严格的验证,主要通过一些数据签名,每个数据都有自己的唯一签名,一旦数据修改,则会生成另一个唯一的签名,最典型的做法就是对内容做一个hash计算,当浏览器端向服务端再请求的时会带上 If-Match 或者 If-Non-Match, 当服务端接收到后之后会对比服务端的签名和浏览器传过来的签名,这也弥补了Last-Modified只能以秒为单位,而不够精确的情况。
Last-Modified 和 Etag 配合 no-cache 使用
通常只会在 cache-control: no-cache 的情况下使用,浏览器也会对资源进行一个缓存, 同时会对服务端进行一个过期认证,一旦服务端返回304状态码,则说明可以复用浏览器的缓存,则会向服务端重新请求数据。
cookie的策略机制
cookie则是服务端和用户端之间一个像身份证一样的东西,一旦后端在返回头中设置了cookie,则在response中会出现设置的cookie数据,同时也会存在浏览器的application/cookie中,当每次发送请求的时候都会在request的头中带上当前域名下的cookie信息
健值对方式设置
在返回头中设置 ‘Set-Cookie’: ‘cookie内容’
设置过期时间
通常情况,在不设置过期时间的时候,浏览器关闭的时候,则cookie,则会失效,我们可以通过max-age或者expire进行一个cookie失效时间的设置
不可获取的 cookie
如果在不设置httponly的情况下,可以通过 document.cookie进行读取,在不同情况下,考虑安全性,可以通过httponly设置,使得 document.cookie无法获取。
https 下的 secure cookie
如果设置了secure只有在https的服务下才会把字段写入application/cookie中,虽然在response有发送cookie这个字段,但是浏览器识别到不是https服务时,会进行忽略。
二级域名下与二级域名的cookie传输
讲一个例子:
公司的所有内部系统都要走一个登陆系统。也就是sso单点登陆,如果登陆是在sso.pilishou.com的二级域名下,而你自己的开发的时候环境是 localhost:9999,当登陆成功时,此时cookie是设在sso.pilishou.com域名下,在本地 127 .0.0.1下发送请求,根本拿不到sso.pilishou.com下的 cookie信息,cookie根本不会从request header中带过去,可以通过host的映射,把127.0.0.1映射成 web.pilishou.com
但是问题来了,sso和 web都是二级域名,在web下同样拿不到 sso下的cookie,此时解决办法,在 sso登成功后,需要后台配合把cookie的信息通过 Domain设置到pilishou.com的主域下
在web二级域名下就可以拿到 sso下请求成功后设置的cookie信息,在不设置 httponly情况下,用document.cookie可以拿到自己想要的 cookie信息,但是在发送的时候,发现request头中根本没有把 cookie信息带入请求。在fetch请求中我们要设置credentials: ‘include’,意思代表允许请求时带上跨域的cookie, 此时就会发现cookie带入了request头部
经历了这么多的设置,在联调的时候,后端同样也需要配合你的行为,需要后端工程师配置在返回头中加入’Access-Control-Allow-Credentials’: ‘true’,允许进行 cookie的跨域,
但是问题又来了,真的好多问题,少一步都不行,此时你的浏览器又会报错,在设置跨域cookie的时候,不允许 response header设置 Origin为 * ,只能设置指定的域名进行一个跨域仿问,此时还需要后端工程师配合把前面的* 改成你当前指定的web.pilishou.com。
http长连接与性能优化的各种架构方式
在以前没有打包工具,或者没有应用到打包工具的时候,一个大项目会有一堆js,一堆css,会引起各种问题,会导致引入资源会出现混乱,资源加载慢,有些时候页面呈现了,点击的时候没有任何响应,这个需要从http请求资源时,经过三次握手后创建的TCP连接说起。
因为每个浏览器的执行策略不一样,所以我只针对Chorme来说,打开开发者工具,点击network,通过右键点击Name,有一个connect id, Chrome可以一次性创建六个并发连接,但是六个并发连接会阻塞后面的资源的请求,如果前六个资源文件很大,后面的资源请求会被一直阻塞着,会进行一个队列的等待请求,当页面在网络不稳的情况下,HTML,CSS,已经加载好了,也渲染完毕,JS 终于等到资源发起请求,但是突然网速变差,用户点击此时是没有任何响应的,因为JS根本还没有加载好。
为了验证,打开网络资源多的网站,把网速调到2G模式,会发现,一开始只会出现6次connect连接,但是也不是一下子全出来,因为创建TCP连接需要经过三次握手,这中间也是需要时间,当6个连接创建完成时,又回到了串行的方式,除非连接请求完成后才会让出连接资源,让下一个队列中的请求,进行复用,不用再创建新的TCP,但是TCP最后在关闭时,浏览器会与服务器自行进行一个协商关闭,也可以设置关闭时间,在多长时间没有请求后,才进行一个连接关闭,观查connect id会发现,只会出现6个connect id,其余的全会被复用,如果有些资源是复用的其它网站的,会另开新的 connect id
解决方案:
所以现在的对于SPA的页面,都采取了资源合并,把CSS,JS进行一个合并,通常在VUE中都打出4个文件,vendor.js, app.js, manifest.js 和 app.css
能让浏览器充分的刚好利用TCP连接并行下载所有文件,无论从性能上还是解决造成用户无响应的问题上都是很好的解决方案,那我再简单的讲解一次为什么要分成这四个文件。
1.vendor.js一般是 node_modules文件,不会轻意更改,所以可以通过浏览器缓存,能长期进行一个缓存 2.app.js 一般都是业务代码的文件,业务代码的文件,对于公司来说是业务代码很一个迭代很频繁的事,所以当用户拉取资源的时候,只需要拉需app的新资源, app.js中还可以分每个module进行一个资源更新,。 3.manifest.js 是一个runtime运行时的文件,无论app.js或者 vendor有改动,manifest文件则就会改动,所以也进行一个单独更新 4。app.css 需要综合考虑,虽然改动小部分资源,但是也会重新拉取,但是节省了请求的次数,对于合并自己可能根据项目进行酌情考虑。
多次复用,和单次复用的决择性
上面我们讲了因为浏览器的请求速度影响和,TCP连接的限制,我们采取了以上的方案,但是每个方案是针对不同的场景和架构的,对于后台管理项目,基本上公司都是统一工程化做的,所有的工程方案都是采用一套或几套,但是采用项目基础文件都是一样的,要升级也会根据项目特定需要才进行升级的,对于公司的内部系统,采用最好的方式就是放弃初次加载的性能,利用缓存进行多项目缓存复用。
通常一个vue的项目,vue.js vuex.js router.js 和一些公共的内部js文件都是在项目架构中集成的。
公司的内部项目一般都有三个环境,加上你本地调试有四个,如果把这些文件全打到vendor中,会产生只要项目重发之后,或者切换环境,在这个四个资源环境中就不能形成一个复用,因为域名都是不一样的,所以浏览器缓存不能共享,往往这些文件在所有项目中,所有环境中都是不会反复变的文件,一次加载,任何环境,任何项目共享利用缓存资源
我们可以利用cdn把以上前面提到的文件进行利用
也可以把文件放到一个域名下的公共目录下,进行利用。
from memory cache 和 from disk cache
from memory cache 从内存中拉取的缓存
from disk cache 从磁盘上拉取的缓存
在资源拉取过后,这里还是针对的Chrome进行解释,浏览器在拉取资源后,会对资源进行磁盘和内存缓存,而css文件会缓存到磁盘上,html,js,图片等文件都会在内存和磁盘进行缓存,当刷新页面时,除了在特定的资源中返回头中写入 cache-contorl: no-cache或者no-store的情况下都会直接从缓存中拉取资源,会在size中显示 from memory cache,而css文件则显示from disk cache, 但是 no-cache验证没有过期,则还会返回304进行读取缓存,只是到原服务器进行了一个验证。
meta http-equiv=”Cache-Control” content=”no-cache” 设置的必要性
此时前端和后端同学前后端已经联调好了,发到测试环境,让测试同志进行测试。
测试说:你页面中一个字写错了,改一下,重新发包我再来测,测试关闭浏览器,刷了一会抖音
前端一顿操作后 。。。。这一顿操作猛如虎
前端说:好了,你测吧。我发上去了,此时也关闭了浏览器,心里想测试要测试磨磨唧唧的,我先刷一会抖音。
测试一顿操作后: 。。。打开浏览器,把地址输入了进去,回车后。。。
测试说: 你到底改没改啊,怎么没有效果。
前端此时也打开浏览器,输入地址,一看,什么情况。开始怀疑人生了。。。我明明改了。怎么没效果。然后又是一顿猛如虎的打开文件看了看,又重新发包。
问题总结:
根本原因,正是因为缓存问题而导致,浏览器对html页会进行一个自动缓存,但是正常刷新情况下,如果用nginx做一个静态资源的情况下,都会进行一个304的重新向服端进行一个资源是否改动的验证,如果没有改动则进行一个304的缓存利用。
当关闭浏览器进程的时候,缓存在内存中的资源会随着浏览器的关闭一起清除,当再次打开浏览器的时候会从磁盘上读取缓存,这时候如果没有设置meta http-equiv=”Cache-Control” content=”no-cache”,当打开浏览器再次访问的时候,html页面初次浏览器会从磁盘上读取,即 from disk cache,那此时肯定用的还是原本旧的资源,这就是问题产生的根本,所以在加入每次都从原服务器验证资源,在打开浏览器的时候就不会出来资源没有及时更新的问题。
redirect 重定向的坑
重定向在response中会有一个location字段进行重定义,比如说返回值/list,需要我们重定向到/list的页面,但是在响应码中,可以返回 302或者301
301 适合永久重定向
301比较常用的场景是使用域名跳转。比如,我们访问 http://www.baidu.com 会跳转到 https://www.baidu.com 发送请求之后,就会返回301状态码,然后返回一个location,提示新的地址,浏览器就会拿着这个新的地址去访问。
302 用来做临时跳转
302和301的区别则是设置了302如果再次访问则是从服务端再次拉取资源,然后进行重定向。301则是如果有缓存文件,则直接读缓存文件上响应头上的重定向位置,如果原服务端重定向的位置有变化,则只能通过用户清除缓存进行重新拉取新资源进行再次重定向,所以301的使用需要严谨。
csp的理解 (cotent Security Policy) 内容安全策略
为了让我们网页更加安全
限制资源获取
资源获取越权
可以通过设置 default-src 设置全局需要资源的内容,也可以设置资源类型的范围。
connect-src 我们连接的资源
style-src 样式请求的资源
script-src 脚本的请求资源 。。。等等
可以通过响应头的返回设置Content-Security-Policy进行设置
有些些xss攻击是通过inline scrpit的方式注入代码进行攻击,可以通过设置进行禁用。可以设置’Content-Security-Policy’:’default-src http: https:’对inline scrpit进行禁用。设置之后,出现 inline script 将会报错:
Refused to execute inline script because it violates the following Content Security Policy directive: “default-src http: https:”. Either the ‘unsafe-inline’ keyword, a hash (‘sha256-9aPvm9lN9y9aIzoIEagmHYsp/hUxgDFXV185413g/Zc=’), or a nonce (‘nonce-…’) is required to enable inline execution. Note also that ‘script-src’ was not explicitly set, so ‘default-src’ is used as a fallback
不允许引入外部连接: 可以设置 ‘Content-Security-Policy’: ‘default-src self’ 实现,如果引用了外部的资源则会报:
Refused to load the script ‘http://static.ymm56.com/common-lib/jquery/3.1.1/jquery.min.js’ because it violates the following Content Security Policy directive: “default-src self”. Note that ‘script-src’ was not explicitly set, so ‘default-src’ is used as a fallback.
如果需要指定外链的地址,则可以在default-src加入指定的地址 其余的则可以根据 Content-Security-Policy 内容安全策略文档进行设置。
关于奇舞周刊
《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。