前端面试题考点之 通信(渲染、http、缓存、异步、跨域、安全)

合集:2023年最全前端面试题考点HTML5+CSS3+JS+Vue3+React18+八股文+手写+项目+笔试_参宿7的博客-CSDN博客

本章内容为一面基础面

为了简洁,相关文章参考链接在标题里

目录

服务器端渲染

基本概念

服务器端渲染SSR(前端服务器node,react/vue,seo)

客户端渲染CSR

浏览器从输入url到渲染页面 过程⭐⭐⭐

DNS

解析过程⭐⭐

OSI模型和TCP/IP协议⭐

HTTP协议

Http和Https区别⭐⭐⭐

常见的请求方式

GET和POST发送请求⭐⭐⭐

异同

POST的content-type数据编码

http报文

HTTP请求(Request)报文

HTTP响应(Response)报文

http版本⭐⭐⭐

http状态码⭐⭐⭐

UDP⭐

TCP⭐⭐⭐

三次握手

四次挥手

流量控制(滑动窗口机制)

拥塞控制

keep-alive持久连接

*粘包

缓存⭐⭐⭐

存储方式

强制缓存

协商缓存

本地存储

session

共同点

Cookie、localStorage和sessionStorage

存储地

内存缓存(from memory cache)

硬盘缓存(from disk cache)

应用

Token、JWT

认证(Authentication)

授权(Authorization)

凭证(Credentials)

Token(令牌)

Acesss Token

基于token的登录流程⭐⭐

Refresh Token

Token 和 Session 的区别

JSON Web Token(JWT)⭐

内容

方式

优缺点

Token 和 JWT 的区别

常见的加密算法

数据交换格式

XML

HTML和XML

应用

JSX

Babel⭐

JSON

JSON解析和生成

异步⭐⭐⭐

webworker(创建分线程)⭐

AJAX

原生AJAX创建⭐

*jQuery ajax

Axios⭐⭐

Axios API

setTimeout()(倒)计时(手写)⭐

setTimeout()和setInterval()⭐

Promise(ES6 解决地狱回调)⭐⭐⭐

promise调用then,

兼容同步任务

Promise.prototype.catch()

Promise.prototype.finally()

Promise.resolve()

Promise.reject()

Promise.all()

Promise.race()

Promise.all()哪怕一个请求失败了也能得到其余正确的请求结果的解决方案⭐⭐

Mypromise

fetch(ES6 拉取网络资源)

*Generator

async/await函数

SPA和MPA

URL

URL和URI

跨域通信⭐⭐⭐

JSONP跨域(⭐手写)

原生实现

封装JSONP

jQuery实现

跨域资源共享(CORS)(⭐手写)

简单请求

普通跨域请求

带cookie跨域请求(⭐手写)

cors中间件

复杂请求

postMessage

代理服务器

proxy代理服务器(ES6)

应用实例

Reflect(ES6)

handler拦截属性

get(target,propKey,receiver)

set(target,propKey,value,receiver)

deleteProperty(target,propKey)

取消代理Proxy.revocable(target, handler)

应用

Nginx反向代理

正向代理和反向代理

websocket协议

web安全及防护

XSS攻击

CSRF攻击⭐⭐⭐

SQL注入攻击

DDoS攻击

服务器端渲染

基本概念


SSR (server side render)服务端渲染,是指由服务侧(server side)完成页面的DOM结构拼接,然后发送到浏览器,为其绑定状态与事件,成为完全可交互页面的过程。
CSR(client side render)客户端渲染,是指由客户端(client side)JS完成页面和数据的拼接,生成DOM结构再交由浏览器渲染成页面的过程。
SPA(single page application)单页面应用,只是局部更新内容。SPA实现的原理就采用了CSR,页面中所有内容由JS控制,需要浏览器进行JS解析才能显示出来。
SEO(search engine optimization)搜索引擎优化,利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名。


服务器端渲染SSR(前端服务器node,seo)

在这里插入图片描述

前端耗时少。因为后端拼接了html浏览器只需直接渲染出来

不利于前后端分离,开发效率低。
有利于SEO。因为在后端有完整的html页面,所以爬虫更容易爬取获得信息,更有利于seo。

后端生成静态化文件。即生成缓存片段,这样就可以减少数据库查询浪费的时间了,且对于数据变化不大的页面非常高效 。


占用服务器端资源。无需占用客户端资源。即解析模板的工作完全交由后端来做。


客户端渲染CSR

在这里插入图片描述

综合来说,客户端渲染适合构建交互性强的应用,而服务端渲染适合需要更好的 SEO、更快的首屏渲染速度和更好的性能的应用。

也可以使用"同构应用"(服务器端渲染 + 客户端渲染结合),在某些页面使用服务端渲染,某些页面使用客户端渲染,以兼顾两者的优势。选择合适的渲染方式取决于具体项目的需求和特点。

浏览器从输入url到渲染页面 过程⭐⭐⭐

查找缓存

  • 合成 URL

浏览区会判断用户输入是合法 URL(Uniform Resource Locator,统一资源定位器),比如用户输入的是搜索的关键词,默认的搜索引擎会合成新的,

如果符合url规则会根据url协议,在这段内容加上协议合成合法的url 

  • 查找缓存

网络进程获取到 URL,

先去本地缓存中查找是否有缓存资源,如果有则拦截请求,直接将缓存资源返回给浏览器进程;

否则,进入网络请请求阶段;    

  • DNS 解析:(域名系统Domain Name System)

DNS 查找数据缓存服务中是否缓存过当前域名信息,有则直接返回;

否则,会进行 DNS 解析返回域名对应的 IP 和端口号

如果没有指定端口号,http 默认 80 端口https 默认 443

如果是 https 请求,还需要建立 TLS 连接;(传输层安全性协议Transport Layer Security)

TCP连接

  • 建立 TCP 连接:

TCP 三次握手与服务器建立连接,然后进行数据的传输;

  • 发送 HTTP 请求

