浏览器结构/运行原理

浏览器是多进程的,分为主进程第三方插件进程GPU进程(用于3D绘制)、浏览器渲染进程(浏览器内核,每个TAB页面对应一个进程。

浏览器内核又有许多线程:

GUI渲染线程:负责解析渲染,布局绘制,重绘时该线程会执行。与js引擎线程互斥。

js引擎线程:也称JS内核,负责处理js脚本。

事件触发线程:归属浏览器而不是js引擎线程(js是单线程),当js引擎执行settimeout/点击事件时,会将对应任务添加到事件触发线程中。符合触发条件时会将任务放入处理队伍队尾等待js引擎线程处理。

定时器触发线程:定时器是单个线程进行计时的,计时结束会将事件添加到事件队列后等待js引擎线程处理。

异步HTTP请求线程:XHR创建时。当检测到状态变化并有回调函数,会将回调函数放进事件队列等待js引擎线程处理。

参考:https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6bc05b033f444b4198f0ce6084cc7b52~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp

从输入url到页面渲染发生了什么?

  • 发起DNS请求,DNS解析器首先去自身的缓存查询对应ip,查询顺序依次是浏览器缓存->系统host缓存->路由缓存->ISP缓存。查看是否有域名对应的ip,没有就向本地DNS服务器发起查询,如果也没有查到,它就会向根DNS服务器发起查询,得到顶级域DNS服务器的ip,再向该顶级域DNS服务器发起查询,得到权威DNS服务器ip,最后权威DNS服务器将查询到的IP地址返回给本地DNS服务器,本地DNS服务器再把结果返回给DNS解析器。

  • 浏览器获取到DNS解析器返回的IP地址,根据IP地址和默认端口跟服务器建立TCP连接。

  • 浏览器发起HTTP请求。连接建立后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的Cookie等数据附加到请求头中,将该请求报文作为TCP三次握手的第三次数据发送给服务器。如果是HTTPS的话,还涉及到HTTPS的加解密流程。

  • 服务器端响应,发送响应数据给浏览器。

  • TCP四次挥手断开连接。

  • 浏览器解析响应并渲染页面,首先浏览器解析响应头。若响应头状态码为301、302,会重定向到新地址;若响应数据类型是字节流类型,一般会将请求提交给下载管理器;若是HTML类型,会进入下一步渲染流程。

  • 浏览器解析HTML内容并渲染,这个具体过程是:

解析HTML构建DOM树;CSS文件构建成CSSOM树;将DOM树和CSSOM树结合,构建渲染树;然后根据渲染树来布局,计算每个节点在屏幕中的位置;再调用GPU绘制合成图层,显示到屏幕上。这个过程浏览器会边解析边渲染。涉及到重排(回流)、重绘

重排(回流)、重绘

浏览器计算元素的位置和尺寸会触发重排绘制元素的颜色、字体等其他属性称为重绘

具体什么情况会触发重排?

添加删除dom;display:none隐藏;调整窗口大小,改编字号,获取特定属性比如offsetWidth和offsetHeight

避免重排:

  • 集中改变样式,不要一条一条的修改;

  • 不要把DOM节点里的属性放到循环;

  • 为动画的HTML元件使用fixed或者absolute,让他们脱离文档流。

  • 尽量不用table布局

  • 尽量用visibility:hidden去替代display:none

浏览器阻塞问题

浏览器获取到HTML文件后解析DOM,会异步请求获取文档中所有的css、js、图片等资源。

css的解析不会影响DOM解析(两树一起构成渲染树),但是会阻塞渲染(不然可能出现不好的用户体验),以及会阻塞js的解析(脚本中可能涉及样式的改变,所以要先等css解析完)。

js下载和解析会阻塞DOM解析(脚本中可能会涉及DOM操作,需等到js解析完成),同样也会阻塞渲染

所以script最好放在页面底部,css放在head

异步加载js的方式

deferasync,加在script标签里,defer表示延迟,async表示同步。

  • 当浏览器遇到async的script标签,马上异步请求该脚本资源,不会阻塞浏览器解析HTML。网络请求回来后,如果HTML还没解析完,就会暂停解析,先让js引擎执行代码,执行后再继续解析。(请求不阻塞解析,js先执行,会阻塞解析)其他脚本不会等待 async 脚本加载完成,同样,async 脚本也不会等待其他脚本。

  • defer会在请求脚本的时候解析HTML,一直到解析完毕再执行js代码

    (一直不会阻塞html解析,请求和解析同样可以同步,但要解析完才执行js)

    defer 特性除了告诉浏览器“不要阻塞页面”之外,还可以确保脚本执行的相对顺序。

async 的优先级高于defer

为什么script标签放在html文档的最后,link标签放在html文档开头?

  • script标签是从上到下边加载边解析执行,下载和解析会阻塞html解析,从而阻塞页面渲染,这是我们不想看到的,所以一般把script标签放在html文档的最后,或者给script标签加上defer,async参数,强制让script标签延迟加载。
  • 多个link是同时加载,先加载完的优先解析。link标签不会阻塞html解析,如果link标签放在dom之后,会导致浏览器发生回流重绘,这个开销是非常大的,所以我们一般把link标签放在html文档开头(head)中。

浏览器合成图层

浏览器渲染的图层一般包括普通图层复合图层。硬件加速的方式可以声明一个复合图层,单独分配资源,硬件加速的方法:translate3d/translateZ,opacity属性,过渡动画,videa,iframe,canvas,webgl等元素。可以避免整个页面重绘,提高性能。

同源策略/跨域是什么?如何解决跨域问题?

跨域问题因为浏览器的同源策略引发。只有协议、域名、端口号都相同的才能算同源。

解决方案

​ 最常用的:

  • jsonp:利用了script标签没有跨域限制的特点,但是一定要后端配合。优点是简单兼容性好,缺点是仅支持get方法,且有遭受XSS攻击的风险。做法:声明一个回调函数,函数名作为url的参数,传递给跨域请求数据的服务器,函数形参为要获取目标数据。后端把要传递的数据放进回调的参数中传过来。

  • CORS:利用了Access-Control-Allow-Origin的响应头部,标识了允许哪个域的请求。注意,如果设置为*则浏览器不会发送cookie过去,即使XHR设置了withCredentials。主要是后端实现。

  • Node中间件代理:配置代理服务器将浏览器请求转发给服务器,再将响应请求转发给浏览器。因为同源策略对服务器不加限制。

  • nginx反向代理:使用nginx配置一个代理服务器做跳板。

    扩展的一些方式:

  • websocket:HTML5协议,实现了浏览器和服务器的全双工通信,也是应用层协议,建立连接也需要HTTP协议,建立后就与HTTP无关了。同样可以实现跨域。

  • window.name

  • document.domain(只适用于二级域名相同的情况如a.test.com 和 b.test.com)

  • postMessage:HTML5的API,是一个可以跨域操作的window属性,

1
2
3
4
5
6
7
frame.contentWindow.postMessage('我爱你', 'http://localhost:4000') //发送数据

window.onmessage = function(e) { //接受返回数据

console.log(e.data) //我不爱你

}

jsonp具体实现:

浏览器安全

CSRF(跨站请求伪造)

攻击者盗用用户身份发起请求。攻击的流程原理是,用户访问网站A,输入用户名和密码登录后,通过认证后,A产生一个cookie返回给浏览器,用户在没有退出A网站之前在同一个浏览器打开了攻击网站B,B发出一个请求要求访问网站A,A把这个请求误认为是用户发起的,于是A处理了攻击网站的请求。

预防CSRF

针对csrf发生在第三方域名攻击者也不能获取到cookie等信息,可以防止不明外域访问提交时要求附加本域才能获取的信息

  • 同源检测:HTTP中的referer header和 origin header字段,记录了请求的来源地址,origin指示了请求来自哪个站点,只有服务器名不包含路径信息,referer指示请求来自哪个具体页面,包含服务器和详细url;

  • 使用token验证,在HTTP请求头添加token字段,服务器建立一个拦截器验证这个token,是否可以验证通过。Referer简单但仍然可能有漏洞,是浏览器提供,不同浏览器的具体实现有差别,攻击者可以通过referrerpolicy隐藏请求中的referer可靠性不大;

  • 使用验证码和密码也可以起到csrf token的作用,而且更安全;

  • samesite cookie属性,为set-cookie响应头新增samesite属性,两个值strict和lax。Strict浏览器在任何跨域请求中都不会携带cookie,新标签重新打开也不会携带,但是跳转子域名或者新标签重新打开刚登陆的网站,之前的cookie都会不存在,需要重新登录,用户体验不好,另外兼容性也不太好;

  • 防止攻击的发生:对用户上传的图片进行转存或校验,用户打开其他用户填写的链接时需告知风险。用csrftester工具去测试csrf漏洞。

XSS(跨站脚本攻击)

攻击者通过在目标网站注入恶意脚本,使之在用户的浏览器上运行。会泄露用户的cookie、sessionid等信息,攻击者可以通过cookie绕过登录。

攻击分为反射性存储型DOM型

反射型:通过url参数直接注入,攻击者构造出带恶意代码的url,服务器将恶意代码从url中取出,拼接在html中返回给浏览器,浏览器解析执行恶意代码。

存储型:将恶意代码存储到数据库;

DOM型:也是攻击者将恶意代码放到url中,且是js的代码,前两种是服务端漏洞,第三种是js漏洞

预防XSS

  • 对数据进行转义,将数据进行json序列化;

  • 后端设置set-cookie响应头:httponly,禁止js读取某些敏感cookie,cookie在客户端上无法被访问,cookie不能跨域;

  • 设置secure,保证cookie只在https下传输。

服务端渲染(SSR)和客户端渲染(SPA)

客户端渲染:单页面应用,节省前后端资源、局部刷新、前后端分离,但首屏渲染时间边变长,存在严重的SEO问题

服务端渲染:服务器直接将带数据的内容通过HTML文本形式返回,(对比SPA,客户端需要执行服务器返回的JavaScript才能得到正确的网页内容)。解决方案有1.next.js nuxt.js2.node+vue-server-render实现

模板引擎(ejs、jade、pug等)

rendertron(google提供的,使得spa也能被不支持执行JavaScript的搜索引擎爬取渲染后的内容)

浏览器缓存/HTTP缓存

对请求过的文件缓存,降低服务器压力。强缓存是客服端不会向服务器发请求,先检查浏览器本地是否有可以用的缓存,如果有则使用,没有则向服务器发请求,看是否命中协商缓存,命中则更新缓存时间,返回304,没有命中则向服务器请求信息。

分为强缓存协商缓存

强缓存响应头中相关字段为ExpiresCache-Control 。前者http1.0是使用的绝对时间,客户端服务器时间不一致可能引起错乱;后者是http1.1使用的相对时间,有个max-age表示在这个请求正确返回时间的多少秒内缓存有效。后者会覆盖前者。

协商缓存相关头字段是Last-Modified/If-Modified-Since() Etag/If-None-Match(http1. 1)

Last-Modified是浏览器向服务器发送资源最后的修改时间。If-Modified-Since表示请求时间。服务器收到请求将请求时间和最后修改时间做对比,如果两者一样则可以使用协商缓存,否则重新发请求,返回最新的资源。

1.1允许使用etag,Etag的值是由服务器返回给前端,是当前资源文件的唯一标识,由文件的索引节点、大小和最后修改时间进行Hash后得到的,只要文件修改了,Etag就会变更。当资源过期,浏览器发现响应头有Etag,再次向服务器请求就会带上If-None-Match,值是Etag的值,服务器收到后进行比对,决定是都命中协商缓存。

Ajax和fetch的区别

都用于发送网络请求。

Ajax:异步的 Javascript 和 XML,是一项让页面可以实现局部刷新的技术。其中 XMLHttpRequest 模块就是实现 Ajax 的一种很好的方式。

缺点:只需要使用ajax却要引入整个JQuery。

Fetch:ES6出现的,是一个真实的api,是基于promise的。它是 XMLHttpRequest 的替代品。

Axios:一个封装库,利用了XHR进行二次封装。xhr是axios中的其中一个请求适配器,axios在nodejs端还有个http的请求适配器。

特点:

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

(功能更多,同样支持Promise API,可以拦截请求和响应、取消请求、自动转换JSON数据,客户端支持防御XSRF攻击。)

1
2
3
4
5
6
7
8
9
// 发送 POST 请求
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
})

