浏览器原理入门(3)
DOM树
从网络传给渲染引擎的HTML文件字节流是无法直接被渲染引擎理解的,所以要将其转化为渲染引擎能够理解的内部结构,这个结构就是DOM。
- 从页面的视角看,DOM是生成页面的基本数据结构
- 从JavaScript脚本视角来看,DOM提供了JavaScript脚本操作的接口,通过这套接口JavaScript可以对DOM结构进行访问,从而改变文档的结构、样式和内容。
- 从安全视角来看,DOM是一道安全防线,一些不安全的内容在DOM解析阶段就被拒之门外。
DOM树如何生成
HTML解析器(HTMLParser)
⽹络进程加载了多少数据,HTML解析器便解析多少数据。
⽹络进程接收到响应头之后,会根据响应头中的content-type字段来判断⽂件的类型,⽐如contenttype的值是“text/html”,那么浏览器就会判断这是⼀个HTML类型的⽂件,然后为该请求选择或者创建⼀个渲染进程。渲染进程准备好之后,⽹络进程和渲染进程之间会建⽴⼀个共享数据的管道,⽹络进程接收到数据后就往这个管道⾥⾯放,⽽渲染进程则从管道的另外⼀端不断地读取数据,并同时将读取的数据“喂”给HTML解析器。你可以把这个管道想象成⼀个“⽔管”,⽹络进程接收到的字节流像⽔⼀样倒进这个“⽔管”,⽽“⽔管”的另外⼀端是渲染进程的HTML解析器,它会动态接收字节流,并将其解析为DOM。
- 通过分词器将字节流转换为Token。
- 第二个和第三个阶段是同步进行的,需要将Token解析为DOM节点,并将DOM节点添加到DOM树中。
HTML解析器维护了一个Token栈结构,该Token栈主要用来计算节点之间的父子关系,在第一个阶段中生成的Token会被按顺序压到这个栈中。
- 如果压入栈中的StartTag Token,HTML解析器会为该Token创建个DOM节点,然后该节点加入到DOM树中,它的父节点就是栈中相邻的那个元素生成的节点。
- 如果解析器解析出文本Token,那么会生成一个文本节点,然后将该节点加入到DOM树中,文本Token是不需要压入到栈中,它的父节点就就是当前栈顶Token所对应的DOM节点。
- 如果分词器解析出来的是EndTag标签,比如是EndTag div,Html解析器会查看Token栈顶的元素是否是StarTag div,如果是九江StartTag div从栈中弹出,表示该div元素解析完成。
<html>
<body>
<div>1</div>
<div>test</div>
</body>
</html>
HTML解析器开始工作时会默认创建一个根为document的空DOM结构