浏览器首先会向服务器发送请求行,它包含了请求方法、请求 URI (统一资源标识符Uniform Resource Identifier HTTP 协议的版本

还会发送请求头,告诉服务器一些浏览器的相关信息,比如浏览器内核,请求域名

  • 服务器处理请求

服务器首先返回响应头+响应行响应行包括协议版本和状态码

  • 页面渲染:

查看响应头的信息,做不同的处理,比如重定向,存储cookie 看看content-type的值,根据不同的资源类型来用不同的解析方式

渲染详情可见2023年最全前端面试题考点HTML5+CSS3+JS_参宿7的博客-CSDN博客

  • 断开 TCP 连接:

数据传输完成,正常情况下 TCP 将四次挥手断开连接

DNS

因特网使用的命名系统,用来把人们方便记忆的主机名转换为机器方便处理的IP地址。

DNS协议属于应用层协议,一般是运行在UDP协议之上,使用53端口。

解析过程⭐⭐

1.当客户端需要域名解析时,通过本机的DNS客户端构造一个DNS请求报文,以UDP数据报的形式发往本地域名服务器

2.域名解析有两种方式:递归查询和迭代查询相结合的查询。

由于递归查询给根域名服务器的负载过大,所以一般不使用。

OSI模型和TCP/IP协议⭐

HTTP协议

HTTP:基于TCP/IP的关于数据如何在万维网中如何通信的协议。

无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息

Http和Https区别⭐⭐⭐

1.`HTTP` 的URL 以http:// 开头,而HTTPS 的URL 以https:// 开头
2.``HTTP` 无法加密,而HTTPS 对传输的数据进行加密,安全
3.`HTTP` 标准端口是80 ,而 HTTPS 的标准端口是443
4.`在OSI` 网络模型中,HTTP工作于应用层,而HTTPS 的安全传输机制工作在传输层

HTTPS是密文传输数据,HTTP是明文传输数据。

HTTPS协议 = HTTP协议 + SSl/TLS协议

用SSL/TLS对数据进行加密和解密

SSL的全称是Secure Sockets Layer,即安全套接层协议

TLS的全称是Transport Layer Security,即安全传输层协议

对数据进行对称加密,对 对称加密所要使用的秘钥 进行非对称加密传输。

  • 服务端的公钥和私钥,用来进行非对称加密
  • 客户端生成的随机秘钥,用来进行对称加密

常见的请求方式

  • POST:用于传输信息给服务器,功能与 GET 类似,但一般推荐使用 POST 方式;

  • GET: 用于请求访问已经被 URI(统一资源标识符)识别的资源,可以通过 URL 传参给服务器;

  • HEAD:,类似 GET 获得报文首部 ,只是不返回报文主体,一般用于验证 URI 是否有效;

  • PUT: 传输文件,报文主体中包含文件内容,保存到对应 URI 位置;

  • DELETE:与 PUT 相反,删除文件,删除对应 URI 位置的文件;

  • OPTIONS:查询相应 URI 支持的 HTTP 方法。

GET和POST发送请求⭐⭐

HTTP协议中的两种发送请求的方法。

异同

:GET和POST本质上就是TCP链接

数据包数量:GET产生一个TCP数据包;POST产生两个TCP数据包

(并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。)

过程

对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

对于POST请求方式,可以将请求数据打包在请求体中,

并通过Headers头部信息里的“Content-Type”字段指定请求体的数据类型为JSON,并

且在服务端返回相应头信息的时候也指定返回类型为JSON。

以下是一个使用POST方法请求并返回JSON类型数据的示例代码:

fetch('yourUrl', { 
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(requestData)
})
.then(function(response) {
  return response.json(); // 解析并返回JSON格式数据
})
.then(function(json) {
  console.log('请求成功返回结果', json);    
})
.catch(function (error) {
  console.log("请求出现了问题::", error);
});

其中 requestData 是一个 JavaScript 对象,可以通过 JSON.stringify() 方法将其转换为 JSON 字符串,然后放到 POST 请求的 Body 中。

应用

在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。

在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。

因为GET一般用于查询信息,POST一般用于提交某种信息进行某些修改操作(私密性的信息如注册、登陆)

所以GET在浏览器回退不会再次请求,POST会再次提交请求

因为GET在浏览器回退不会再次请求,POST会再次提交请求

所以GET请求会被浏览器主动缓存,POST不会,要手动设置
       GET请求参数会被完整保留在浏览器历史记录里,POST中的参数不会

因为 GET请求参数会被完整保留在浏览器历史记录里
所以GET请求在URL中传送的参数是有长度限制的,而POST没有限制

因为GET参数通过URL传递,POST放在Request body
所以GET参数暴露在地址栏不安全,POST放在报文内部更安全

POST的content-type数据编码

Content-Type(MediaType),即是Internet Media Type,互联网媒体类型,也叫做MIME类型。(最初MIME是用于电子邮件系统的)

在HTTP协议消息头中,使用Content-Type来表示请求和响应中的媒体类型信息。

它用来告诉服务端如何处理请求的数据,以及告诉客户端(一般是浏览器)如何解析响应的数据,比如显示图片,解析并展示html等等。

Content-Type的格式:type/subtype ;parameter

  • type:主类型,任意的字符串,如text,如果是*号代表所有;
  • subtype:子类型,任意的字符串,如html,如果是*号代表所有,用“/”与主类型隔开;
  • parameter:可选参数,如charset,boundary等。

POST 方法中对发送数据编码的方式,也就是 Content-Type 有四种方式,

application/x-www-form-urlencoded (URL encoded)(默认
multipart/form-data (键值对型数据)
application/json (Json 类型数据)(最方便)
text/xml (xml)(HTML文档标记)


传统的ajax请求时候,Content-Type默认为"文本"类型。

传统的form提交的时候,Content-Type默认为"Form"类型。

axios传递字符串的时候,Content-Type默认为"Form"类型。

axios传递对象的时候,Content-Type默认为"JSON"类型

  1. 文本类型(text)

    • text/plain:纯文本文件,例如 .txt 文件。
    • text/html:HTML 文档,用于网页。
    • text/css:层叠样式表文件,控制网页的样式。
    • text/xml:XML 数据。
  2. 图像类型(image)

    • image/jpeg:JPEG 图像,例如 .jpg.jpeg 文件。
    • image/png:PNG 图像,通常用于带有透明度的图像。
    • image/gif:GIF 图像,支持动画。
  3. 音频类型(audio)

    • audio/mpeg:MP3 音频文件。
    • audio/wav:WAV 音频文件。
    • audio/ogg:Ogg 音频文件,如 Ogg Vorbis。
  4. 视频类型(video)

    • video/mp4:MP4 视频文件。
    • video/webm:WebM 视频文件,常用于网络上的视频。
    • video/mpeg:MPEG 视频文件。
  5. 应用程序类型(application)

    • application/pdf:Adobe PDF 文件。
    • application/json:JSON 数据。
    • application/xml:XML 数据。
  6. 压缩类型(application)

    • application/zip:ZIP 压缩文件。
    • application/x-gzip:GZIP 压缩文件。
  7. 多媒体类型(multipart)

    • multipart/form-data:用于在 HTML 表单上传文件时的数据类型。

http报文

响应报头
常见的响应报头字段有: Server, Connection...


响应报文
你从服务器请求的HTML,CSS,JS文件就放在这里面

HTTP请求(Request)报文

报文格式为:请求行 – HTTP头(通用信息头,请求头,实体头) – 请求报文主体(只有POST才有报文主体)

HTTP响应(Response)报文

报文格式为:状态行 – HTTP头(通用信息头,响应头,实体头) – 响应报文主体

在这里插入图片描述

20210328150930116.png

http版本⭐⭐⭐

1.0->1.1(一次传输多个文件,默认Connection: keep-alive

http1.x解析基于文本

http2.0采用二进制格式,新增特性 多路复用、header压缩、服务端推送(静态html资源)

HTTP/2和HTTP/3,主要支持 并发处理减少延迟 方面。

  1. HTTP/1.0:关闭连接,无持久化(单个连接单个请求/响应),无管道化(并发发送请求)

    • 最早的版本,使用短连接:每个请求/响应后都会关闭连接,需要重新建立连接以发送下一个请求。
    • 不支持持久连接,每次请求都需要重新建立连接,导致较高的延迟。
    • 不支持请求头的管道化,只能等待前一个请求的响应返回后才能发送下一个请求。
  2. HTTP/1.1:持久(单个连接不同时间多个请求/响应),管道化,分块/流式传输(边收边处理)

    • 引入了持久连接:允许在单个连接上发送多个请求和响应,减少了连接建立的开销,降低了延迟。
    • 支持请求头的管道化:可以在同一连接上并发发送多个请求,而不必等待前一个请求的响应返回。
    • 引入了分块传输编码(chunked transfer encoding):支持流式传输,可以边接收边处理响应内容。
  3. HTTP/2:二进制传输(更小的帧)多路复(单个连接同时多个请求/响应),头部压缩,服务器推送

    • 引入了二进制传输:将请求和响应数据分割成更小的帧并进行二进制编码,提高了传输的效率和速度。
    • 支持多路复用:在一个连接上可以同时发送多个请求和响应,避免了"队头阻塞"问题,提高了并发性能。
    • 使用头部压缩:通过对头部信息使用压缩算法减少传输的数据量。
    • 支持服务器推送(server push):服务器可以主动推送资源给客户端,提前满足客户端可能需要的资源。
  4. HTTP/3:(QUIC协议)UDP代替TCP(时延,拥塞),传输层也能多路复用,0-RRT握手,改进头部压缩

    • 基于QUIC协议:使用UDP代替TCP作为传输层协议,减少了连接建立的时延,并且解决了TCP中的一些拥塞控制问题。
    • 支持多路复用:类似于HTTP/2,但在传输层也支持多路复用,进一步提高了性能。
    • 引入了0-RTT握手:允许在已建立连接的基础上进行更快的新连接建立。
    • 改进了头部压缩算法。

http状态码⭐⭐⭐

状态码是由3位数组成,第一个数字定义了响应的类别,且有五种可能取值:
    1xx Informational(信息状态码)      接受请求正在处理
    2xx Success(成功状态码)            请求正常处理完毕

    3xx Redirection(重定向状态码)      需要附加操作已完成请求

    4xx Client Error(客户端错误状态码)  服务器无法处理请求
    5xx Server Error(服务器错误状态码)  服务器处理请求出错

常见状态码:

    200 响应成功
    204 返回无内容

    301永久重定向  (请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。)
    302临时重定向(服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。)
    304资源缓存(自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。)

    400 错误请求(请求格式错误,服务器不理解请求的语法。)
    422 无法处理(请求格式正确,但是由于含有语义错误,无法响应)
    401 未授权(请求要求身份验证。)
    403服务器禁止访问
    404服务器找不到请求的网页
    

    500 502服务器内部错误
    504 服务器繁忙

UDP⭐

应用层协议DNS基于UDP

  • `TCP`向上层提供面向连接可靠服务 ,`UDP`向上层提供无连接不可靠服务。
  • `TCP`准确(文件传输),`UDP`实时(视频会议、直播
  • `TCP`仅支持一对一,`UDP`支持一对一,一对多,多对一和多对多交互通信
  • `TCP`面向字节流传输,`UDP`面向报文传输

TCP⭐⭐⭐

三次握手

四次挥手

TIME-WAIT:2 MSL (Maximum segment lifetime) 最长报文最大生存时间

流量控制(滑动窗口机制

让发送方的发送速率不要太快,要让接收方来得及接收。

还可接收的窗口是 rwnd = 400 ”(receiver window) 。

发送方的发送窗口不能超过接收方给出的接收窗口的数值。TCP的窗口单位是字节,不是报文段。

拥塞控制

拥塞:资源供不应求

  • 慢开始、拥塞避免
  • 快重传、快恢复

keep-alive持久连接

为了能够提升效率,在 HTTP 1.1 规范中把 Connection 头写入了标准,并且默认启用。

浏览器 或 服务器在HTTP头部加上 Connection: keep-alive,TCP 就会一直保持连接。

它会在隔开一段时间之后发送几次没有数据内容的网络请求来判断当前连接是不是应该继续保留。

可能会造成大量的无用途连接,白白占用系统资源

*粘包

在流传输中出现,UDP不会出现粘包,因为它有消息边界

TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

情况

粘包情况有两种,一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的包

措施

(1)对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;

(2)对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;

(3)由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。分包多发

问题

(1)设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。

(2)只能减少出现粘包的可能性,但并不能完全避免粘包,当发送频率较高时,或由于网络突发可能使某个时间段数据包到达接收方较快,接收方还是有可能来不及接收,从而导致粘包。

(3)避免了粘包,但应用程序的效率较低,对实时应用的场合不适合。

总结

接收方创建一预处理线程,对接收到的数据包进行预处理,将粘连的包分开。实验证明这种方法是高效可行的。

缓存

存储方式

白话:

强制缓存就是根据headers中的信息(expires,cache-control)强行从本地拿缓存,

拿不到再和服务器协商拿缓存,

如果服务器返回304(缓存无更新),就又从本地拿缓存。

否则,将从服务器那拿到的新资源存入浏览器

强制缓存

浏览器在加载资源的时候,会根据本地缓存中的headers中的信息(expires,cache-control)是否要强缓存,如果命中的话,则会使用缓存中的资源,否则继续发送请求。

其中Cache-Control优先级比Expires高。

Exprires的值为服务端返回的数据到期时间。当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。

但由于服务端时间和客户端时间可能有误差,这也将导致缓存命中的误差,另一方面,Expires是HTTP1.0的产物,故现在大多数使用Cache-Control替代。

协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识(If-None-Match/If-Modified-Since)向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程

  1. 协商缓存生效,返回304(缓存无更新),如下
    2.
  2. 协商缓存失效,返回200和请求结果结果,如下

在这里插入图片描述

强制缓存优先于协商缓存进行

本地存储

浏览器本地保存数据的一种方案,且会在同域名下的每次请求自动附加该域名关联的cookie。

cookie 最常见的用法是作为用户登录凭证,赋予原本无状态HTTP 协议以一种状态

​​

Cookie属性

必选属性:Name Value

可选属性:有效期、使用范围、安全性的可选属性组成

Cookie 的作用域是和 domain(域名或 ip)绑定的,端口无关

cookie 是不可跨域的: 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的靠的是 domain)

  • Expires/Max-Age属性:有两种存储类型的Cookie:会话性(默认)持久性

会话性cookie仅保存在客户端内存中,并在用户关闭 整个 浏览器时失效

持久性Cookie会保存在用户的硬盘中,直至生存期到或用户直接在网页中单击“注销”等按钮结束会话时才会失效

Max-Age:失效时间(单位秒)

负数,该 cookie 为临时 cookie关闭浏览器即失效,

如果为 0,表示删除该 cookie 。默认为 -1

如果 Max-Age 和 Expires 同时存在,以 Max-Age 为准

  • Path属性:指定 cookie 在哪个路径(路由)下生效,默认是 '/'
    如果设置为 /abc,则只有 /abc 下的路由可以访问到该 cookie,如:/abc/read
  • Domain属性:指定了可以访问该 Cookie 的 Web 域,默认当前域名
  • Secure属性:指定是否使用HTTPS安全协议发送Cookie。默认为false。安全协议有 HTTPS,SSL等,在网络上传输数据之前先将数据加密。
  • httpOnly属性:无法通过 JS 脚本 读取到该 cookie 的信息,防止恶意脚本通过访问 Cookie 来进行攻击。但还是能通过 Application 中手动修改 cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全

HTTP 响应报文通过 Set-Cookie 头字段给浏览器给当前网站设置 cookie:

HTTP/1.1 200 OK
Set-Cookie: token=abcd; Max-Age=6000; Path=/; Expires=Thu, 28 Apr 2022 16:31:36 GMT; HttpOnly
            name=value;           (s);      ;        <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT

Cookie的安全规范

  • 不放重要数据,重要数据放Session。
  • Cookie 数据加签名。对 Cookie 数据添加签名,这样 Cookie 如果被篡改了,服务端使用的时候通过校验签名就能发现了。
  • Cookie数据加密。加密后数据就很难篡改了,但是加解密过程会带来性能损耗
  • 现代的Web开发趋向于使用其他机制,如Token或Session来管理用户状态和身份验证,以减少对cookie的依赖。

session

就从存储在服务器上的无数条 session 信息中去查找客户端请求时带过来的 Cookie 的状态。如果服务器没有这条 session 信息就添加。

浏览器端第一次发送请求到服务器端,服务器端创建一个Session,同时会创建一个特殊的Cookie(name为JSESSIONID固定值,value为session对象的ID)(JSESSIONID:j session id)

服务器端根据该Cookie查询Session对象,从而区分不同用户。

session 是基于 cookie 实现的,

session 存储在服务器端,

sessionId 会被存储到客户端的cookie 中,是连接 Cookie 和 Session 的一道桥梁

共同点

cookie和session都是用来跟踪浏览器用户身份会话方式

用于浏览器中存储数据的

浏览器的本地存储主要分为Cookie、WebStorage和IndexDB运行在浏览器的非关系型数据库)

其中WebStorage又可以分为localStorage和sessionStorage

Cookie、localStorage和sessionStorage

同:都是保存在浏览器端、且同源的

异:

cookie在浏览器和服务器间来回传递

存储地

  • cookie数据保存在客户端,session数据保存在服务端,session更安全
  • sessionStorage和localStorage不会把数据发给服务器,仅在本地保存

存取值的类型

  • Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,
  • Session 可以存任意数据类型。

存储大小

  •  单个 Cookie 保存的数据不能超过 4K,
  • Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。

有效期

  • localStorage始终有效,窗口或浏览器关闭也一直保存,本地存储,因此用作持久数据;

  • cookie的有效期是可以设置的,默认情况下是关闭浏览器后失效。

  • sessionStorage的有效期是仅存在于当前会话,关闭当前会话或者浏览器后就会失效。

为了会话恢复或恢复上次打开的标签页,可能会导致浏览器保留会话信息,包括Session Cookie。关闭浏览器 不会导致 session 被删除,迫使服务器为 session 设置了一个失效时间,

当距离客户端上一次使用 session 的时间超过这个失效时间时,

服务器就认为客户端已经停止了活动,才会把 session 删除以节省存储空间

  1. 隐私浏览模式: 使用隐私浏览模式(如Chrome的隐身模式或Firefox的隐私浏览)时,会话Cookie通常会在关闭窗口后被删除,以确保隐私。

作用域不同

  • sessionStorage:不在不同的浏览器窗口中共享,即使是同一个页面

  • cookie,localstorage:在所有同源窗口中都共享;也就是说只要浏览器不关闭,数据仍在

存储地

​状态码为灰色的请求则代表使用了强制缓存,请求对应的Size值则代表该缓存存放的位置,分别为from memory cache 和 from disk cache。

  • 浏览器读取缓存的顺序为memory –> disk。
  • 访问URL–> 200 –> 关闭标签页 –> 重新打开URL–> 200(from disk cache) –> 刷新 –> 200(from memory cache)

内存缓存(from memory cache)

  • 快速读取:内存缓存会将编译解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取。
  • 时效性:一旦该进程关闭,则该进程的内存则会清空。

硬盘缓存(from disk cache)

重新解析该缓存内容,读取复杂,速度比内存缓存慢。

应用

js和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取;

css文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(from disk cache)
前端基础-浏览器缓存/HTTP缓存机制(面试常考)_浏览器缓存机制 面试_LYFlied的博客-CSDN博客

Token、JWT

认证(Authentication)

针对用户,验证当前用户的身份

互联网中的认证:

  • 用户名密码登录
  • 手机号接收验证码

授权(Authorization)

针对第三方应用,用户授予第三方应用访问该用户某些资源的权限

  • 你在安装手机应用的时候,APP 会询问是否允许授予权限(访问相册、地理位置等权限)
  • 你在访问微信小程序时,当登录时,小程序会询问是否允许授予权限(获取昵称、头像、地区、性别等个人信息)

实现授权的方式有:cookie、session、token、OAuth

凭证(Credentials)

实现认证和授权的前提是需要一种媒介(证书) 来标记访问者的身份

当用户登录成功后,服务器会给该用户使用的浏览器颁发一个令牌(token),这个令牌用来表明你的身份,每次浏览器发送请求时会带上这个令牌

Token(令牌)

Acesss Token

  • 访问资源接口(API)时所需要的资源凭证
  • 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)
  • 特点:
    • 服务端无状态化、可扩展性好
    • 支持移动端设备
    • 安全
    • 支持跨程序调用
  • token 的身份验证流程:

基于token的登录流程

1. 客户端使用用户名跟密码请求登录

2. 服务端收到请求,去验证用户名与密码

3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端

4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage

5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token

6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

  • 每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里
  • 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库
  • token 完全由应用管理,所以它可以避开同源策略

Refresh Token

refresh token 是专用于刷新 access token 的 token。

如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。

有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作。

  • Access Token 的有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Token,如果 Refresh Token 也失效了,用户就只能重新登录了。
  • Refresh Token 过期时间是存储在服务器的数据库中,只有在申请新的 Acesss Token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 Session 一样一直保持在内存中以应对大量的请求。

Token 和 Session 的区别

  • Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息
  • Token 是令牌访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。
  • 作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击
  • Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。
  •  Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。
  • Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。这里的 Token 是唯一的。不可以转移到其它 App上,也不可以转到其它用户上。
  • Session 只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。
  • 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token

JSON Web Token(JWT)

跨域认证解决方案。是一种认证授权机制

因为 JWT 通常使用 localstorage (也可以使用 Cookie),所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)

JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

内容

1.Header
JWT头是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存

{
  "alg": "HS256",
  "typ": "JWT"
}
2.Payload
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择

iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

方式

当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 Cookie 里面自动发送,但是这样不能跨域,

所以更好的做法是放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT

GET /calendar/v1/events
Host: api.example.com
Authorization: Bearer <token>

服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为。

由于 JWT 是自包含的,因此减少了需要查询数据库的需要

JWT 的这些特性使得我们可以完全依赖其无状态的特性提供数据 API 服务,甚至是创建一个下载流服务。

  • 跨域方式

JWT 放在 POST 请求的数据体里。

  • 通过 URL 传输方式
http://www.example.com/user?token=xxx

优缺点

  • 优点:

基于JSON,方便解析,可以在令牌中自定义丰富内容,易扩展。

通过非对称加密及数字签名技术,可以防止篡改、安全性高。

可以不依赖认证服务就可以完成授权。

  • 缺点:

JWT令牌较长,占存储空间比较大。

由于服务器不需要存储 Session 状态,因此使用过程中无法废弃某个 Token 或者更改 Token 的权限。也就是说一旦 JWT 签发了,到期之前就会始终有效,除非服务器部署额外的逻辑。

Token 和 JWT 的区别

相同:

  • 都是访问资源的令牌
  • 都可以记录用户的信息
  • 都是使服务端无状态化
  • 都是只有验证成功后,客户端才能访问服务端上受保护的资源

区别:

  • Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
  • JWT: 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。

常见的加密算法

哈希算法(Hash Algorithm)又称散列算法

JWT详解_baobao555#的博客-CSDN博客

数据交换格式

服务器端与客户端之间进行数据传输与交换的格式。

XML

HTML和XML

HTML(HyperText Markup Language超文本标记语言)

XML(Extensiable Markup Language可扩展标记语言)

发展史:HTML->XHTML->XML

HTML缺点:

  • 不能自定义标签
  • 本身缺少含义

应用

  • 程序间的数据通信(例:QQ之间传输文件)
  • 配置文件(例:structs-config.xml)
  • 小型数据库(例:msn中保存用户聊天记录就是用XML文件)直接读取文件比读数据库快。

JSX

JSX =JavaScript + XML。是一个 JavaScript 的语法扩展。

let element = React.createElement('h2', {title: 'hi'}, [
    'hello world',
    React.createElement('span', null, '!!!!!!')
]);

JSX写起来就方便很多了,在内部会转换成React.createElement(),然后再转换成对应的虚拟DOM,但是JSX语法浏览器不认识,所以需要利用babel插件进行转义处理

Babel

JavaScript 编译器

将es6、es7、es8等语法转换成浏览器可识别的es5或es3语法,即浏览器兼容的语法,比如将箭头函数转换为普通函数

将jsx转换成浏览器认的js

JSON

JSON,全称是 JavaScript Object Notation,即 JavaScript对象标记法。

是一种轻量级(Light-Meight)、基于文本的(Text-Based)、可读的(Human-Readable)格式。

名称中虽然带有JavaScript,但是指其语法规则是参考JavaScript对象的,而不是指只能用于JavaScript 语言。

相比 XML(另一种常见的数据交换格式),文件更小

//JSON
{
	"name":"参宿",                    //"key":value,value可以str、num、bool、null、obj,arr。
	"id":7,                    //并列的数据之间用逗号(“,”)分隔
	"fruits":["apple","pear","grape"]   //数组用[],对象用{}
}

//XML
<root>
	<name>"参宿"</name>
	<id>7</id>
	<fruits>apple</fruits>
	<fruits>pear</fruits>
	<fruits>grape</fruits>
</root>

JSON解析和生成

var str = '{"name": "参宿","id":7}';      //'JSON字符串'
var obj = JSON.parse(str);                //JSON.parse(str)
console.log(obj);                       //JSON的解析(JSON字符串转换为JS对象)

//Object { name: "参宿", id: 7 }
var jsonstr = JSON.stringify(obj);        //JSON.stringify(obj)
console.log(jsonstr);                     //JSON的生成(JS对象转换为JSON字符串)

JSON.parse(text[, reviver])//reviver函数参数,修改解析生成的原始值,调用时机在 parse 函数返回之前。
//k:key,v:value
JSON.parse('{"p": 5}', function (k, v) {
    if(k === '') return v;     // 如果到了最顶层,则直接返回属性值,
    return v * 2;              // 否则将属性值变为原来的 2 倍。
});                            // { p: 10 }

//从最最里层的属性开始,一级级往外,最终到达顶层,也就是解析值本身
JSON.parse('{"1": 1, "2": 2,"3": {"4": 4, "5": {"6": 6}}}', function (k, v) {
    console.log(k); // 输出当前的属性名,从而得知遍历顺序是从内向外的,
                    // 最后一个属性名会是个空字符串。
    return v;       // 返回原始属性值,相当于没有传递 reviver 参数。
});

// 1
// 2
// 4
// 6
// 5
// 3
// ""

异步⭐⭐⭐

  • 异步是通过一次次的循环事件队列来实现的.例如 setTimeout setInterval xmlHttprequest 等
  • 同步阻塞式的IO,在高并发环境会是一个很大的性能问题,

所以同步一般只在基础框架的启动时使用,用来加载配置文件,初始化程序什么的.

node单线程的,网络请求,浏览器事件等操作都需要使用异步的方法。

webworker(创建分线程)⭐

当在 HTML 页面中执行脚本时,页面的状态是不可响应的,直到脚本已完成。

web worker 是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能

index.js为加载到html页面中的主线程(js文件)

work.js为在index中创建的分线程

index.js:
创建分线程  
var w =new webwork('work.js')//创建 Web Worker 对象
向子线程发送数据
w.postmesage('数据') 

work.js:
onmessage = function(ev) {
     console.log(ev);//接受主线程发送过来的ev.data数据
     this.postMessage('数据')//通过 postmesage('数据') 向主线程发送数据
}

通过w.onmessage=function(ev){ev.data} ev.data 接受  a 的值.
w.terminate();//终止 Web Worker

AJAX

  • ajax 全名 async javascript and XML(异步JavaScript和XML)

前后端交互的重要工具

无需重新加载整个网页的情况下,能够更新部分网页

​​

原生AJAX创建⭐

//1.创建xhr 核心对象
var xhr=new XMLHttpRequest();
​
//2.调用open 准备
xhrReq.open(method, url, [async, user, password]);//请求方式,请求地址,true异步,false 同步
xhr.open('post','http://www.baidu.com/api/search',true)

//3.如果是post请求,必须设置请求头。
//xhr.setRequestHeader('Content-Type', 'application/json')
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
​
//4.调用send 发送请求 (如果不需要参数,就写null)
xhr.send('user=tom&age=10&sex=女')
​
//5.监听异步回调 onreadystatechange
xhr.onreadystatechange=function(){
    if(xhr.readyState==4){                    //表示请求完成
        if(xhr.status==200){                 //状态码 为 200 表示接口请求成功
            console.log(xhr.responseText);   //responeseText 为相应数据。字符串类型。
            var res=JSON.parse(xhr.responseText);
            console.log(res);

            if(res.code==1){
            modal.modal('hide');
           location.reload();
       }
    }

*jQuery ajax

 $(function(){
            // jQuery里使用ajax
            $.ajax({
            url:"/js/data.json",
            type:'GET',
            dataType:'json',
            data:{'aa':1},
            })
            //设置请求成功后的回调函数
            .done(function(dat){
            console.log(dat);
         })
         //设置请求失败后的回调函数和
         .fail(function(dat){
            console.log('no!');
         })
        })

jQuery是一个 JavaScript 工具库

Axios⭐⭐

axios 是一个基于Promise 的ajax的封装,用于浏览器和 nodejs HTTP 客户端

特征:

  • 从浏览器中创建 XMLHttpRequest
  • 从 node.js 发出 http 请求
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 自动转换JSON数据
  • 客户端支持防止 CSRF
import axios from "axios";

axios.get('/users')
  .then(res => {
    console.log(res.data);
  });

// 向给定ID的用户发起请求
axios.get('/user?ID=12345')
  .then(function (response) {
    // 处理成功情况
    console.log(response);
  })
  .catch(function (error) {
    // 处理错误情况
    console.log(error);
  })
  .then(function () {
    // 总是会执行
  });

axios.get('/user?ID=12345')===
axios.get('/user', {
    params: {
      ID: 12345
    }
  })


axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

Axios API

1.axios(config)
// 发送 POST 请求
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
});
 
2.axios(url[, config])
// 发送 GET 请求(默认的方法)
axios('/user/12345');

// 在 node.js 用GET请求获取远程图片
axios({
  method: 'get',
  url: 'http://bit.ly/2mTM3nY',
  responseType: 'stream'
})
  .then(function (response) {
    response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
  });

axios.get(`http://localhost:3000/search/users?q=${keyWord}`)

get('') (自动默认) 和get(``)(反单引号)均可

axios基本用法_面条请不要欺负汉堡的博客-CSDN博客_axios

setTimeout()(倒)计时(手写)⭐

setInterval() 方法重复调用一个函数或执行一个代码段,在每次调用之间具有固定的时间延迟。

​//返回值timeoutID是一个正整数,表示定时器的编号。
let timeoutID = scope.setTimeout(function[, delay]),//delay表示最小等待时间,真正等待时间取决于前面排队的消息
clearTimeout(timeoutID) //取消该定时器。

<body>
<button onclick="startCount()">开始计数!</button> <input type="text" id="txt"> 
<button onclick="stopCount()">停止计数!</button>
	</body>
	<script type="text/javascript">
		var c = 0;
		var t;
		var timer = 0;
 
		function timedCount() {
			document.getElementById("txt").value = c;
			c = c + 1;
			t = setTimeout(function() {
				timedCount()
			}, 1000);
		}
 
		function startCount() {
			if (!timer) {
				timer = 1;
				timedCount();
			}
		}
 
		function stopCount() {
			clearTimeout(t);
			timer = 0;
		}
	</script>
 

<body>
<button onclick="startCount()">开始倒计时</button> <input type="text" id="txt"> 
</body>
	<script type="text/javascript">
		var c = 0;
		var t;
 
		function timedCount() {
			c -=1;
			document.getElementById("txt").value=c;
			
			if(c===0){
				clearTimeout(t);
				return;
			}
			t = setTimeout(function() {
				timedCount()
			}, 1000);
		}
 
		function startCount() {
			    c=document.getElementById("txt").value;
				timedCount();
		}

</script>

setTimeout()和setInterval()⭐

  • 同一个对象上(一个window或者worker),setTimeout()或者setInterval()在后续的调用不会重用同一个定时器编号
  • 如果省略delay, 取默认值 0,意味着“马上”执行,或者尽快执行。
  • 不同的对象使用独立的编号池
  • 浏览器会控制最小的超时时间固定在4ms到15ms之间。

  • setTimeout :延时delay毫秒之后,直接将回调函数加入事件队列

  • setInterval :延时delay毫秒之后,先看看事件队列中是否存在还没有执行setInterval的回调函数,如果存在,就不要再往事件队列里加入回调函数了。

  • setTimeout 保证调用的时间间隔是一致的,

  • setInterval的设定的间隔时间包括了执行回调的时间。

  • setInterval的回调函数不报错

Promise(ES6 解决地狱回调)⭐⭐⭐

回调函数
当一个函数作为参数传入另一个函数中,并且它不会立即执行,只有当满足一定条件后该函数才可以执行,这种函数就称为回调函数,例如setTimeout

回调地狱:回调函数套回调函数,回调的多重嵌套,会导致代码可读低、编写费劲、容易出错,故而被称为 callback hell

Promise:ES6异步操作的一种解决方案

Promise 构造函数接受一个函数作为参数,该函数是同步的并且会被立即执行,称为起始函数

起始函数包含两个函数参数 resolvereject,分别表示 Promise 成功和失败的状态。

起始函数执行成功时,它应该调用 resolve 函数并传递成功的结果。

起始函数执行失败时,它应该调用 reject 函数并传递失败的原因。

promise 有三个状态:

pending[待定]初始状态,fulfilled[实现]操作成功,rejected[被否决]操作失败

如果不使用 resove 和 reject 两个函数 状态为pendding

Promise 构造函数返回一个 Promise 对象,该对象具有以下几个方法:

  • then:用于处理 Promise 成功状态的回调函数,参数为resolve传递的参数。
  • catch:用于处理 Promise 失败状态的回调函数,参数为reject传递的参数。
  • finally:无论 Promise 是成功还是失败,都会执行的回调函数。
const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    if (Math.random() < 0.5) {
      resolve('success');
    } else {
      reject('error');
    }
  }, 1000);
});
 
