一.Http客户端
1.最简单的Http客户端
- 浏览器是最简单的Http客户端,当我们在链接中输入URL地址时,浏览器会发送请求得到返回内容并渲染出来
- 可以在浏览器开发者工具的Network中查看到请求与响应内容
- 能够实现发送Http请求报文的工具就是Http客户端
二.CORS跨域请求的限制与解决
-
模拟跨域请求
- 定义html模板用于显示页面并在script中发送请求到8887端口
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> This is a simple html template. </body> <script> // 发送get请求到8887 let xhr = new XMLHttpRequest(); xhr.open('GET','http://127.0.0.1:8887/'); xhr.send(); </script> </html>
-
定义server.js启动web服务在8888端口上用于读取html
const http = require('http'); const fs = require('fs'); http.createServer((request, response)=>{ console.log('Request come:', request.url); const html = fs.readFileSync('test.html', 'utf8');//以utf8的形式读取test.html response.writeHead(200, { 'Content-Type':'text/html'// 需要在请求头里定义,否则会认为是字符串 }); response.end(html); }).listen(8888);//监听在8888端口 console.log('Server listening on 8888');
- 定义server2.js启动web服务在8887端口上用于制造跨域问题
const http = require('http'); http.createServer((request, response)=>{ console.log('Request come:', request.url); response.end('123'); }).listen(8887);//监听在8887端口 console.log('Server listening on 8887');
-
将8888和8887端口上服务启动后,访问localhost:8888打开console会提示如下错误
Failed to load http://127.0.0.1:8887/: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8888' is therefore not allowed access.
-
按照提示给8887端口服务添加’Access-Control-Allow-Origin’请求头后就允许跨域了
const http = require('http');
http.createServer((request, response)=>{
console.log('Request come:', request.url);
response.writeHeader(200, {
'Access-Control-Allow-Origin':'*' //表示任何域名的页面都可以访问此服务
});
response.end('123');
}).listen(8887);//监听在8887端口
console.log('Server listening on 8887');
- 如果没有设置此请求头,浏览器其实也会发送请求到服务端,并接受服务端返回的内容,当解析时发现返回的响应头里没有Access-Control-Allow-Origin属性时,浏览器会忽略响应里的内容,从而在控制台报错(故跨域是浏览器提供的功能,必须要服务器设置允许跨域的请求头,浏览器才会将返回的内容返回回来)
- 浏览器允许像link,img,script等标签的src属性是跨域链接的,所以将上述url写在script标签中可以发现请求是可以成功发送的,即允许跨域
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
This is a simple html template.
</body>
<script src="http://127.0.0.1:8887/"></script>
</html>
- jsonp的原理就相当于添加了script标签,并在script标签中填写对应的跨域接口去发送请求访问资源
- 如果给定
'Access-Control-Allow-Origin':'*' //表示任何域名的页面都可以访问此服务
对于网站来说是不安全的,可以通过设置特定的域名的方式允许指定host访问当前服务
const http = require('http');
http.createServer((request, response)=>{
console.log('Request come:', request.url);
response.writeHeader(200, {
'Access-Control-Allow-Origin':'http://localhost:8888' //允许8888端口上的请求访问此服务
});
response.end('123');
}).listen(8887);//监听在8887端口
console.log('Server listening on 8887');
- 注意:localhost在dns中会被解析成127.0.0.1,但浏览器不会知道解析结果,所以如果从localhost访问127.0.0.1的服务也将认为是跨域请求
三.CORS跨域限制以及预请求验证
1.预请求允许方法
- GET / HEAD / POST 方法请求时不需要预请求去验证的
- 其他方法浏览器会有一个预请求的方式去验证的
2.允许Content-Type
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
- 以上三种是使用form表单可以设置的类型,除上述三种Content-Type其他请求发送的时候也会通过预请求验证后才会发送
3.其他限制
- 请求头限制 【参考: 请求头预请求限制】
- XMLHttpRequestUpload对象均没有注册任何事件监听器
- 请求中没有使用ReadableStream
4.自定义请求头实现跨域请求
-
初始代码
- server.js启动在8888端口上访问test.html资源并显示到页面
const http = require('http'); const fs = require('fs'); http.createServer((request, response)=>{ console.log('Request come:', request.url); const html = fs.readFileSync('test.html', 'utf8');//以utf8的形式读取test.html response.writeHead(200, { 'Content-Type':'text/html'// 需要在请求头里定义,否则会认为是字符串 }); response.end(html); }).listen(8888);//监听在8888端口 console.log('Server listening on 8888');
- test.html显示页面内容并通过script中使用fetch发送POST请求(设置自定义的请求头’Test-CORS’)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> This is a simple html template. </body> <script> fetch("http://localhost:8887/", { method: 'POST', headers: { 'Test-CORS': 'Jack' } }); </script> </html>
- server2.js启动在8887端口上并添加响应头
Access-Control-Allow-Origin':'*'
const http = require('http'); http.createServer((request, response)=>{ console.log('Request come:', request.url); response.writeHeader(200, { 'Access-Control-Allow-Origin':'*' }); response.end('123'); }).listen(8887);//监听在8887端口 console.log('Server listening on 8887');
-
可以看到在console里面报错:
Failed to load http://localhost:8887/: Request header field Test-CORS is not allowed by Access-Control-Allow-Headers in preflight response. Uncaught (in promise) TypeError: Failed to fetch
, 且在Network中有Request Method为OPTIONS的对应预请求 -
在server2.js添加响应头中的
'Access-Control-Allow-Headers':'Test-CORS'
属性,再次发送请求即可以看到console不进行报错提示,且Network中会有对应两个请求,一个是预请求的OPTIONS Method内容,一个是正常的POST内容
const http = require('http');
http.createServer((request, response)=>{
console.log('Request come:', request.url);
response.writeHeader(200, {
'Access-Control-Allow-Origin':'*',
'Access-Control-Allow-Headers':'Test-CORS'
});
response.end('123');
}).listen(8887);//监听在8887端口
console.log('Server listening on 8887');
- 同样,当我们想设置允许对应方法的预请求时,只需要添加
'Access-Control-Allow-Method':'PUT,DELETE'
即可突破跨域限制 - 浏览器为什么要设置跨域限制呢?
- 在网页中进行跨域操作的时候是保证服务端安全的,不允许随随便便的方法或请求头发送进行跨域,不希望通过跨域请求导致恶意篡改数据,从而在服务端我们可以进行判断:对应某些跨域的请求我们是否要进行响应
- 同样,我们可以通过
Access-Control-Max-Age
属性设置请求下面允许以指定方式进行跨域的请求最长时间,在指定的时间内不需要再发送预请求进行验证了,直接发送请求获取响应即可
四.缓存Cache-Control
1.Cache-Control特性
- 可缓存性【指定哪些地方可以进行缓存】
- public: 在http请求返回的过程中,在Cache-Control设置public值代表Http请求返回的内容所经过的任何路径当中,包括中间的Http代理服务器以及发出请求的客户端浏览器,都可以对返回内容进行缓存。【Http经过的任何地方都可以进行缓存】
- private:只有发起请求的浏览器进行缓存
- no-cache:任何节点都不可以进行缓存
2.到期Cache-Control值
max-age=<seconds时间>
设置缓存多少秒之后过期,之后再次发送请求到服务器端去请求新内容s-maxage=<seconds>
会代替max-age,只有在代理服务器里才会生效。即在浏览器端,浏览器还是会读取max-age作为缓存的到期时间,但是在代理服务器中如果同时设置了max-age和s-maxage,即会读取s-maxage的值max-stale=<seconds>
在max-age过期之后,如果我们返回的资源里面有max-stale设置是发起请求这一方带的头,表示即便缓存已经过期了,但如果还在max-stale时间内则还可以使用过期的缓存而不需要到服务器上发起新的请求。max-stale在浏览器是用不到的,因为浏览器在发起请求以及静态资源的请求当中,并不会主动设置这个头,而这个头只有在发起端设置才是有用的,在服务端返回内容设置是无用的
3.重新验证Cache-Control值
- must-revalidate: 在设置了max-age的缓存当中,如果它过期了就必须要到原服务端发起请求重新获取数据再验证内容是否已经过期,不能直接使用本地的缓存
- proxy-revalidate: 用在缓存服务器中,指定缓存服务器必须在过期的时候到源服务器上重新请求一遍,而不能直接使用缓存服务器上的缓存
- 通常以上两个头在浏览器访问页面中不常被用到,了解即可
4.其他Cache-Control值
- no-store: 需要跟no-cache进行区分,no-cache允许在本地/代理服务器上进行缓存,每次发起请求的时候都要到服务器上进行验证一下,如果服务器返回请求告诉可以使用本地缓存,你才可以真正使用本地缓存[需要在服务器端进行验证的]; 而no-store是本地/代理服务器上是一定不存缓存,每次都会到服务器上拿新的内容才能使用
- no-transform: 用在proxy服务器上,由于有的proxy服务器认为返回的资源太大了,帮进行一些压缩/格式转换,通过no-transform头可以告诉代理服务器不要改动返回的资源,即不要进行压缩或格式转换
5.示例代码
- 相关代码
- server.js通过判断针对不同请求设置
'Cache-Control':'max-age=20'
进行缓存
const http = require('http'); const fs = require('fs'); http.createServer((request, response)=>{ console.log('Request come:', request.url); if(request.url === '/') { const html = fs.readFileSync('test.html', 'utf8');//以utf8的形式读取test.html response.writeHead(200, { 'Content-Type':'text/html'// 需要在请求头里定义,否则会认为是字符串 }); response.end(html); } if(request.url === '/script.js') { response.writeHead(200, { 'Content-Type':'text/javascript', 'Cache-Control':'max-age=20'// 设置max-age为20s,可以使用逗号分隔添加多个Cache-Control值,后面的会替换前面的 }); response.end('console.log("script loaded")')//当发送请求获取/script.js资源时,就会打印console.log内容 } }).listen(8888);//监听在8888端口 console.log('Server listening on 8888');
- server2.js
const http = require('http'); http.createServer((request, response)=>{ console.log('Request come:', request.url); response.writeHeader(200, { 'Access-Control-Allow-Origin':'*', 'Access-Control-Allow-Headers':'Test-CORS' }); response.end('123'); }).listen(8887);//监听在8887端口 console.log('Server listening on 8887');
- test.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> This is a simple html template. </body> <script src="./script.js"></script> </html>
- script.js
fetch("http://localhost:8887/", { method: 'POST', headers: { 'Test-CORS': 'Jack' } });
- server.js通过判断针对不同请求设置
- 演示效果
- 勾掉Network中的
Disable cache
选项,否则会在浏览器发送请求的时候自动屏蔽Cache-Control头,导致每次不会从缓存中读,勾掉表示允许缓存的访问 - 第一次访问的时候看到script.js文件的size是有值的,表示数据是从网络获取的
- 刷新后发现size显示的是from memory cache,是从浏览器缓存中读取的,Time为0
- 勾掉Network中的
- 存在问题
为了前端访问静态资源的方便,我们通常会设置Cache-Control并给一个较大值,方便当浏览器再次请求时,静态资源不再重新获取,而使用本地缓存。但是如果服务器更新了静态资源代码后,由于浏览器在之前已经确定了读取的是缓存内容,即不会发送请求获取新的静态资源,此时就会出现静态资源的更新不同步 - 解决方案
通常我们的做法是将静态资源打包时给静态资源后根据静态资源内容给文件名后追加哈希码,如第一次打包script.js文件起名为script101010.js,更改了js文件后第二次打包由于内容的不同hash也会不同,从而导致文件名不同,即为script202020.js,此时由于url不同,浏览器会重新请求资源内容;若第二次该js文件内容没被更改,打包的hash值不会变更还是script101010.js,此时url还是相同的,浏览器即会使用缓存资源
这就是为什么我们通常使用静态资源打包后都会在文件后有一串hash码,而且hash码会变,是业界最通用的刷新浏览器缓存的方案
五.缓存验证Last-Modified和Etag的使用
1.验证头
- Last-Modified
- Etag
2.Last-Modified
- 上次修改时间
- 配合If-Modified-Since或者If-Unmodified-Since使用:如果我们请求资源,在返回的里面有LastModified头里面有对应的时间,在下次发送请求的时候会携带上,作为If-Modified-Since或If-Unmodified-Since值。服务器可以读取请求中的If-Modified-Since值和上次修改时间进行比较,如果自上次没有修改,服务器就会告诉浏览器可以使用缓存
- 对比上次修改时间以验证资源是否需要更新
3.Etag
- 通过数据签名进行判断
- 常用做法:对签名内容进行哈希计算,作为服务端返回的Etag的值
- 配合If-Match或者If-Non-Match使用:发送请求的时候携带服务端返回的Etag的值作为请求头中If-Match或者If-Non-Match的值,从而服务端接收到这个请求中的值对比资源的签名判断是否使用缓存
4.示例代码
- test.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
This is a simple html template.
</body>
<script src="./script.js"></script>
</html>
- script.js
fetch("http://localhost:8887/", {
method: 'POST',
headers: {
'Test-CORS': 'Jack'
}
});
- server.js
const http = require('http');
const fs = require('fs');
http.createServer((request, response)=>{
console.log('Request come:', request.url);
if(request.url === '/') {
const html = fs.readFileSync('test.html', 'utf8');//以utf8的形式读取test.html
response.writeHead(200, {
'Content-Type':'text/html'// 需要在请求头里定义,否则会认为是字符串
});
response.end(html);
}
if(request.url === '/script.js') {
response.writeHead(304, { // 304语义就是Not Modified,chrome会忽略服务返回的内容,并自动返回缓存的内容
'Content-Type':'text/javascript',
'Cache-Control':'max-age=2000000,no-cache',// 设置max-age为2000000s,保证下次请求还会发送到服务端使用no-cache
'Last-Modified' : '123',//访问第一次的时候会在response中携带回去,第二次发送请求的时候会在请求中携带If-Modified-Since请求头为123
'Etag' : '777'//下次发送请求的时候携带的请求头If-None-Match为777
});
const etag = request.headers['If-None-Match'];//读If-None-Match值,如果有则表示之前携带回去了
if(etag === '777') {
response.end('');
}else{
response.end('console.log("script loaded")')//当发送请求获取/script.js资源时,就会打印console.log内容
}
}
}).listen(8888);//监听在8888端口
console.log('Server listening on 8888');
- server2.js
const http = require('http');
http.createServer((request, response)=>{
console.log('Request come:', request.url);
response.writeHeader(200, {
'Access-Control-Allow-Origin':'*',
'Access-Control-Allow-Headers':'Test-CORS'
});
response.end('123');
}).listen(8887);//监听在8887端口
console.log('Server listening on 8887');
- 主要修改了server.js,模拟逻辑:添加了判断请求中的If-None-Match(响应的Etag携带的值,在请求时就是If-None-Match),如果匹配则写入304状态码表示Not Modified,浏览器会忽略服务器返回的内容并自动返回缓存的内容。如果没有则表示之前没有发起过请求,则正常输出响应内容
未完待续…