情景题
# 情景题
# 当在浏览器输入google.com
回车之后会发生什么本博客最重要的一个问题
在回车之前,会执行一次当前页面的
beforeunload
事件,可以让页面退出之前执行一些数据清理工作,或者,有表单没有提交的情况提示用户是否确认离开缓存判断 判断资源是否存在缓存中,若在缓存且没有失效,就直接使用,否则向服务器发起新请求
这里的缓存主要指的是
- 如果是
https
的话,有可能先找Service Worker
,比如你设置了请求拦截,离线缓存的话 - 如果没有,再找浏览器的内存缓存(
Memory Cache
) - 如果还没有,再找硬盘缓存(
Disk Cache
)( 强缓存和协商缓存都属于硬盘缓存) - 如果这三种都没有找到,请求还是
http2
的话,还可能会查找推送缓存(Push Cache
),就是找Session
(Session
会话结束就会释放,所以存在时间很短)
- 如果是
解析
URL
对URL
进行解析,分析所用的传输协议 域名 端口 资源路径,如果不合法,就传递给搜索引擎,如果没问题,浏览器检查URL
是否出现非法字符,若有则进行转义DNS
解析- 首先在浏览器缓存中查找对应的
IP
地址,如果查到就直接返回,查不到就下一步 - 将请求发给本地
dns
服务器,在本地域名服务器查询,如果查到直接返回,差不到就下一步 - 本地
dns
服务器向根域名服务器发送请求,根域名服务器返回一个所查询域的顶级服务器地址 - 本地
dns
向顶级域名服务器发送请求,接收请求的服务器查询自己的缓存,如果有记录就返回查询结果,没有就返回下一级的权威域名服务器地址 - 本地
dns
向权威域名服务器请求,域名服务器返回对应结果 - 本地
dns
将结果保存在缓存中,便于下次使用 - 将结果返回给浏览器
- 首先在浏览器缓存中查找对应的
- (用户向本地
dns
服务器发起请求是递归请求,本地dns
向各级域名服务器发起的请求是迭代请求)
服务器作用笔记
- 根域名服务器:离用户较近,当所要查询的主机也属于同一个本地
ISP
时,该本地域名服务器立即就能将所查询的主机名转换为它的IP
地址,而不需要再去询问其他的域名服务器。 - 根域名服务器:最高层次、最重要的服务器,所有的根域名服务器都知道所有的顶级域名服务器的域名和
IP
地址。不管是哪一个本地域名服务器,若要对因特网上任何一个域名进行解析,只要自己无法解析,就首先求助于根域名服务器 - 顶级域名服务器: 负责管理在该顶级域名服务器注册的所有二级域名。当收到
DNS
查询请求时,就给出相应的回答(可能是最后的结果,也可能是下一步应当找的域名服务器的IP
地址)。 - 权限域名服务器是负责一个区的域名服务器,用来保存该区中的所有主机的域名到
IP
地址的映射。当一个权限域名服务器还不能给出最后的查询回答时,就会告诉发出查询请求的DNS
客户,下一步应当找哪一个权限域名服务器。
值得注意的是
- 如果没有配置
CDN
,就直接返回解析到的IP
- 如果有配置
CDN
,权威域名会返回一个CName别名记录,它指向CDN
网络中的智能DNS
负载均衡系统,然后负载均衡系统通过智能算法,将最佳CDN
节点的IP
返回
获取
MAC
地址 当浏览器得到ip
地址时,数据传输还得知道mac
地址,数据是从应用层下发到传输层,tcp
会指定源端口号和目的端口号,然后下发给网络层,网络层将本地ip
作为源地址,获取的ip
作为目的ip
,然后下发给数据链路层,数据链路层要加入通信双方的mac
地址,本地mac
作为源mac
地址,目的mac
地址分情况处理,通过ip
地址与本地子网掩码相与,判断是否和请求主机处于同一个子网,如果在同一子网,用ARP
协议获得目的主机的mac
地址,如果不在,请求转发给网关,由他来转发,此时同样可以通过arp
协议获得网关的mac
地址,此时目的主机的mac
地址为网关的地址TCP
三次握手 下面是TCP
建立连接的三次握手的过程。
- 首先客户端向服务器发送一个
SYN
连接请求报文段和一个随机序号,进入SYN-SENT
状态 - 服务端接收到请求后向客户端发送一个
SYN ACK
报文段,确认连接请求,并且也向客户端发送一个随机序号,进入LISTEN
状态 - 客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个
ACK
确认报文段,进入ESTABLISH
状态 - 服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了,也进入
ESTABLISH
状态- 两次握手不行的简单原因:服务器就没有办法知道自己的序号是否 已被确认,防止失效的连接请求报文段突然又传送到主机
B
。
- 两次握手不行的简单原因:服务器就没有办法知道自己的序号是否 已被确认,防止失效的连接请求报文段突然又传送到主机
HTTPS
握手 通信还存在TLS
的四次挥手
首先客户端向服务端发送协议的版本号、一个随机数和可以使用的加密方法
服务器收到后确认加密方法,向客户端发送一个随机数和自己的数字证书
客户端收到后检查数字证书是否有效,有效的话再生成一个随机数,使用证书的公钥对随机数加密 然后发给服务端,并且会提供一个前面所有内容的
hash
值给服务器检验服务器接收后用私钥对数据解密,同时向客户端发送一个前面所有内容的
hash
值供客户端检验,双方这时有三个随机数,按之前的加密方法,使用这三个随机数生成一把密钥,以后双方传输数据时用这个密钥加密再传输
返回数据 当页面请求发到服务端,服务端会返回一个
html
文件作为响应,浏览器接到响应 对html
进行解析,开始页面渲染TCP
四次挥手 最后一步是TCP
断开连接的四次挥手过程。
- 若客户端认为数据发送完成,则它需要向服务端发送连接释放请求,进入
FIN_WAIT1
状态。 - 服务端收到连接释放请求后,会告诉应用层要释放
TCP
链接。然后会发送ACK
包,并进入CLOSE_WAIT
状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为TCP
连接是双向的,所以服务端仍旧可以发送数据给客户端, 此时服务端处于CLOSE_WAIT
,客户端收到确认报文后处于FIN_WAIT2
状态 - 服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入
LAST-ACK
状态 - 客户端收到释放请求后,向服务端发送确认应答,此时客户端进入
TIME-WAIT
状态。该状态会持续2MSL
(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入CLOSED
状态。 - 当服务端收到确认应答后,也便进入
CLOSED
状态。- 四次挥手简单原因:服务端还没发完消息
- 等待
2MSL
简单原因是:防止发送给服务器的确认报文段丢失或者出错,从而导致服务器 端不能正常关闭。
- 解析响应数据
如果返回的状态码是301
或302
就需要重定向
到其他URL
,在重定向地址会在响应头的Location
字段中,然后一切从头开始,否则然后根据情况选择关闭TCP
连接或者保留重用
然后网络线程会通过SafeBrowsing
来检查站点是不是恶意站点,如果是就展示警告页面,告诉你这个站点有安全问题,浏览器会阻止访问,当然也可以强行继续访问。
SafeBrowsing
是谷歌内部的一套站点安全系统,通过检查该站点的数据来判断是不是安全,比如通过查看该站点的IP
有没有在谷歌黑名单中,如果是Chrome
浏览器的话
响应成功返回状态码2xx
,然后判断资源能不能缓存,如果可以就先缓存
起来
然后对响应解码,比如gzip压缩
,然后根据资源类型(Content-Type
)决定如何处理,如果浏览器判断是下载文件,那么请求会被提交给浏览器的下载管理器
,同时URL
请求流程就结束了
否则网络线程会通知UI
线程,然后UI
线程会创建一个渲染器进程来准备渲染页面
然后浏览器进程通过IPC
管道将数据传给渲染器进程的主线程,准备渲染流程
默认情况下会为每一个标签页配置一个渲染进程,但是也有例外,比如从
A
页面里面打开一个新的页面B
页面,而A
页面和B
页面又属于同一站点的话,A
和B
就共用一个渲染进程,其他情况就为B
创建一个新的渲染进程
渲染进程收到确认消息后,会和网络进程建立传输数据的管道,开始执行解析数据、下载资源等
为什么进入新页面,之前的页面不会立马消失,而是要加载一会才会更新的原因
- 因为这时候的旧的文档还在网络进程中,渲染进程准备好了之后,渲染进程会向浏览器进程发出提交文档的消息
- 浏览器进程收到后会开始清理当前的旧页面的文档,然后发出确认消息给渲染进程,同时浏览器更新浏览器界面(安全状态、
URL
、前进后退历史状态)并更新页面(此时是空白页)
- 页面渲染
渲染进程将
HTML
内容转换为能够读懂DOM
树结构。渲染引擎将
CSS
样式表转化为浏览器可以理解的styleSheets
,计算出DOM
节点的样式。创建布局树,并计算元素的布局信息。
对布局树进行分层,并生成分层树。
为每个图层生成绘制列表,并将其提交到合成线程。合成线程将图层分图块,并栅格化将图块转换成位图。
合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上
笔记
其中的构建
dom
树的过程如下- 转码(
Bytes -> Characters
)—— 读取接收到的HTML
二进制数据,按指定编码格式将字节转换为HTML
字符串 Tokens
化(Characters -> Tokens
)—— 解析HTML
,将HTML
字符串转换为结构清晰的Tokens
,每个Token
都有特殊的含义同时有自己的一套规则- 构建
Nodes
(Tokens -> Nodes
)—— 每个Node
都添加特定的属性(或属性访问器)也叫词法分析:将token
转换为对象并定义属性和规则,通过指针能够确定Node
的父、子、兄弟关系和所属treeScope
(例如:iframe
的treeScope
与外层页面的treeScope
不同) - 构建
DOM
树(Nodes -> DOM Tree
)—— 最重要的工作是建立起每个结点的父子兄弟关系(解析器会维护一个解析栈,栈底为document
,也就是DOM
树的根节点,然后根据节点对象关系按顺序依次向解析栈添加,形成DOM树
)
DOM
树 和 渲染树 的区别:DOM
树与HTML
标签一一对应,包括head
和隐藏元素- 渲染树不包括
head
和隐藏元素,大段文本的每一个行都是独立节点,每一个节点都有对应的css
属性
样式计算
渲染引擎将
CSS
样式表转化为浏览器可以理解的styleSheets
,计算出DOM
节点的样式。值得注意的是
CSS
解析和DOM
解析是可以同时进行的,但是script
执行和CSS
解析不能同时进行,CSS
会阻塞JS
执行因为
JS
执行时可能在文档的解析过程中 获取样式信息,如果样式信息没有加载和解析完毕,JS
就会得到错误的值,所以会延迟JS
执行CSS
样式来源主要有3
种,分别是通过link
引用的外部CSS
文件、style
标签内的CSS
、元素的style
属性内嵌的CSS
。页面布局
布局过程,即
排除 script、meta 等功能化、非视觉节点
,排除display: none
的节点,计算元素的位置信息,确定元素的位置,构建一棵只包含可见元素布局树。如图生成分层树
页面中有很多复杂的效果,如一些复杂的
3D
变换、页面滚动,或者使用z-index
做z
轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree
)具体
拥有层叠上下文属性的元素,比如:
html
z-index
不为auto
position:fixed
opacity
小于1
transform
不为none
filter不为none
-webkit-overflow-scrolling:touch
有
裁剪
的地方,比如内容溢出裁剪不裁剪出现滚动条的话,
滚动条
也会被提升为单独的层
栅格化
合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图
通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(
viewport
)。在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。另外渲染引擎还维护了一个栅格化(光栅化)线程,合成线程将分割好的图块发送给
栅格化线程
,然后分别栅格化每个图块,再将栅格化之后的图块存储在GPU内存
中合成器线程能够对不同的栅格化线程做优先处理,所以出现在视口内的图块会被优先栅格化
- 转码(
- 合成和显示:当图块都被栅格化完成后,合成线程会收集栅格化线程的
draw quads
图块信息,该信息记录了图块在内存中的位置信息和图块在页面中的位置信息
根据这些信息,合成器线程生成一个合成器帧,然后通过IPC
传给浏览器进程
浏览器进程里有个叫 viz
的组件,用来接收这个合成器帧
然后浏览器进程再将合成器帧绘制到显存中,再通过 GPU
渲染在屏幕上,这时候终于看到了页面内容
当屏幕内容发生变化,比如滚动了页面,合成器线程就会将栅格化好的层合成一个新的合成器帧,新的帧再传到显存,GPU
再渲染到页面上
延伸
这里可以问到transform
不用重排,不会导致卡顿的原因
刚才的流程我们知道栅格化的整个流程是不占用主线程的,只在合成器线程和栅格线程中运行
这就意味着它不用JS
抢主线程,刚才提到反复重绘和重排会导致掉帧,是因为JS
阻塞了主线程,而通过CSS
中的动画属性transform
实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格化线程中,所以不会受到主线程JS
执行的影响
更重要的是通过transform
实现的动画由于不需要经过布局和绘制,样式计算等操作,所以节省了很多运算时间
补充 两种合成
# 显式合成
下面是显式合成
的情况:
一、 拥有层叠上下文的节点。
层叠上下文也基本上是有一些特定的CSS
属性创建的,一般有以下情况:
HTML
根元素本身就具有层叠上下文。- 普通元素设置
position
不为static
并且设置了z-index
属性,会产生层叠上下文。 - 元素的
opacity
值不是1
- 元素的
transform
值不是none
- 元素的
filter
值不是none
- 元素的
isolation
值是isolate
will-change
指定的属性值为上面任意一个。
二、需要剪裁的地方。
比如一个div
,你只给他设置 100 * 100
像素的大小,而你在里面放了非常多的文字,那么超出的文字部分就需要被剪裁。当然如果出现了滚动条,那么滚动条会被单独提升为一个图层。
# 隐式合成
接下来是隐式合成
,简单来说就是层叠等级低
的节点被提升为单独的图层之后,那么所有层叠等级比它高
的节点都会成为一个单独的图层。
这个隐式合成其实隐藏着巨大的风险,如果在一个大型应用中,当一个z-index
比较低的元素被提升为单独图层之后,层叠在它上面的的元素统统都会被提升为单独的图层,可能会增加上千个图层,大大增加内存的压力,甚至直接让页面崩溃。这就是层爆炸的原理。
值得注意的是,当需要repaint
时,只需要repaint
本身,而不会影响到其他的层。
JS
引擎解析过程:调用JS
引擎执行JS
代码(JS
的解释阶段,预处理阶段,执行阶段生成执行上下文,VO
,作用域链、回收机制等等)
- 创建
window
对象:window
对象也叫全局执行环境,当页面产生时就被创建,所有的全局变量和函数都属于window
的属性和方法,而DOM Tree
也会映射在window
的doucment
对象上。当关闭网页或者关闭浏览器时,全局执行环境会被销毁。 - 加载文件:完成
js
引擎分析它的语法与词法是否合法,如果合法进入预编译 - 预编译:在预编译的过程中,浏览器会寻找全局变量声明,把它作为
window
的属性加入到window
对象中,并给变量赋值为'undefined'
;寻找全局函数声明,把它作为window
的方法加入到window
对象中,并将函数体赋值给他(匿名函数是不参与预编译的,因为它是变量)。而变量提升作为不合理的地方在ES6
中已经解决了,函数提升还存在。 - 解释执行:执行到变量就赋值,如果变量没有被定义,也就没有被预编译直接赋值,在
ES5
非严格模式下这个变量会成为window
的一个属性,也就是成为全局变量。string
、int
这样的值就是直接把值放在变量的存储空间里,object
对象就是把指针指向变量的存储空间。函数执行,就将函数的环境推入一个环境的栈中,执行完成后再弹出,控制权交还给之前的环境。JS
作用域其实就是这样的执行流机制实现的。
补充:显示器显示图像的原理
无论是 PC
还是手机屏幕,都有一个固定的刷新频率,一般是 60 HZ
,即 60
帧,也就是一秒更新 60
张图片,一张图片停留的时间约为 16.7 ms
。
而每次更新的图片都来自显卡的前缓冲区。而显卡接收到浏览器进程传来的页面后,会合成相应的图像,并将图像保存到后缓冲区,然后系统自动将前缓冲区
和后缓冲区
对换位置,如此循环更新。
这也就是当某个动画大量占用内存的时候,浏览器生成图像的时候会变慢,图像传送给显卡就会不及时,而显示器还是以不变的频率刷新,因此会出现卡顿,也就是明显的掉帧现象
# 扫码登陆流程
问:假设扫码途中QRCode
刷新了怎么办?
可以在PC
端新增状态,已扫码待确认,来阻止轮询超时重新获取二维码
问:PC
怎么根据二维码进行响应
PC
端可以通过获取二维码的状态来进行相应的响应:
- 二维码
未扫描
:无操作 - 二维码
已失效
:提示刷新二维码 - 二维码
已成功
:从服务端获取PC token
问:可以用长轮询嘛 ws
呢
可以。长轮询是指客户端主动给服务端发送二维码状态的查询请求,服务端会按情况对请求进行阻塞,直至二维码信息更新或超时。当客户端接收到返回结果后,若二维码仍未被扫描,则会继续发送查询请求,直至状态变化(已失效或已成功)
Websocket
是指前端在生成二维码后,会与后端建立连接,一旦后端发现二维码状态变化,可直接通过建立的连接主动推送信息给前端。
# 为什么通常在发送数据埋点请求的时候使用的是 1x1
像素的透明 gif
图片重要
- 能够完成整个
HTTP
请求+响应(尽管不需要响应内容) - 触发
GET
请求之后不需要获取和处理数据、服务器也不需要发送数据 - 跨域友好
- 执行过程无阻塞
- 相比
XMLHttpRequest
对象发送GET
请求,性能上更好 GIF
的最低合法体积最小(最小的BMP
文件需要74
个字节,PNG
需要67
个字节,而合法的GIF
,只需要43
个字节)
为什么不能用请求其他文件资源(js/css/ttf
)的方式进行上报呢
创建资源节点后只有将对象注入到浏览器DOM
树后,浏览器才会实际发送资源请求。而且载入js/css
资源还会阻塞页面渲染,影响用户体验。
构造图片打点不仅不用插入DOM
,只要在js
中new
出Image
对象就能发起请求,而且还没有阻塞问题,在没有js
的浏览器环境中也能通过img
标签正常打点。
同样都是图片,上报时选用了1x1
的透明GIF
,而不是其他的PNG/JEPG/BMP
文件
首先,1x1
像素是最小的合法图片。而且,因为是通过图片打点,所以图片最好是透明的,这样一来不会影响页面本身展示效果,二者表示图片透明只要使用一个二进制位标记图片是透明色即可,不用存储色彩空间数据,可以节约体积。因为需要透明色,所以可以直接排除JEPG
。
同样的响应,GIF
可以比BMP
节约41%
的流量,比PNG
节约35%
的流量。GIF
才是最佳选择。
- 可以进行跨域
- 不会携带
cookie
- 不需要等待服务器返回数据
# 简单描述一下 Babel
的编译过程特别重要
概念
Babel
是一个 JavaScript
编译器,是一个工具链,主要用于将采用 ECMAScript 2015+
语法编写的代码转换为向后兼容的 JavaScript
语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
Babel
本质上就是在操作 AST
来完成代码的转译。AST
是抽象语法树(Abstract Syntax Tree, AST
)
Babel
的功能很纯粹,它只是一个编译器。大多数编译器的工作过程可以分为三部分:
- 解析(
Parse
) :将源代码转换成更加抽象的表示方法(例如抽象语法树)。包括词法分析和语法分析。词法分析主要把字符流源代码(Char Stream
)转换成令牌流(Token Stream
),语法分析主要是将令牌流转换成抽象语法树(Abstract Syntax Tree,AST)
。 - 转换(
Transform
) :通过Babel
的插件能力,对(抽象语法树)做一些特殊处理,将高版本语法的AST
转换成支持低版本语法的AST
。让它符合编译器的期望,当然在此过程中也可以对AST
的Node
节点进行优化操作,比如添加、更新以及移除节点等。 - 生成(
Generate
) :将AST
转换成字符串形式的低版本代码,同时也能创建Source Map
映射。
经过这三个阶段,代码就被 Babel
转译成功了。
# 列举一下有哪些捕获/监控错误的方式特别重要
try/catch
能捕获常规运行时错误,语法错误和异步错误不行
// 常规运行时错误,可以捕获 ✅
try {
console.log(notdefined);
} catch(e) {
console.log('捕获到异常:', e);
}
// 语法错误,不能捕获 ❌
try {
const notdefined,
} catch(e) {
console.log('捕获到异常:', e);
}
// 异步错误,不能捕获 ❌
try {
setTimeout(() => {
console.log(notdefined);
}, 0)
} catch(e) {
console.log('捕获到异常:',e);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
window.onerror
pure js
错误收集,window.onerror
,当JS
运行时错误发生时,window
会触发一个ErrorEvent
接口的error
事件。
/**
* @param {String} message 错误信息
* @param {String} source 出错文件
* @param {Number} lineno 行号
* @param {Number} colno 列号
* @param {Object} error Error对象
*/
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:', {message, source, lineno, colno, error});
}
2
3
4
5
6
7
8
9
10
11
先验证下几个错误是否可以捕获。
// 常规运行时错误,可以捕获 ✅
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
console.log(notdefined);
// 语法错误,不能捕获 ❌
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
const notdefined,
// 异步错误,可以捕获 ✅
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
setTimeout(() => {
console.log(notdefined);
}, 0)
// 资源错误,不能捕获 ❌
<script>
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
return true;
}
</script>
<img src="https://yun.tuia.cn/image/kkk.png">
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
window.addEventListener
当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个
Event
接口的error
事件,这些error
事件不会向上冒泡到window
,但能被捕获。而window.onerror
不能监测捕获。
// 图片、script、css加载错误,都能被捕获 ✅
<script>
window.addEventListener('error', (error) => {
console.log('捕获到异常:', error);
}, true)
</script>
<img src="https://yun.tuia.cn/image/kkk.png">
<script src="https://yun.tuia.cn/foundnull.js"></script>
<link href="https://yun.tuia.cn/foundnull.css" rel="stylesheet"/>
// new Image错误,不能捕获 ❌
<script>
window.addEventListener('error', (error) => {
console.log('捕获到异常:', error);
}, true)
</script>
<script>
new Image().src = 'https://yun.tuia.cn/image/lll.png'
</script>
// fetch错误,不能捕获 ❌
<script>
window.addEventListener('error', (error) => {
console.log('捕获到异常:', error);
}, true)
</script>
<script>
fetch('https://tuia.cn/test')
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
new Image
运用的比较少,可以单独自己处理自己的错误。
但通用的fetch
怎么办呢,fetch
返回Promise
,但Promise
的错误不能被捕获,怎么办呢?
Promise
错误
- 普通
Promise
错误
try/catch
不能捕获Promise
中的错误
// try/catch 不能处理 JSON.parse 的错误,因为它在 Promise 中
try {
new Promise((resolve,reject) => {
JSON.parse('')
resolve();
})
} catch(err) {
console.error('in try catch', err)
}
// 需要使用catch方法
new Promise((resolve,reject) => {
JSON.parse('')
resolve();
}).catch(err => {
console.log('in catch fn', err)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async
错误
try/catch
不能捕获async
包裹的错误
const getJSON = async () => {
throw new Error('inner error')
}
// 通过try/catch处理
const makeRequest = async () => {
try {
// 捕获不到
JSON.parse(getJSON());
} catch (err) {
console.log('outer', err);
}
};
try {
// try/catch不到
makeRequest()
} catch(err) {
console.error('in try catch', err)
}
try {
// 需要await,才能捕获到
await makeRequest()
} catch(err) {
console.error('in try catch', err)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import chunk
错误
import
其实返回的也是一个promise
,因此使用如下两种方式捕获错误
// Promise catch方法
import(/* webpackChunkName: "incentive" */'./index').then(module => {
module.default()
}).catch((err) => {
console.error('in catch fn', err)
})
// await 方法,try catch
try {
const module = await import(/* webpackChunkName: "incentive" */'./index');
module.default()
} catch(err) {
console.error('in try catch', err)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
以上三种其实归结为Promise
类型错误,可以通过unhandledrejection
捕获
// 全局统一处理Promise
window.addEventListener("unhandledrejection", function(e){
console.log('捕获到异常:', e);
});
fetch('https://tuia.cn/test')
2
3
4
5
不过捕捉不到行数,触发时间在被
reject
但没有reject
处理的时候,可能发生在window
下,也可能在Worker
中
Vue
错误
由于
Vue
会捕获所有Vue
单文件组件或者Vue.extend
继承的代码,所以在Vue
里面出现的错误,并不会直接被window.onerror
捕获,而是会抛给Vue.config.errorHandler
。
/**
* 全局捕获Vue错误,直接扔出给onerror处理
*/
Vue.config.errorHandler = function (err) {
setTimeout(() => {
throw err
})
}
2
3
4
5
6
7
8
React
错误
react
通过componentDidCatch
,声明一个错误边界的组件
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
class App extends React.Component {
render() {
return (
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
但error boundaries
并不会捕捉以下错误:React
事件处理,异步代码,error boundaries
自己抛出的错误
# 前端容灾是什么 该怎么解决
前端容灾指的因为各种原因后端接口挂了(比如服务器断电断网等等),前端依然能保证页面信息能完整展示。
比如 banner
或者列表之类的等等数据是从接口获取的,要是接口获取不到了,怎么办呢?
LocalStorage
在接口正常返回的时候把数据都存到 LocalStorage
,可以把接口路径作为 key
,返回的数据作为 value
然后之次再请求,只要请求失败,就读取 LocalStorage
,把上次的数据拿出来展示,并上报错误信息,以获得缓冲时间
CDN
同时,每次更新都要备份一份静态数据放到CDN
在接口请求失败的时候,并且 LocalStorage
也没有数据的情况下,就去 CDN
摘取备份的静态数据
Service Worker
假如不只是接口数据,整个 html
都想存起来,就可以使用 Service Worker
做离线存储
利用 Service Worker
的请求拦截,不管是存接口数据,还是存页面静态资源文件都可以
// 拦截所有请求事件 缓存中有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
// 查找request中被缓存命中的response
e.respondWith(caches.match(e.request).then( response => {
if (response) {
return response
}
console.log('fetch source')
}))
})
2
3
4
5
6
7
8
9
10
做好这些,整个网站就完全可以离线运行了,但是问题也很明显,就是时效性较高的页面可能会有数据无法同步更新的问题(比如商家库存不足了显示不一致)
另外要注意的是要保证前端页面自身可发布更新,比如页面异常后,此时业务系统要发新版本进行修复和更新,要能确保新版本的资源可以全量替换旧版本的线上资源
# 前端工程化的理解
模块化
将一个大的文件,拆分成多个相互依赖的小文件,按一个个模块来划分
组件化
页面上所有的东西都可以看成组件,页面是个大型组件,可以拆成若干个中型组件,然后中型组件还可以再拆,拆成若干个小型组件
- 组件化≠模块化。模块化只是在文件层面上,对代码和资源的拆分;组件化是在设计层面上,对于
UI
的拆分 - 目前市场上的组件化的框架,主要的有
Vue
,React
,Angular2
规范化
在项目规划初期制定的好坏对于后期的开发有一定影响。包括的规范有
- 目录结构的制定
- 编码规范
editorconfig
、eslint
配置,git husky
在git commit
后自动通过规范化修复 - 前后端接口规范
- 文档规范
- 组件管理
Git
分支管理Commit
描述规范commitizen
使用统一风格的commit
语句- 定期
codeReview
- 视觉图标规范
自动化
也就是简单重复的工作交给机器来做,自动化也就是有很多自动化工具代替我们来完成,例如持续集成、自动化构建、自动化部署、自动化测试等等
笔记
CI
是什么
- 持续集成:当代码仓库代码发生变更,就会自动对代码进行测试和构建,反馈运行结果。
CD
是什么
- 持续交付:持续交付是在持续集成的基础上,可以将集成后的代码依次部署到测试环境、予发布环境、生产环境等中