promise.then(result => {
  console.log(result);
}).catch(error => {
  console.log(error);
});
new Promise(function (resolve, reject) {
    var a = 0;
    var b = 1;
    if (b == 0) reject("Divide zero");
    else resolve(a / b);
}).then(function (value) {
    console.log("a / b = " + value);
}).catch(function (err) {
    console.log(err);
}).finally(function () {
    console.log("End");
});

promise调用then,

如果非promise,会将值包装成promise

new Promise(function (resolve, reject) {
    setTimeout(function () {
        console.log("First");
        resolve();
    }, 1000);
}).then(function () {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log("Second");
            resolve();
        }, 4000);
    });
}).then(function () {
    setTimeout(function () {
        console.log("Third");
    }, 3000);
});
new Promise(function (resolve, reject) {
    console.log(1111);
    resolve(2222);
}).then(function (value) {
    console.log(value);
    return 3333;
}).then(function (value) {
    console.log(value);
    throw "An error";
}).catch(function (err) {
    console.log(err);
});
// then方法
then(resolveFn, rejectFn) {
  //return一个新的promise
  return new MyPromise((resolve, reject) => {
    //把resolveFn重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论
    const fulfilledFn = value => {
      try {
        //执行第一个(当前的)Promise的成功回调,并获取返回值
        let x = resolveFn(value)
        //分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
        //这里resolve之后,就能被下一个.then()的回调获取到返回值,从而实现链式调用
        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
      } catch (error) {
        reject(error)
      }
    }
    //把后续then收集的依赖都push进当前Promise的成功回调队列中(_resolveQueue), 这是为了保证顺序调用
    this._resolveQueue.push(fulfilledFn)

    //reject同理
    const rejectedFn  = error => {
      try {
        let x = rejectFn(error)
        x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
      } catch (error) {
        reject(error)
      }
    }
    this._rejectQueue.push(rejectedFn)
  })
}
  • 值穿透:根据规范,如果 then() 接收的参数不是function,那么我们应该忽略它。