浏览器本地存储/cookie、sessionStorage、localStorage 三者的区别

cookiesessionStoragelocalStorage 三者的区别

【得分点】

数据存储位置、生命周期、存储大小、写入方式、数据共享、发送请求时是否携带、应用场景

共同点

这三者都是浏览器的本地存储。

不同点

首先他们的生命周期不一样,cookie的生命周期是由服务器端设置好的,sessionStorage只存在于浏览器窗口打开的时候,窗口关闭时自动清除;localStorage是长期的存储,只要不手动清除就会一直存在。

然后是存储大小的不同,cookie的存储空间大小大概是4k,而sessionStorage和localStorage存储空间较大一些,大概在5M。

再就是这三者都要遵循浏览器同源策略,sessionStorage还限制必须是同一个页面。

另外前端给后端发请求时会自动携带上cookie,其他两种不会,始终只会保存在客户端。

实践应用

所以根据这些特点,他们三者的应用场景也有不同,cookie一般用于存储登录验证信息sessionid或者token;

localstorage常用于存储不易变动的数据,减轻服务器压力,比如说购物车功能,就是把购物车中的商品存入到localStorage中,不管怎么刷新始终都在,不会向服务器频繁发请求;

sessionStorage可以检测用户是否刷新了页面,比如如果要实现从一个列表的某个地方点进去查看详情,回退后依然在那个位置,就可以把页面的坐标存进sessionStorage,只要不刷新页面,就始终不会清除位置信息。

