浏览器是如何渲染页面的?
- 浏览器通过请求 URL 得到一个 HTML 文本
- 解析 HTML 文本,构建 DOM 树
- 解析 HTML 的同时,如果遇到内联样式或者样式脚本,则下载并构建 CSSOM 树,若遇到 JS 脚本,则会下载执行脚本。一般会阻塞 DOM 树的解析。
- DOM 树和 CSSOM 构建完成之后,将两者合并成渲染树(render tree)
- 对渲染树进行布局,生成布局树(layout tree)
- 对布局树进行绘制,生成页面(paint)
说一下回流、重绘以及如何避免?
回流:
当一个元素的 尺寸、位置 等发生变化的时候,浏览器会重新计算该元素的 几何信息。
回流(reflow)也可以称为重排。
- 第一次确定节点的尺寸和位置信息,称之为
布局(Layout)。 - 之后再次获取节点的尺寸、位置信息,称之为
回流。
什么情况下会引起回流呢?
- 添加、删除 DOM 节点。
- 改变了布局,修改了节点的 width、height 等值。
- 触发了 resize,窗口尺寸发生了变化。
- 调用 getComputedStyle 方法获取尺寸、位置信息。
重绘:
当一个元素的 外观、样式 发生变化而布局没有变化的时候,重新绘制的过程叫做“重绘”。比如 背景色、颜色
重绘(repaint)对页面再次绘制。
- 第一次渲染内容称之为绘制(paint)。
- 之后的重新绘制称之为重绘。
什么情况下会引起重绘呢?
- 修改背景色、文字颜色、边框颜色等。
联系:
- 回流一定会引起重绘,重绘不一定会引发回流。所以回流很消耗性能。
避免:
css:
- 如果需要设置动画效果,最好将元素脱离正常的文档流。
- 使用 transform 替代动画。
- 避免使用 CSS 表达式(例如:calc()):参与计算会使用到 getComputedStyle。
js:
- 避免频繁操作样式,最好将样式列表定义为 class 并一次性更改 class 属性。
- 避免频繁操作 DOM,创建一个 documentFragment ,在它上面处理所有 DOM 操作,最后再把它添加到文档中。
- 可以先为元素设置为 display: none,操作结束后再把它显示出来。
display: none; 元素不会出现在 render 树,但是 dom 树上还是存在的,否则无法响应事件。
说说你对 SPA 单页面的理解,它的优缺点分别是什么?
SPA( single page application )单页面应用。整个应用只有一个 HTML 页面,用户与应用程序交互时动态更新该页面。
常利用路由机制实现 HTML 内容的变换,避免整个页面的重新加载。
优点:
- 用户体验好、交互速度快,内容的改变不需要重新加载整个页面,避免了页面的重复渲染;
- 基于上面一点,SPA 相对对服务器压力小,流量也小;
- 前后端职责分离,前端进行交互逻辑,后端负责数据处理;
缺点:
- 白屏时间长:需要由空的 HTML 页面根据 js 动态生成 DOM 结构,并绘制成页面,这个过程会出现白屏时间;
- SEO 难度较大长
客户端渲染(client side render)
浏览器请求 url 获取 HTML 页面,一般拿到的都是一个 HTML 的空壳,里面存在着 HTML 的基本结构以及很多的 script 脚本。 浏览器解析并构建 DOM 树时遇见 script 脚本就会去执行,script 脚本可以动态的去改变 DOM 树的结构。 这种渲染方式叫动态渲染,也叫客户端渲染。
服务端渲染(server side render)
服务端渲染就是在浏览器请求页面 URL 的时候,服务端将我们需要的 HTML 文本组装好,并返回给浏览器。 这个 HTML 文本被浏览器解析之后,不需要经过 JavaScript 脚本的执行,即可直接构建出希望的 DOM 树并展示到页面中。 这个服务端组装 HTML 的过程,叫做服务端渲染。
利于SEO(只处理 HTML) :
- 低级爬虫:只请求URL,URL返回的HTML是什么内容就爬什么内容。
- 高级爬虫:请求URL,加载并执行JavaScript脚本渲染页面,爬JavaScript渲染后的内容。
低级爬虫对客户端渲染的页面来说,简直无能为力,因为返回的HTML是一个空壳,它需要执行 JavaScript 脚本之后才会渲染真正的页面。 而目前像百度、谷歌、微软等公司,有一部分年代老旧的爬虫还属于低级爬虫,使用服务端渲染,对这些低级爬虫更加友好一些。
白屏时间短: 服务端渲染在浏览器请求 URL 之后已经得到了一个带有数据的 HTML 文本,浏览器只需要解析 HTML,直接构建 DOM 树就可以。 而客户端渲染,需要先得到一个空的HTML页面,这个时候页面已经进入白屏,之后还需要经过加载并执行 JavaScript、请求后端服务器获取数据、JavaScript 渲染页面几个过程才可以看到最后的页面。特别是在复杂应用中,由于需要加载 JavaScript 脚本,越是复杂的应用,需要加载的 JavaScript 脚本就越多、越大,这会导致应用的首屏加载时间非常长,进而降低了体验感。
Cookie、LocalStorage、SessionStorage 区别
1. 存储的大小不同
- cookie 的存储是4kb左右,存储量较小,一般页面最多存储20条左右信息。
- localStorage 和 sessionStorage 的存储容量是5Mb(官方介绍,可能和浏览器有部分差异性)。
2. 存储的时间有效期不同
- cookie 的可以设置过期时间。
- sessionStorage 的有效期是仅保持在当前页面,关闭当前会话页或者浏览器后就会失效。如果用户在浏览器中打开新的同源页面,那么新的同源页面将无法访问 sessionStorage 中的数据。
- localStorage 的有效期是在不进行手动删除的情况下是一直有效的。localStorage 中的数据可以在同一浏览器的所有同源页面中共享。
3. 与服务端的通信
- cookie 会参与到与服务端的通信中,一般会携带在 http 请求的头部中。
- localStorage 和 sessionStorage 是单纯的前端存储,不参与与服务端的通信。
什么是浏览器的同源策略?
我对浏览器的同源策略的理解是,一个域下的 js 脚本在未经允许的情况下,不能够访问另一个域的内容。这里的同源的指的是两个域的协议、域名、端口号必须相同,否则不属于同一个域。
同源策略主要限制了三个方面:
- DOM 层面:当前域下的 js 脚本不能对其他域下的 DOM 对象进行读写操作。
- 数据层面:当前域下的 js 脚本不能访问其他域下的 Cookies、IndexDB、LocalStorage、SessionStorage。
- 网络层面:当前域下的 ajax、fetch 不能发送跨域请求。(XML HttpRequest、Fetch等请求)
同源策略的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者 script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作。
说一说跨域是什么?如何解决跨域问题?
跨域:
跨域是由于浏览器的同源策略引起的,是浏览器的一种安全策略,不允许网站执行非同源网站的脚本。
同源策略:协议、域名、端口三者有其中任意一个不一样就会触发跨域。
请求发送了,服务器也返回了,浏览器拒绝接收。
解决:
- cors:跨域资源共享,后端设置响应头 res.setHeader('Access-Control-Allow-Origin', '*');以及资源类型为 javascript。
- 开发时前端服务配置 proxy 代理(代理服务器请求发送数据会绕过浏览器),打包后后端 配置 nginx 反向代理。
- jsonp:利用 script 标签可以跨域请求资源,将回调函数作为参数拼接在 url 中。后端收到请求,调用该回调函数,并将数据作为参数返回去,注意设置响应头返回文档类型,应该设置成 javascript。
- websocket:本身就不存在同源限制。
- postmessage + iframe,可以实现两个 html 页面间的跨域通信
postmessage + iframe
起两个服务,a.html 起在 localhost:3000 上,b.html 起在 localhost:4000 上
//a.html
<body>
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" "load()"></iframe>
<script type="text/javascript">
function load(){
let frame = document.querySelector('#frame');
console.log(frame);
frame.contentWindow.postMessage('我叫俞华','http://localhost:4000')//向端口为4000的域发送内容"我叫俞华"
}
window.onmessage = function(e){
console.log(e.data)
}
</script>
</body>
//b.html
<body>
aaa
<script type="text/javascript">
window.onmessage = function(e){
console.log('eeeeeeeee',e)
console.log(e.data);
//向父级(发射源)发送消息
e.source.postMessage('你好','http://localhost:3000');
}
</script>
</body>
node 中间代理解决跨域原理图
以 vite 或者 webpack 为例:
在 vite 启动的时候,创建了一个开发服务器,然后根据我们进行的开发服务器配置进行 node 中间件代理。vite 根据配置和我们请求的 api 地址去请求对应的 api地址,我们怎么把参数给它的,它就怎么给目标地址;然后目标地址怎么给 vite 的,vite 就原模原样的给我们。
说一下浏览器的垃圾回收机制?
主要讲谷歌的 v8 引擎。
浏览器垃圾回收机制根据数据的存储方式分为 "栈垃圾回收" 和 "堆垃圾回收" 。
栈垃圾回收: 当一个函数执行结束之后,JavaScript 引擎会通过向下移动 ESP(指针)来销毁该函数保存在栈中的执行上下文,对应的内存也被释放了。
堆垃圾回收: 在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。
- 新生代中使用 Scavenge 算法。分为 from 和 to。使用副垃圾回收器。64位操作系统下是64MB。
- 老生代中使用标记-清除算法和标记-整理算法。使用主垃圾回收器。64位操作系统下是1400MB。
加分回答 Scavenge算法: 新生代互换
- 标记:对 from 区域中的垃圾进行标记。
- 清除垃圾数据。
- 整理碎片化内存:副垃圾回收器会把存活的对象复制到 to 区域中,并且有序的排列起来,复制后 to 区域就没有内存碎片了。
- 新生代互换:完成复制后,form 区域与 to 区域进行角色翻转(只是内存名称互换),这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
标记-清除算法:
- 标记:标记阶段递归遍历根元素,在这个遍历过程中,能到达的元素称为活动对象,不能到达的元素就可以判断为垃圾数据。
- 清除:将垃圾数据进行清除。 产生内存碎片:对一块内存多次执行标记-清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存。
标记-整理算法:
- 标记:和标记-清除的标记过程一样。
- 整理:让所有存活的对象都向内存的一端移动。
- 清除:清理边界以外的内存。
全停顿:
执行垃圾回收算法,需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿。如果执行垃圾回收的时间很长,则用户等待的时间也会很长。
增量标记算法: 为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收“标记”过程和 JavaScript 脚本执行交替执行(也就是执行一段 js 脚本,再执行一段垃圾回收的标记操作,以此反复),直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法。
问:为什么老生代不使用和新生代一样的方法?
答: 新生代采用这种方式可以认为是使用空间换取时间的方法,老生代占据内存空间大,再分为 form 与 to 区域的话,极大的浪费了内存空间。
说一下浏览器输入 URL 发生了什么?
1. URL 解析
判断浏览器输入的是否是一个合规的 URL,如果不是合规的 URL 就会当做一个搜索内容交给搜索引擎去搜索。
URL 的组成:
例:https://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#anchor
1. 协议(protocol):互联网支持多种协议,必须指明网址使用哪一种协议,默认是 HTTP 协议。
2. 主机(host):资源所在服务器的名字,也叫做域名,但是有些主机没有域名,只有 IP 地址,所以叫主机比较合适。
3. 端口(port):同一个域名下面可能同时包含多个网站,它们之间通过端口(port)区分。默认端口是 80/443。
4. 路径(path):/path/to/myfile.html
5. 查询参数:?key1=value1&key2=value2
6. 锚点:#anchor,浏览器加载页面以后,会自动滚动到锚点所在的位置,锚点名称通过网页元素的id属性命名。
2. 查找缓存
如果有浏览器缓存并且缓存未过期则直接返回页面。没有缓存直接进行 DNS 域名解析。
3. DNS解析
DNS(Domain Names System)域名系统。是进行域名和与之相对应的 IP 地址进行转换的服务器。主要查找对应域名的主机。
1. 查找缓存:
- 浏览器缓存:检查当前域名的缓存是否在浏览器中。
- 去本地的 hosts 文件中查找。
- 操作系统缓存:操作系统 DNS 缓存。
- 路由器缓存:路由器 DNS 缓存。
- 本地的 DNS 服务器:就是在客户端电脑上设置的首选 DNS 服务器。
2. 查询 IP 地址:
- 根域名服务器
- 顶级域名服务器
- 权威域名服务器
找到 IP 地址后,将它记录在缓存中,供下次使用。
4. TCP连接:三次握手
主要是为了确定双方都具有收发能力。
第一次握手:客户端主动连接服务器,等待服务器确认。-- 表明客户端具有发送能力。
第二次握手:服务器收到消息后发出应答。-- 表明服务器具有接受与发送的能力。
第三次握手:客户端收到应答后,向服务器发送 “确认发送报文段”。-- 表明客户端具有接受能力。
扩展:为什么是三次握手?
答:防止已失效的连接请求又传送到服务器端,因而产生错误
5. 浏览器发送请求
建立连接后,浏览器 HTTP 请求报文获取数据。
报文分为:请求报文、响应报文
请求报文:请求行、请求头、空行、请求体。
- 请求行:请求方法,请求 url、http 协议及其版本。例 POST /report/job/listJobs HTTP/1.1。
- 请求头:浏览器的基本信息,比如:域名、Cookie、浏览器内核、操作系统等。
- 空行:最后一个请求头之后是空行,发送回车符和换行符,通知服务器以下不再有请求头。
- 请求体:当 POST、PUT 请求时,请求数据放在请求体中。
响应报文:响应行、响应头、空行、响应体。
- 响应行:由 HTTP 版本协议字段、状态码和状态码的描述文本3个部分组成。HTTP/1.1 200 OK。
- 响应头:用于指示客户端如何处理响应体,告诉浏览器响应的类型,字符编码和字节大小等信息。
- 空行:最后一个响应头之后是空行,发送回车符和换行符,通知客户端以下不再有响应头。
- 响应体:返回客户端所需要的数据。
此时我们可以拿到返回的 HTML 文件,开始解析渲染页面。
6. 浏览器解析渲染页面
1. HTML 解析
解析 HTML 构建 DOM Tree,当遇到 script 脚本时,会阻塞 DOM Tree 的构建,首先下载并执行 js 代码,之后才继续解析 HTML,构建 DOM 树。(document Object Model)文档对象模型。
2. CSS 解析
在解析 HTML 的过程中,遇到 css 的 link 元素,会下载 css 文件,注意:下载不会阻塞 HTML 的解析。
下载完后,对 css 文件进行解析,构建出 CSSOM 树 (CSS Object Model, css 对象模型)。
3. 构建 Render Tree
DOM 树与 CSSOM 树组合构建 Render 树。
4. 布局(Layout)和绘制(Paint)
布局:
- Render 树会表示显示那些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息。
- 布局的主要目的是为了确定呈现树中所有的宽度、高度和位置信息。
绘制:
- 将每个节点绘制到页面上。
7. 断开连接:TCP 四次挥手
第一次挥手:客户端向服务器发送断开连接请求报文,等待服务器确认。
第二次挥手:服务器收到消息后发出应答。
第三次挥手:服务器向客户端发送断开连接请求报文,等待客户端确认。
第四次挥手:客户端收到消息后发出应答。
DNS 完整的查询过程
DNS(Domain Names System)域名系统。是进行域名和与之相对应的 IP 地址进行转换的服务器。主要查找对应域名的主机。
域名缓存:
计算机中两种缓存方式:
- 浏览器缓存: 浏览器在获取网站域名的实际 IP 地址后会对其进行缓存,减少网络请求的损耗
- 操作系统缓存: 操作系统的缓存其实是用户自己配置的
hosts文件
查询过程:
-
首先搜索浏览器的 DNS 缓存,缓存中维护一张域名与 IP 地址的对应表
-
若没有命中,则会去本地的 hosts 文件中查找。
-
若没有找到,则继续查找操作系统的 DNS 缓存。
-
若没有命中,则操作系统将域名发送至本地域名服务器,本地域名服务器采用递归查询自己的 DNS 缓存,查找成功则返回结果
-
若本地域名服务器的 DNS 缓存没有命中,则本地域名服务器向上级域名服务器进行迭代查询
- 首先本地域名服务器向根域名服务器发起请求,根域名服务器返回顶级域名服务器的地址给本地服务器
- 本地域名服务器拿到这个顶级域名服务器的地址后,就向其发起请求,获取权限域名服务器的地址
- 本地域名服务器根据权限域名服务器的地址向其发起请求,最终得到该域名对应的 IP 地址
-
本地域名服务器将得到的 IP 地址返回给操作系统,同时自己将 IP 地址缓存起来
-
操作系统将 IP 地址返回给浏览器,同时自己也将 IP 地址缓存起来
-
至此,浏览器就得到了域名对应的 IP 地址,并将 IP 地址缓存起来
浏览器缓存
浏览器缓存分为强制缓存和协商缓存,强制缓存优先于协商缓存。
- 若强制缓存( Expires 和 Cache-Control,Cache-Control 优先级高于 Expires )生效则直接使用缓存。
- 若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高),协商缓存由服务器决定是否使用缓存
- 若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回304,继续使用缓存
主要过程如下:
常见的 Web 攻击方式
- XSS (Cross Site Scripting) 跨站脚本攻击
- CSRF(Cross-site request forgery)跨站请求伪造
- SQL 注入攻击
XSS(跨站脚本攻击)
存储型
1、攻击者通过输入框将恶意代码提交到目标网站的数据库中
2、用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器
3、用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行
4、恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作
这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。
反射型
1、攻击者构造出特殊的 URL,其中包含恶意代码
2、用户打开带有恶意代码的 URL 时,服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器
3、用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行
4、恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作
区别:
存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。
DOM 型
1、攻击者构造出特殊的 URL,其中包含恶意代码
2、用户打开带有恶意代码的 URL
3、用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行
4、恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作
区别:
DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。
前面两种恶意脚本都会经过服务器端然后返回给客户端,相对DOM型来说比较好检测与防御,而DOM型不用将恶意脚本传输到服务器在返回客户端,这就是DOM型和反射、存储型的区别,