如果没有忽略,当then()回调不为function时将会抛出异常,导致链式调用中断

  • 处理状态为resolve/reject的情况:其实我们上边 then() 的写法是对应状态为pending的情况,但是有些时候,resolve/reject 在 then() 之前就被执行(比如Promise.resolve().then()),如果这个时候还把then()回调push进resolve/reject的执行队列里,那么回调将不会被执行,因此对于状态已经变为fulfilledrejected的情况,我们直接执行then回调
// then方法,接收一个成功的回调和一个失败的回调
  then(resolveFn, rejectFn) {
    // 根据规范,如果then的参数不是function,则我们需要忽略它, 让链式调用继续往下执行
    typeof resolveFn !== 'function' ? resolveFn = value => value : null
    typeof rejectFn !== 'function' ? rejectFn = reason => {
      throw new Error(reason instanceof Error? reason.message:reason);
    } : null
  
    // return一个新的promise
    return new MyPromise((resolve, reject) => {
      // 把resolveFn重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论
      const fulfilledFn = value => {
        try {
          // 执行第一个(当前的)Promise的成功回调,并获取返回值
          let x = resolveFn(value)
          // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
          reject(error)
        }
      }
  
      // reject同理
      const rejectedFn  = error => {
        try {
          let x = rejectFn(error)
          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
          reject(error)
        }
      }
  
      switch (this._status) {
        // 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行
        case PENDING:
          this._resolveQueue.push(fulfilledFn)
          this._rejectQueue.push(rejectedFn)
          break;
        // 当状态已经变为resolve/reject时,直接执行then回调
        case FULFILLED:
          fulfilledFn(this._value)    // this._value是上一个then回调return的值(见完整版代码)
          break;
        case REJECTED:
          rejectedFn(this._value)
          break;
      }
    })
  }