token、cookie和session的区别(登录验证方案有哪些?)

登陆验证方案有session、token、jwt(json web token)

【为什么需要这些登录验证方案?】因为http协议是一个无状态的协议,这次请求这上次请求是没有关系,互不影响的,所以需要一种同一域名下共享数据的方式。

  • session是存储在服务器端的,是一个状态列表。有一个唯一标识符sessionid,通常放在cookie里面,浏览器想再次请求时就会在请求头携带有sessionid的cookie,服务器根据id从内存中获取对应用户信息返回给浏览器。

  • 所以说cookie是经常和session一起使用的,cookie只是一种本地存储方式

  • tokenuid + time(时间戳)+ sign(签名)+ 固定参数构成,服务端不会保存身份验证相关的数据,它只是一个临时的证书签名。一般也是可以存储在cookie、localstorage这些里面的。

    【负载均衡】为了负载均衡,一般会设置一个第三方缓存数据库redis,把token存进redis里,避免了session存在一个服务器上,而去另一个服务器请求时找不到数据的情况。

    【认证流程】和session差不多,浏览器再次请求时会携带token,然后在redis中查询这个token,以token为key获取用户信息返回给客户端。

    【和session方案相比的安全性】另外,token也可以避免CSRF。因为csrf只能伪装成用户发请求,但是拿不到cookie,token验证方案要求每次提交请求时带上token,这个token攻击者是拿不到的,因为cookie采取同源策略,只能在自己的网页上getCookie。

    【引入jwt方案】另外,token还可以用jwt方案,不用redis实现负载均衡,直接将加密后的用户数据存储在token里面,安全性也更高。用户登陆成功后将信息进行加密生成token返回给客户端。生成token同时会加入一个密钥,后续浏览器使用token向服务器请求信息要先进行解码。