在实际生产环境中HTML源文件中既包含CSS和JavaScript,又包含图片、音频、视频等文件,所以处理过程远比上面这个示范Demo复杂。
JavaScript是如何影响DOM生成的
- 当解析到内嵌JavaScript脚本标签时,HTML解析器暂停工作,JavaScript引擎介入,并执行script标签中的脚本,脚本会修改DOM中内容,脚本执行完成之后,HTML解析器恢复解析过程,继续解析后续的内容,直到生成最终的DOM。
- JavaScript文件的下载过程会阻塞DOM解析
- Chrome浏览器做了很多优化,其中一个主要的优化是预解析操作。当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析HTML文件中包含的JavaScript、CSS等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件。
- 使用CDN来加速JavaScript文件的加载,压缩JavaScript文件的体积。
- 可以将该JavaScript脚本设置为异步加载,通过async或defer来标记代码。
在执行JavaScript之前,需要先解析JavaScript语句之上所有的css样式。如果代码里引用了外部的CSS文件,那么在执行JavaScript之前,还需要等待外部CSS文件下载完成,并解析生成CSSOM对象之后,才能执行JavaScript脚本。不管该脚本是否操纵了CSSOM,都会执行CSS文件下载,解析操作,再执行JavaScript脚本。
总结:JavaScript会阻塞DOM生成,而样式文件又会阻塞JavaScript的执行
CSS如何影响首次加载
<html>
<head>
<link href="theme.css" rel="stylesheet"/>
</head>
<body>
<div>Bob</div>
</body>
</html>
渲染流水线为什么需要CSSOM:
- 提供给JavaScript操作样式表的能力
- 布局树的合成提供了基础的样式信息
CSSOM体现在DOM中就是document.styleSheets。
等DOM和CSSOM都构建好厚,渲染引擎就会构建布局树
<html>
<head>
<link href="theme.css" rel="stylesheet"/>
</head>
<body>
<div>test</div>
<script>
console.log('test')
</script>
<div>test</div>
</body>
</html>
<html>
<head>
<link href="theme.css" rel="stylesheet"/>
</head>
<body>
<div>test</div>
<script src="foo.js"></script>
<div>test</div>
</body>
</html>
短暂白屏市场策略:
- 通过内联JavaScript、内联CSS来移除这两种类型的文件下载,这样获取到HTML文件之后就可以直接开始渲染流程了。
- 但并不是所有的场合都适合内联,那么还可以尽可能减少文件大小,比如通过webpack等工具移除不必要的注释,并压缩JavaScript文件。
- 可以将一些不需要再解析HTML阶段使用的JavaScript标记上sync或者defer。
- 对于大的css文件,可以通过媒体查询属性,将其拆分为多个不同用途的CSS文件,这样只有在特定的场景下才会加载特定的CSS文件。
CSS动画比JavaScript高效
显示器是怎样显示图像的
每个显⽰器都有固定的刷新频率,通常是60HZ,也就是每秒更新60张图⽚,更新的图⽚都来⾃于显卡中⼀个叫前缓冲区的地⽅,显⽰器所做的任务很简单,就是每秒固定读取60次前缓冲区中的图像,并将读取的图像显⽰到显⽰器上。显卡的更新频率和显⽰器的刷新频率是⼀致的,显卡的职责就是合成新的图像,并将图像保存到后缓冲区中,⼀旦显卡把合成的图像写到后缓冲区,系统就会让后缓冲区和前缓冲区互换,这样就能保证显⽰器能读取到最新显卡合成的图像。
任意一帧的生成方式,有重排、重绘和合成三种当时
这三种⽅式的渲染路径是不同的,通常渲染路径越⻓,⽣成图像花费的时间就越多。⽐如重排,它需要重新根据CSSOM和DOM来计算布局树,这样⽣成⼀幅图⽚时,会让整个渲染流⽔线的每个阶段都执⾏⼀遍,如果布局复杂的话,就很难保证渲染的效率了。⽽重绘因为没有了重新布局的阶段,操作效率稍微⾼点,但是依然需要重新计算绘制信息,并触发绘制操作之后的⼀系列操作。相较于重排和重绘,合成操作的路径就显得⾮常短了,并不需要触发布局和绘制两个阶段,如果采⽤了GPU,那么合成的效率会⾮常⾼。合成操作是在合成线程上完成的,这也就意味着在执⾏合成操作时,是不会影响到主线程执⾏的
如何利用分层技术优化代码
- 在写Web应⽤的时候,可能经常需要对某个元素做⼏何形状变换、透明度变换或者⼀些缩放操作,如果使⽤JavaScript来写这些效果,会牵涉到整个渲染流⽔线,所以JavaScript的绘制效率会⾮常低下。 •可以使⽤will-change来告诉渲染引擎你会对该元素做⼀些特效变换,提前告诉渲染引擎box元素将要做⼏何变换和透明度变换操作,这时候渲染引擎会将该元素单独实现⼀帧,等这些变换发⽣时,渲染引擎会通过合成线程直接去处理变换,这些变换并没有涉及到主线程,这样就⼤⼤提升了渲染的效率。这也是CSS动画⽐JavaScript动画⾼效的原因
优化页面
要让页面更快地显示和响应
加载阶段
- 减少关键资源个数:⼀种⽅式是可以将JavaScript和CSS改成内联的形式,⽐如上图的JavaScript和CSS,若都改成内联模式,那么关键资源的个数就由3个减少到了1个。另⼀种⽅式,如果JavaScript代码没有DOM或者CSSOM的操作,则可以改成async或者defer属性;同样对于CSS,如果不是在构建⻚⾯之前加载的,则可以添加媒体取消阻⽌显现的标志。当JavaScrip标签加上了async或者defer、CSSlink属性之前加上了取消阻⽌显现的标志后,它们就变成了⾮关键资源了。
- 降低关键资源⼤⼩:压缩CSS和JavaScript资源,移除HTML、CSS、JavaScript⽂件中⼀些注释内容,也可以通过前⾯讲的取消CSS或者JavaScript中关键资源的⽅式。降低关键资源的RTT次数(Round Trip Time):通过减少关键资源的个数和减少关键资源的⼤⼩搭配来实现。除此之外,还可以使⽤CDN来减少每次RTT时⻓。
交互阶段
- 较少JavaScript脚本执行时间:将一次执行的函数分解为多个任务,使得每次的执行时间不要过久、采用Web Workers
避免强制同步布局:尽量不要在修改DOM结构是再去查询一些相关值- 合理利用CSS合成动画
- 避免频繁的垃圾回收
浏览器安全
同源策略
如果两个URL的协议、域名和端⼝都相同,我们就称这两个URL同源。同源策略会隔离不同源的DOM、⻚⾯数据和⽹络通信,进⽽实现Web⻚⾯的安全性。不过⻥和熊掌不可兼得,要绝对的安全就要牺牲掉便利性,因此我们要在这⼆者之间做权衡,找到中间的⼀个平衡点,也就是⽬前的⻚⾯安全策略原型
- 页面中可以引用第三方资源,不过这也暴露了很多诸如XSS的安全问题,因此又在这种开放的基础之上引入了CSP来限制其自由程度。
- 使用XMLHttpRequest和Fetch都是无法直接进行跨域请求的,因此浏览器又在这种严格策略的基础之上引入了跨资源共享策略,让其可以安全地进行跨域操作。
- 两个不同源的DOM是不能相互操作的,因此浏览器又实现了跨文档的消息机制,让其可以比较安全地通信。
跨站脚本攻击(XSS)
XSS攻击是指黑客往HTML文件中或者DOM中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。
- 窃取Cookie信息
- 监听用户行为
- 修改DOM伪造假的登陆窗口,用来欺骗用户输入用户名和密码等信息
- 在页面内生成浮窗广告
恶意脚本是怎么注入的
存储型XSS攻击
首先黑客利用站点漏洞将一段恶意JavaScript代码提交到网站的数据库中
然后用户向网站请求包含了恶意JavaScript脚本的页面
当用户浏览该页面的时候,恶意脚本就会将用户的Cookie信息等数据上传到服务器
反射型XSS攻击
恶意JavaScript脚本属于⽤⼾发送给⽹站请求中的⼀部分,随后⽹站⼜把恶意JavaScript脚本返回给⽤⼾。当恶意JavaScript脚本在⽤⼾⻚⾯中被执⾏时,⿊客就可以利⽤该脚本做⼀些恶意操作。在现实⽣活中,⿊客经常会通过QQ群或者邮件等渠道诱导⽤⼾去点击这些恶意链接,所以对于⼀些链接我们⼀定要慎之⼜慎。Web服务器不会存储反射型XSS攻击的恶意脚本,这是和存储型XSS攻击不同的地⽅。
基于DOM的XSS攻击
基于DOM的XSS攻击是不牵涉到页面Web服务器的。具体来讲,黑客通过各种手段将恶意脚本注入到用户的页面中,比如通过网络劫持在页面传输过程中修改HTML页面的内容,这种劫持类型很多,有通过WIFI路由器劫持的,有通过本地恶意软件来劫持的,它们的共同点是在Web资源传输过程或者在用户使用页面的过程中修改Web页面的数据。
如何阻止XSS攻击
- 服务器对输入脚本进行过滤或转码
- 充分利用CSP:限制加载其他域下的资源文件、进制向第三方域提交数据,这样的用户数据也不会外泄,禁止直行内联脚本和未授权的脚本、提供上报机制。
- 使用HttpOnly
CSRF攻击
黑客利用了用户的登陆状态,并通过第三方的站点来做一些坏事。
- 自动发起Get、POST请求
- 引诱用户点击链接:通常出现在论坛或者恶意邮件上。 ⿊客站点代码,⻚⾯上放了⼀张美⼥图⽚,下⾯放了图⽚下载地址,⽽这个下载地址实际上是⿊客⽤来转账的接⼝,⼀旦⽤⼾点击了这个链接,那么他的币就被转到⿊客账⼾上了。和XSS不同的是,CSRF攻击不需要将恶意代码注⼊⽤⼾的⻚⾯,仅仅是利⽤服务器的漏洞和⽤⼾的登录状态来实施攻击。
如何防止CSRF攻击
充分利用好Cookie的SameSite属性(Strict、Lax和None三个值)
- Strict最为严格。如果SameSite的值是Strict,那么浏览器会完全禁⽌第三⽅Cookie。简⾔之,如果你从极客时间的⻚⾯中访问InfoQ的资源,⽽InfoQ的某些Cookie设置了SameSite=Strict的话,那么这些Cookie是不会被发送到InfoQ的服务器上的。只有从InfoQ的站点去请求InfoQ的资源时,才会带上这些Cookie。
- Lax相对宽松⼀点。在跨站点的情况下,从第三⽅站点的链接打开和从第三⽅站点提交Get⽅式的表单这两种⽅式都会携带Cookie。但如果在第三⽅站点中使⽤Post⽅法,或者通过img、iframe等标签加载的URL,这些场景都不会携带Cookie。
- ⽽如果使⽤None的话,在任何情况下都会发送Cookie数据。验证请求的来源站点:Referer是HTTP请求头中的⼀个字段,记录了该HTTP请求的来源地址、Origin属性,在⼀些重要的场合,⽐如通过XMLHttpRequest、Fecth发起跨站请求或者通过Post⽅法发送请求时,都会带上Origin属性。Origin属性只包含了域名信息,并没有包含具体的URL路径,这是Origin和Referer的⼀个主要区别。服务器的策略是优先判断Origin,如果请求头中没有包含Origin属性,再根据实际情况判断是否使⽤Referer值。CSRFToken:在浏览器向服务器发起请求时,服务器⽣成⼀个CSRFToken。在浏览器端如果要发起转账的请求,那么需要带上⻚⾯中的CSRFToken,然后服务器会验证该Token是否合法
HTTPS