兼容同步任务

executor是异步任务:

Promise的执行顺序是new Promise -> then()收集回调 -> resolve/reject执行回调

executor是同步任务:new Promise -> resolve/reject执行回调 -> then()收集回调,resolve的执行跑到then之前去了,

为了兼容这种情况,给resolve/reject执行回调的操作包一个setTimeout,让它异步执行。

//Promise/A+规定的三种状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  // 构造方法接收一个回调
  constructor(executor) {
    this._status = PENDING     // Promise状态
    this._value = undefined    // 储存then回调return的值
    this._resolveQueue = []    // 成功队列, resolve时触发
    this._rejectQueue = []     // 失败队列, reject时触发

    // 由于resolve/reject是在executor内部被调用, 因此需要使用箭头函数固定this指向, 否则找不到this._resolveQueue
    let _resolve = (val) => {
      //把resolve执行回调的操作封装成一个函数,放进setTimeout里,以兼容executor是同步代码的情况
      const run = () => {
        if(this._status !== PENDING) return   // 对应规范中的"状态只能由pending到fulfilled或rejected"
        this._status = FULFILLED              // 变更状态
        this._value = val                     // 储存当前value

        // 这里之所以使用一个队列来储存回调,是为了实现规范要求的 "then 方法可以被同一个 promise 调用多次"
        // 如果使用一个变量而非队列来储存回调,那么即使多次p1.then()也只会执行一次回调
        while(this._resolveQueue.length) {    
          const callback = this._resolveQueue.shift()
          callback(val)
        }
      }
      setTimeout(run)
    }
    // 实现同resolve
    let _reject = (val) => {
      const run = () => {
        if(this._status !== PENDING) return   // 对应规范中的"状态只能由pending到fulfilled或rejected"
        this._status = REJECTED               // 变更状态
        this._value = val                     // 储存当前value
        while(this._rejectQueue.length) {
          const callback = this._rejectQueue.shift()
          callback(val)
        }
      }
      setTimeout(run)
    }
    // new Promise()时立即执行executor,并传入resolve和reject
    executor(_resolve, _reject)
  }

  // then方法,接收一个成功的回调和一个失败的回调
  then(resolveFn, rejectFn) {
    // 根据规范,如果then的参数不是function,则我们需要忽略它, 让链式调用继续往下执行
    typeof resolveFn !== 'function' ? resolveFn = value => value : null
    typeof rejectFn !== 'function' ? rejectFn = reason => {
      throw new Error(reason instanceof Error? reason.message:reason);
    } : null
  
    // return一个新的promise
    return new MyPromise((resolve, reject) => {
      // 把resolveFn重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论
      const fulfilledFn = value => {
        try {
          // 执行第一个(当前的)Promise的成功回调,并获取返回值
          let x = resolveFn(value)
          // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
          reject(error)
        }
      }
  
      // reject同理
      const rejectedFn  = error => {
        try {
          let x = rejectFn(error)
          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (error) {
          reject(error)
        }
      }
  
      switch (this._status) {
        // 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行
        case PENDING:
          this._resolveQueue.push(fulfilledFn)
          this._rejectQueue.push(rejectedFn)
          break;
        // 当状态已经变为resolve/reject时,直接执行then回调
        case FULFILLED:
          fulfilledFn(this._value)    // this._value是上一个then回调return的值(见完整版代码)
          break;
        case REJECTED:
          rejectedFn(this._value)
          break;
      }
    })
  }
}

Promise.prototype.catch()

catch()方法返回一个Promise,并且处理拒绝的情况。它的行为与调用Promise.prototype.then(undefined, onRejected) 相同。

//catch方法其实就是执行一下then的第二个回调
catch(rejectFn) {
  return this.then(undefined, rejectFn)
}