前端路由

将不同页面交给不同路由来做,页面使用期间不会刷新,稍复杂的SPA项目中都会用到。

优点:

  1. 用户体验好,和后台网速没有关系,不需要每次都从服务器全部获取,快速展现给用户
  2. 可以再浏览器中输入指定想要访问的url路径地址  
  3. 实现了前后端的分离,方便开发。有很多框架都带有路由功能模块。

缺点:

  1. 使用浏览器的前进,后退键的时候会重新发送请求,没有合理地利用缓存

  2. 单页面无法记住之前滚动的位置,无法在前进,后退的时候记住滚动的位置

路由的hash和history模式区别

hash模式:

  • hash 模式是一种把前端路由的路径用井号 # 拼接在真实 url 后面的模式。当井号 # 后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发 onhashchange 事件。
  • hash变化会触发网页跳转,即浏览器的前进和后退。
  • hash 可以改变 url ,但是不会触发页面重新加载(hash的改变是记录在 window.history 中),即不会刷新页面。也就是说,所有页面的跳转都是在客户端进行操作。因此,这并不算是一次 http 请求,所以这种模式不利于 SEO 优化。hash 只能修改 # 后面的部分,所以只能跳转到与当前 url 同文档的 url
  • hash 通过 window.onhashchange 的方式,来监听 hash 的改变,借此实现无刷新跳转的功能。
  • hash 永远不会提交到 server 端(可以理解为只在前端自生自灭)。

history模式:

  • history APIH5 提供的新特性,允许开发者直接更改前端路由,即更新浏览器 URL 地址而不重新发起请求

  • 新的 url 可以是与当前 url 同源的任意 url ,也可以是与当前 url 一样的地址,但是这样会导致的一个问题是,会把重复的这一次操作记录到栈当中。

  • 通过 history.state ,添加任意类型的数据到记录中。

  • 可以额外设置 title 属性,以便后续使用。

  • 通过 pushStatereplaceState 来实现无刷新跳转的功能。

  • 使用 history 模式时,在对当前的页面进行刷新时,此时浏览器会重新发起请求。如果 nginx 没有匹配得到当前的 url ,就会出现 404 的页面。

参考资料

https://juejin.cn/post/6993840419041706014

前端性能优化

加载方面的优化比如:

  • 按需加载 静态资源cdn加速
  • 图片压缩文件压缩:可以使用打包工具。
  • 减少网络请求次数:精灵图节流防抖

渲染方面的优化:

  • 提前渲染:ssr服务端渲染
  • 避免渲染阻塞:css放在HTML的头部,js放在HTML的body底部。(css不会阻塞dom的解析和dom树生成,会阻塞页面渲染;js会阻塞dom解析和页面渲染,不会阻塞资源下载;浏览器遇到script标签且无defer、async属性会立即下载执行中断HTML的解析)
  • 避免无用的渲染:懒加载(路由懒加载 模块懒加载)。
  • 减少渲染次数:缓存(HTTP缓存、浏览器本地缓存、Vue的keep-alive缓存 )、对DOM的操作合并,尽量避免重流的发生。

图片优化

  • 字体图标代替图片图标
  • webpack缓存
  • 图片延迟加载(data-src)
  • 图片压缩(image-webpack-loader)