HTTP⼀直保持着明⽂传输数据的特征。但这样的话,在传输过程中的每⼀个环节,数据都有可能被窃取或者篡改,这也意味着你和服务器之间还可能有个中间⼈,你们在通信过程中的⼀切内容都在中间⼈的掌握中,如下图:

对发起HTTP请求的数据进⾏加密操作和对接收到HTTP的内容进⾏解密操作。
对称加密是指加密和解密都使⽤的是相同的密钥。
⾮对称加密算法有A、B两把密钥,如果你⽤A密钥来加密,那么只能使⽤B密钥来解密;反过来,如果你要B密钥来加密,那么只能⽤A密钥来解密。公钥是每个⼈都能获取到的,⽽私钥只有服务器才能知道,不对任何⼈公开。
- ⾸先浏览器向服务器发送对称加密套件列表、⾮对称加密套件列表和随机数client-random;
- 服务器保存随机数client-random,选择对称加密和⾮对称加密的套件,然后⽣成随机数servicerandom,向浏览器发送选择的加密套件、service-random和公钥;
- 浏览器保存公钥,并⽣成随机数pre-master,然后利⽤公钥对pre-master加密,并向服务器发送加密后的数据
- 最后服务器拿出⾃⼰的私钥,解密出pre-master数据,并返回确认消息。
到此为⽌,服务器和浏览器就有了共同的client-random、service-random和pre-master,然后服务器和浏览器会使⽤这三组随机数⽣成对称密钥,因为服务器和浏览器使⽤同⼀套⽅法来⽣成密钥,所以最终⽣成的密钥也是相同的。需要特别注意的⼀点,pre-master是经过公钥加密之后传输的,所以⿊客⽆法获取到pre-master,这样⿊客就⽆法⽣成密钥,也就保证了⿊客⽆法破解传输过程中的数据了。
添加证书

相比于第三版的HTTPS协议,主要改变:
- 服务器没有直接返回公钥给浏览器,而是反回了数字证书,而公钥正是包含在数字证书中的。