Promise.prototype.finally()

finally()方法返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。在finally之后,还可以继续then。并且会将值原封不动的传递给后面的then

//finally方法
finally(callback) {
  return this.then(
    value => MyPromise.resolve(callback()).then(() => value),             // MyPromise.resolve执行回调,并在then中return结果传递给后面的Promise
    reason => MyPromise.resolve(callback()).then(() => { throw reason })  // reject同理
  )
}
复制代码

MyPromise.resolve(callback())的意义:这个写法其实涉及到一个finally()的使用细节,finally()如果return了一个reject状态的Promise,将会改变当前Promise的状态,这个MyPromise.resolve就用于改变Promise状态,在finally()没有返回reject态Promise或throw错误的情况下,去掉MyPromise.resolve也是一样的

Promise.resolve()

Promise.resolve(value)方法返回一个以给定值解析后的Promise 对象。如果该值为promise,返回这个promise;如果这个值是thenable(即带有"then" 方法)),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;否则返回的promise将以此值完成。此函数将类promise对象的多层嵌套展平。

//静态的resolve方法
static resolve(value) {
  if(value instanceof MyPromise) return value // 根据规范, 如果参数是Promise实例, 直接return这个实例
  return new MyPromise(resolve => resolve(value))
}
复制代码

Promise.reject()

Promise.reject()方法返回一个带有拒绝原因的Promise对象。

//静态的reject方法
static reject(reason) {
  return new MyPromise((resolve, reject) => reject(reason))
}

Promise.all()

Promise.all(iterable)方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve)

如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果

//静态的all方法
static all(promiseArr) {
  let index = 0
  let result = []
  return new MyPromise((resolve, reject) => {
    promiseArr.forEach((p, i) => {
      //Promise.resolve(p)用于处理传入值不为Promise的情况
      MyPromise.resolve(p).then(
        val => {
          index++
          result[i] = val
          //所有then执行后, resolve结果
          if(index === promiseArr.length) {
            resolve(result)
          }
        },
        err => {
          //有一个Promise被reject时,MyPromise的状态变为reject
          reject(err)
        }
      )
    })
  })
}
复制代码

Promise.race()

Promise.race(iterable)方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

static race(promiseArr) {
  return new MyPromise((resolve, reject) => {
    //同时执行Promise,如果有一个Promise的状态发生改变,就变更新MyPromise的状态
    for (let p of promiseArr) {
      MyPromise.resolve(p).then(  //Promise.resolve(p)用于处理传入值不为Promise的情况
        value => {
          resolve(value)        //注意这个resolve是上边new MyPromise的
        },
        err => {
          reject(err)
        }
      )
    }
  })
}

Promise.all()哪怕一个请求失败了也能得到其余正确的请求结果的解决方案

await(串行):如果在一个async的方法中,有多个await操作的时候,程序会变成完全的串行操作,一个完事等另一个但是为了发挥node的异步优势,

当异步操作之间不存在结果的依赖关系时,可以使用promise.all来实现并行,all中的所有方法是一同执行的。

执行后的结果:

async函数中,如果有多个await关键字时,如果有一个await的状态变成了rejected,那么后面的操作都不会继续执行,promise也是同理

await的返回结果就是后面promise执行的结果,可能是resolves或者rejected的值使用场景循环遍历方便了代码需要同步的操作(文件读取,数据库操作等)
 

promise.all并行(同时)执行promise,当其中任何一个promise 出现错误的时候都会执行reject,导致其它正常返回的数据也无法使用

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');//setTimeout(function[, delay, arg1, arg2, ...]);
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
});
// Expected output: Array [3, 42, "foo"]

map的每一项都是promise,catch方法返回值会被promise.reslove()包裹,这样传进promise.all的数据都是resolved状态的。

let p1 = Promise.resolve(1)
let p2 = Promise.resolve(2)
let p3 = Promise.resolve(3)
let p4 = Promise.resolve(4)
let p5 = Promise.reject("error")
let arr = [p1,p2,p3,p4,p5];

let all = Promise.all(arr.map((promise)=>promise.catch((e)=>{console.log("错误信息"+e)})))
all.then(res=>{console.log(res)}).catch(err=>console.log(err));

Mypromise

//Promise/A+规范的三种状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  // 构造方法接收一个回调
  constructor(executor) {
    this._status = PENDING     // Promise状态
    this._resolveQueue = []    // 成功队列, resolve时触发
    this._rejectQueue = []     // 失败队列, reject时触发

    // 由于resolve/reject是在executor内部被调用, 因此需要使用箭头函数固定this指向, 否则找不到this._resolveQueue
    let _resolve = (val) => {
      if(this._status !== PENDING) return   // 对应规范中的"状态只能由pending到fulfilled或rejected"
      this._status = FULFILLED              // 变更状态

      // 这里之所以使用一个队列来储存回调,是为了实现规范要求的 "then 方法可以被同一个 promise 调用多次"
      // 如果使用一个变量而非队列来储存回调,那么即使多次p1.then()也只会执行一次回调
      while(this._resolveQueue.length) {    
        const callback = this._resolveQueue.shift()
        callback(val)
      }
    }
    // 实现同resolve
    let _reject = (val) => {
      if(this._status !== PENDING) return   // 对应规范中的"状态只能由pending到fulfilled或rejected"
      this._status = REJECTED               // 变更状态
      while(this._rejectQueue.length) {
        const callback = this._rejectQueue.shift()
        callback(val)
      }
    }
    // new Promise()时立即执行executor,并传入resolve和reject
    executor(_resolve, _reject)
  }

  // then方法,接收一个成功的回调和一个失败的回调
  then(resolveFn, rejectFn) {
    this._resolveQueue.push(resolveFn)
    this._rejectQueue.push(rejectFn)
  }
}

【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理) - 掘金

fetch(ES6 拉取网络资源)

以前发送请求,使用ajax或者axios,现在还可以使用fetch。这个是window自带的方法,和xhr是一个级别的。(xhr=new XMLHttpRequest())

fetch(input[, init]);

// url (必须), options (可选)
fetch('/some/url', {method: 'get'})

.then(function(response) {

})

.catch(function(err) {
    // 出错了;等价于 then 的第二个参数,但这样更好用更直观 :(

});

第二个then接收的才是后台传过来的真正的数据

window.fetch(url, { method: 'get'})   //fetch() 返回响应的promise 
    // 第一个then  设置请求的格式
        .then(res => return res.json())  // 第一层then返回的是:响应的报文对象  
        .then((data) => {             //第二层:如果想要使用第一层的数据,必须把数据序列化  
                                       res.json() 返回的对象是一个 Promise对象再进行then()
         <!-- data为真正数据 -->
    }).catch(e => console.log("Oops, error", e))

Fetch()方法介绍_小钢炮vv的博客-CSDN博客_fetch

*Generator

ES6 引入Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。yield表达式就是暂停标志,把函数的执行流挂起,通过next()方法可以切换到下一个状态,为改变执行流程提供了可能,从而为异步编程提供解决方案。

function* myGenerator() {
  yield '1'
  yield '2'
  return '3'
}

const gen = myGenerator();  // 获取迭代器
gen.next()  //{value: "1", done: false}
gen.next()  //{value: "2", done: false}
gen.next()  //{value: "3", done: true}

  • 形式上,Generator是一个普通函数。

    区别一是function命令和函数名之间有一个星号*

    区别二是函数体内部使用yield定义不同的状态

  • 调用后函数并不执行,返回的是一个指向内部状态的指针对象Iterator。

function* gen(x){
  console.log('x='+x)
  var y = yield x + 2;

  return y;
}
//调用Generator 函数
var g = gen(1);
 
g.next();
// x=1
// {value: 3, done: false}

async/await函数

异步函数实际上原理与 Promise 原生 API 的机制是一模一样的,只不过更便于阅读。

相当于promise用法的语法糖,async/await实际上是对Generator(生成器)的封装

ES6 引入了 Generator 函数,被ES7 提出的async/await取代了,

将Generator函数的星号 * 替换成async,将yield替换成await。

相对于Generator函数的改进:自带执行器,会自动执行。

await规定了异步操作只能一个一个排队执行,从而达到用同步方式,执行异步操作的效果

Promise.resolve(a)
  .then(b => {
    // do something
  })
  .then(c => {
    // do something
  })

//等价于
async () => {
  const a = await Promise.resolve(a);
  const b = await Promise.resolve(b);
  const c = await Promise.resolve(c);
}

async关键字+函数,表明该函数内部有异步操作

函数返回的是一个状态为fulfilledPromise对象

如果结果是,会经过Promise包装返回。
如果是promise则会等待promaise 返回结果,否则,就直接返回对应的值,

await 操作符+promise对象用于组成表达式

awai+,就会转到一个立即resolve的Promise对象。

async function asyncFunc() {
    let value = await new Promise(
        function (resolve, reject) {
            resolve("Return value");
        }
    );
    console.log(value);
}
asyncFunc();

await只能在async函数中出现, 普通函数直接使用会报语法错误SyntaxError

await语句后的Promise对象变成reject状态时,那么整个async函数会中断,后面的程序不会继续执行

处理异常的机制将用 try-catch 块实现

async function asyncFunc() {
    try {
        await new Promise(function (resolve, reject) {
            throw "Some error"; // 或者 reject("Some error")
        });
    } catch (err) {
        console.log(err);
        // 会输出 Some error
    }
}
asyncFunc();

async和await的讲解_傲娇的koala的博客-CSDN博客_async await

SPA和MPA

SEO是由英文Search Engine Optimization缩写而来, 中文意译为“搜索引擎优化”。

SEO是指通过对网站进行站内优化和修复(网站Web结构调整、网站内容建设、网站代码优化和编码等)和站外优化,从而提高网站的网站关键词排名以及公司产品的曝光度。

  • 爬虫在爬取的过程中,不会去执行js,所以隐藏在js中的跳转也不会获取到

单页Web应用(single page web application,SPA)。

整个应用只有一个完整的页面。

点击页面中的链接不会刷新页面,只会做页面的局部更新

数据都需要通过ajax请求获取,并在前端异步展现

SPA和MPA的区别_糖果的小九的博客-CSDN博客

URL

字符串说明
://协议符号
/分隔目录和子目录
测试代表需要编译处理了的路径
分隔实际的URL和参数
&URL中指定的参数间的分隔符
=左边为参数名、右边参数值
搜&索搜索词含有中文,含有保留字段,需要编译

#符号的url就是一个 Fragment URL。#指定了网页中的一个位置

浏览器就会查询网页中name属性值匹配print<a>标签。即:<a name="print"></a> ,
或者是 id 属性匹配 print 的 <a> 标签。即<a id="print"></a>
匹配后,浏览器会将该部分滚动到可视区域的顶部。

#仅作用于浏览器,它不会影响服务器端。所以http请求中不会包括#
 

URL和URI

URL (Uniform Resource Locator ),统一资源定位符,对可以从互联网上得到的资源的位置访问方法的一种简洁的表示,是互联网上标准资源唯一的地址

URI(Uniform Resource Identifier),统一资源标识符,结构如下

 foo://example.com:8042/over/there?name=ferret#nose

   \_/ \______________/ \________/\_________/ \__/

    |         |              |         |        |

  scheme     authority      path      query   fragment

URL 是 URI 的一个子集, URL 同时说明访问方式

跨域通信⭐⭐⭐

所谓同源(域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)

同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能

如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。(跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。)

同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。

服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。)

服务器与服务器之间是可以通信的不受同源策略的影响:Nginx反向代理,proxy代理

同源策略限制内容有:

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AJAX 请求发送后,结果被浏览器拦截了

允许跨域加载资源的标签:

  • script标签的跨域功能:<img src=XXX><script src=XXX>
  • 规定外部脚本文件的 URL:<link href=XXX>

跨域的解决方案思路两种,绕过去和cors;

  • iframe方式可传递数据,但组织和控制代码逻辑太复杂,鸡肋;
  • jsonp适合加载不同域名的js、css,img静态资源,现在浏览器兼容性高了,以及受限于仅get方式,逐步淘汰了;
  • Nginx反向代理和nodejs中间件跨域原理都相似,是绕过去的方式,是从古至今通用的没完解决方案,适合前后端分离的前端项目调后端接口。都是搭建一个服务器,直接在服务器端请求HTTP接口,缺点也许是服务器压力大一点,实际中那点压力根本不是大问题;同时反向代理更适合内部应用间访问和共享;
  • cors才是真正的称得上跨域请求解决方案(支持所有类型的HTTP请求,但浏览器IE10以下不支持)适合做ajax各种跨域请求;
  • websocket都是HTML5新特性兼容性不是很好,只适用于主流浏览器和IE10+。

JSONP跨域手写)

服务器与客户端跨源通信常用方法。

JSONP(JSON With Padding)是利用<script src=XXX>跨域

因为是动态创建script标签,所以它只支持get请求不支持post请求

  • 优点
  1. 简单适用,
  2. 兼容低版本IE,可以向不支持CORS的网站请求数据( IE<=9, Opera<12, or Firefox<3.5 )
  • 不足
  1. 只支持get请求,
  2. 只支持跨域 HTTP 请求不安全,可能遇到XSS攻击
  3. 不能解决不同域的两个页面之间如何进行 Javascript 调用

原生实现

<script>
    var script = document.createElement('script');
    script.type = 'text/javascript';

    // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
    document.head.appendChild(script);

    // 回调执行函数
    function handleCallback(res) {
        alert(JSON.stringify(res));
    }
 </script>
  • src=跨域的API数据接口地址+向服务器传递该函数名(?问号传参

?user=admin&callback=handleCallback)

  • 客户端声明一个回调函数,其函数名当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。
  • 服务器接收到请求后,查找数据库,把返回的data和传递进来的函数名拼接成一个字符串,
  • 例如:传递进去的函数名是handleCallback,它准备好的数据是handleCallback('res')
  • 服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数handleCallback,对返回的数据进行操作。

封装JSONP

在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时需封装一个 JSONP函数。

// index.html
function jsonp({ url, params, callback }) {
  return new Promise((resolve, reject) => {

    let script = document.createElement('script')
    window[callback] = function(data) {
      resolve(data)
      document.body.removeChild(script)
    }
    //抽出参数,新建对象
    params = { ...params, callback } // wd=b&callback=show
    let arrs = []
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`)
    }
    script.src = `${url}?${arrs.join('&')}`//拼接
    document.body.appendChild(script)
  })
}
jsonp({
  url: 'http://localhost:3000/say',
  params: { wd: 'Iloveyou' },
  callback: 'show'
}).then(data => {
  console.log(data)
})

http://localhost:3000/say?wd=Iloveyou&callback=show这个地址请求数据,然后后台返回show('我不爱你'),最后会运行show()这个函数,打印出'我不爱你'

// server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
  let { wd, callback } = req.query
  console.log(wd) // Iloveyou
  console.log(callback) // show
  res.end(`${callback}('我不爱你')`)
})
app.listen(3000)

jQuery实现

JSONP都是GET和异步请求的,不存在其他的请求方式和同步请求,

且jQuery默认就会给JSONP的请求清除缓存

$.ajax({
url:"http://crossdomain.com/jsonServerResponse",
dataType:"jsonp",
type:"get",//可以省略
jsonpCallback:"show",//->自定义传递给服务器的函数名,而不是使用jQuery自动生成的,可省略
jsonp:"callback",//->把传递函数名的那个形参callback,可省略
success:function (data){
console.log(data);}
});

跨域资源共享(CORS)手写)

Cross-Origin Resource Sharing W3C 标准 CORS(主流的解决方案,推荐使用)

允许浏览器向跨源服务器发送XMLHttpRequest请求,从而克服 AJAX 只能同源使用的限制

属于跨源 AJAX 请求根本解决方法,最常用的一种解决办法

目前,所有浏览器都支持该功能,IE浏览器不能低于IE10

CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现

服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

CORS分为简单请求,非简单/复杂请求

简单请求

只要同时满足以下两大条件,就属于简单请求

条件1:使用下列方法之一:

  • GET
  • HEAD
  • POST

条件2:Content-Type 的值仅限于下列三者之一:

  • text/xxx
  • multipart/form-data(键值对型数据)
  • application/x-www-form-urlencoded(URL encoded)(默认

application/xml 、 text/xml、text/html、text/plain的区别

1、text/html是html格式的正文 

2、text/plain是无格式正文(可以有效避免XSS漏洞)

3、text/xml忽略xml头所指定编码格式而默认采用us-ascii编码

4、application/xml会根据xml头指定的编码格式来编码:

请求中的任意 XMLHttpRequestUpload 对象没有注册任何事件监听器

XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问

GET /cors? HTTP/1.1
Host: localhost:2333
Connection: keep-alive
Origin: http://localhost:2332
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Accept: */*
Referer: http://localhost:2332/CORS.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
If-None-Match: W/"1-NWoZK3kTsExUV00Ywo1G5jlUKKs"

普通跨域请求

只需服务器端设置Access-Control-Allow-Origin(表示接受那些域名的请求(*为所有)

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

带cookie跨域请求手写)

前后端都需要进行设置

 Access-Control-Allow-Credentials值为true时,Access-Control-Allow-Origin必须有明确的值,不能是通配符(*)

withCredentials 表示跨域请求是否提供凭据信息(cookie、HTTP认证及客户端SSL证明等)

cors中间件

cors是Express的一个第三方中间件
使用的步骤分为如下三步:
1.运行 npm install cors 安装中间件
2.使用const cores = require(‘cors’) 导入中间件
3.在路由之前调用app.use(cors())配置中间件

复杂请求

在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

PUT向后台请求时,后台需做如下配置:

// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS请求不做任何处理
if (req.method === 'OPTIONS') {
  res.end() 
}
// 定义后台返回的内容
app.put('/getData', function(req, res) {
  console.log(req.headers)
  res.end('我不爱你')
})

完整复杂请求的例子,并且介绍下CORS请求相关的字段:

// index.html
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiamen' // cookie不能跨域
xhr.withCredentials = true // 前端设置是否带cookie
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'xiamen')
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
      console.log(xhr.response)
      //得到响应头,后台需设置Access-Control-Expose-Headers
      console.log(xhr.getResponseHeader('name'))
    }
  }
}
xhr.send()
//server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
//server2.js
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000'] //设置白名单
app.use(function(req, res, next) {
  let origin = req.headers.origin
  if (whitList.includes(origin)) {
    // 设置哪个源可以访问我
    res.setHeader('Access-Control-Allow-Origin', origin)
    // 允许携带哪个头访问我
    res.setHeader('Access-Control-Allow-Headers', 'name')
    // 允许哪个方法访问我
    res.setHeader('Access-Control-Allow-Methods', 'PUT')
    // 允许携带cookie
    res.setHeader('Access-Control-Allow-Credentials', true)
    // 预检的存活时间
    res.setHeader('Access-Control-Max-Age', 6)
    // 允许返回的头
    res.setHeader('Access-Control-Expose-Headers', 'name')
    if (req.method === 'OPTIONS') {
      res.end() // OPTIONS请求不做任何处理
    }
  }
  next()
})
app.put('/getData', function(req, res) {
  console.log(req.headers)
  res.setHeader('name', 'jw') //返回一个响应头,后台需设置
  res.end('我不爱你')
})
app.get('/getData', function(req, res) {
  console.log(req.headers)
  res.end('我不爱你')
})
app.use(express.static(__dirname))
app.listen(4000)

上述代码由http://localhost:3000/index.htmlhttp://localhost:4000/跨域请求,后端是实现 CORS 通信的关键

postMessage

HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,

可以实现跨文本档、多窗口、跨域消息传递。

otherWindow.postMessage(message, targetOrigin, [transfer]);

  • message: 将要发送到其他 window的数据。
  • targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。
  • transfer(可选):是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

http://localhost:3000/a.html页面向http://localhost:4000/b.html传递“我爱你”,然后后者传回"我不爱你"。

// a.html
//内联框架元素 (<iframe>) 表示嵌套的browsing context。将另一个 HTML 页面嵌入到当前页面中。
  <iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件
  //内嵌在http://localhost:3000/a.html
    <script>
      function load() {
        let frame = document.getElementById('frame')
        frame.contentWindow.postMessage('我爱你', 'http://localhost:4000') //发送数据
        window.onmessage = function(e) { //接受返回数据
          console.log(e.data) //我不爱你
        }
      }
    </script>
// b.html
  window.onmessage = function(e) {
    console.log(e.data) //我爱你
    e.source.postMessage('我不爱你', e.origin)
 }

代理服务器

前端配置一个代理服务器代替浏览器去发送请求:

因为服务器与服务器之间是可以通信的不受同源策略的影响。

proxy代理服务器(ES6)

target表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理

handler通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时的代理行为

const p = new Proxy(target, handler)

应用实例

公司会用proxy代理脚本设置,控制HTTP流量的访问,增强网络安全性。

Reflect(ES6)

ES6中操作对象而提供的新 API,若需要在Proxy内部调用对象的默认行为,建议使用Reflect

  • 只要Proxy对象具有的代理方法,Reflect对象全部具有,以静态方法的形式存在
  • 修改某些Object方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回false
  • Object操作都变成函数行为

handler拦截属性

target,propKey,value,receiver:目标对象、属性名、属性值 proxy 实例本身

以下为关键属性

get(target,propKey,receiver)

拦截对象属性的读取

let person = {
  name: "Guest"
};
let proxy = new Proxy(person, {
  get: function(target, propKey, receiver) {
    return Reflect.get(target, propKey, receiver)
    // or
    // return target[propKey]
  }
});

proxy.name // "Guest"

get能够对数组增删改查进行拦截,将读取数组负数的索引

function createArray(...elements) {
  //handler对象
  let handler = {
    //get函数属性
    get(target, propKey, receiver) {

      let index = Number(propKey);
    //实现循环索引
      if (index < 0) {
        propKey = String(target.length + index);
      }

      return Reflect.get(target, propKey, receiver);
    }
  };

  let target = [];
  target.push(...elements);
  return new Proxy(target, handler);
}

let arr = createArray('a', 'b', 'c');
arr[-1] // c

set(target,propKey,value,receiver)

拦截对象属性的设置

 现定义一个对象 规定 年龄输入整数时才被赋值,访问无效属性时控制台提醒

const obj = { name: "张三", age: 18 };

const proxy = new Proxy(obj, {

    get(target, prop) {
      if (prop in target) {
        return Reflect.get(target, prop);
      } else {
        console.error("字段不存在")
        return undefined;
      }
    },

    set(target, propKey, value, receiver) {

      if (propKey === "age") {
        if (typeof value === "number") {
          return Reflect.set(target, propKey, value, receiver);
          // or
          // target[propKey] = value 
          // return true
        } else {
          console.error("年龄只能输入正整数");
          return false;
        }

      } else {
        return false;
      }
    }
});

proxy.age = 20;  
console.log(proxy.age);  // 20
proxy.age = "22";
console.log(proxy.age);  // 20
console.log(proxy.test); // undefined

deleteProperty(target,propKey)

拦截delete proxy[propKey]的操作,返回一个布尔值

如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除

function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`无法删除私有属性`);
  }
}

var handler = {
  deleteProperty (target, key) {
    invariant(key, 'delete');
    Reflect.deleteProperty(target,key)
    return true;
  }
};


var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: 无法删除私有属性

取消代理Proxy.revocable(target, handler)

Proxy.revocable(target, handler);

应用

类似于设计模式中的代理模式,常用功能如下:

  • 拦截和监视外部对对象的访问
  • 降低函数或类的复杂度
  • 在复杂操作前对操作进行校验或对所需资源进行管理

使用 Proxy 保障数据类型的准确性

let data = { num: 0 };
data = new Proxy(data, {
    set(target, key, value, proxy) {
        if (typeof value !== 'number') {
            throw Error("属性只能是number类型");
        }
        return Reflect.set(target, key, value, proxy);
    }
});

data.num = "foo"
// Error: 属性只能是number类型
data.num = 1
// 赋值成功

声明一个私有的 apiKey,便于 api 这个对象内部的方法调用

let api = {
    _apiKey: 'kafakakafaka',
};
//私有属性名 常量数组
const RESTRICTED = ['_apiKey'];

api = new Proxy(api, {
    get(target, key, receiver) {
        if(RESTRICTED.indexOf(key) > -1) {
            throw Error(`${key} 不可访问.`);
        } 
        return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
        if(RESTRICTED.indexOf(key) > -1) {
            throw Error(`${key} 不可修改`);
        }
        return Reflect.set(target, key, value, receiver);
    }
});

console.log(api._apiKey)
api._apiKey = '987654321'
// 上述都抛出错误

观察者模式(Observer mode):函数自动观察数据对象,一旦对象有变化,函数就会自动执行

observable函数返回一个原始对象的 Proxy 代理,拦截赋值操作,触发充当观察者的各个函数

//观察者函数都放进Set集合
const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});

//当修改obj的值,在会set函数中拦截
function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  //自动执行Set所有的观察者
  queuedObservers.forEach(observer => observer());
  return result;
}

Nginx反向代理

nginx代理跨域,实质和CORS跨域原理一样,通过配置文件设置请求响应头Access-Control-Allow-Origin...等字段。

Nginx是高性能的 HTTP 和反向代理的web服务器

是最简单的跨域方式,只需要修改 nginx 的配置即可解决跨域问题

  1. node中间件和nginx反向代理,都是搭建一个中转 nginx 服务器,用于转发请求
  2. 请求发给代理服务器,静态页面代理服务器是同源的,
  3. 代理服务器再向后端服务器发请求,服务器和服务器之间不存在同源限制

正向代理和反向代理

正向代理是代理用户客户端,为客户端发送请求,对服务器隐藏真实客户端

反向代理以代理服务器来接收客户端的请求,然后将请求转发给内部网络上的服务器,将从服务器上得到的结果返回给客户端。

正向代理主要是用来解决访问限制问题;

反向代理则是提供负载均衡、安全防护等作用。

前端应用运行在 http://localhost:3000,允许来自该域的跨域请求访问您的后端 API,该 API 运行在 http://localhost:8000

server {
    #HTTP 协议来监听请求,应该使用 listen 80;
    listen 80;
    server_name localhost;

    location /api {
        proxy_pass http://localhost:8000;

        # 允许来自前端应用的跨域请求
        add_header 'Access-Control-Allow-Origin' 'http://localhost:3000';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';

        # 支持 OPTIONS 请求,用于预检请求
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'http://localhost:3000';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';

            # 响应预检请求,直接返回 204 No Content
            add_header 'Content-Length' 0;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            return 204;
        }
    }
}

websocket协议

HTML5 的一个持久化的协议,它实现了浏览器与服务器全双工通信

WebSocket HTTP 都是应用层协议,都基于 TCP 协议

WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了

原生WebSocket API使用起来不太方便

Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容

本地文件socket.html向localhost:3000发生数据和接受数据:

// socket.html
<script>
    let socket = new WebSocket('ws://localhost:3000');
    socket.onopen = function () {
      socket.send('我爱你');//向服务器发送数据
    }
    socket.onmessage = function (e) {
      console.log(e.data);//接收服务器返回的数据
    }
</script>
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//记得安装ws
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
  ws.on('message', function (data) {
    console.log(data);
    ws.send('我不爱你')
  });
})

web安全及防护

XSS攻击

跨站脚本攻击Cross-Site Scripting,代码注入攻击。

当被攻击者登陆网站时就会执行这些恶意代码,这些脚本可以读取 cookie,session tokens,或者其它敏感的网站信息,对用户进行钓鱼欺诈,甚至发起蠕虫攻击等。

  • 解决:

 url参数使用encodeURIComponent方法转义

尽量不用InnerHtml插入HTML内容

使用特殊符号、标签转义符。

CSRF攻击

跨站请求伪造Cross-site request forgery,在第三方网站中,向被攻击网站发送跨站请求。

利用用户在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

  • 解决:添加验证码、使用token

SQL注入攻击

SQL命令插入到Web表单递交或输入域名,最终达到欺骗服务器执行恶意的SQL命令。

解决:表单输入时通过正则表达式将一些特殊字符进行转换

DDoS攻击

分布式拒绝服务,全称 Distributed Denial of Service其原理就是利用大量的请求造成资源过载,导致服务不可用。

解决:

  1. 限制单IP请求频率。

  2. 防火墙等防护设置禁止ICMP包等

  3. 检查特权端口的开放

  • 0
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值