1. 常见的浏览器内核
浏览器/RunTime | 内核(渲染引擎) | JavaScript 引擎 |
---|---|---|
Chrome | webkit->blink | V8 |
FireFox | Gecko | SpiderMonkey |
Safari | Webkit | JavaScriptCore |
Edge | EdgeHTML | Chakra(for JavaScript) |
IE | Trident | JScript(IE3.0-IE8.0) |
Opera | Presto->blink | Linear A(4.0-6.1)/ Linear B(7.0-9.2)/ Futhark(9.5-10.2)/ Carakan(10.5-) |
Node.js | V8 |
2. 浏览器的主要组成部分
- 「用户界面」 - 包括地址栏、前进/后退按钮、书签菜单等。
- 「浏览器引擎」 - 在用户界面和呈现引擎之间传送指令。
- 「呈现引擎」 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
- 「网络」 - 用于网络调用,比如 HTTP 请求。
- 「用户界面后端」 -用于绘制基本的窗口小部件,比如组合框和窗口。
- 「JavaScript 解释器」- 用于解析和执行 JavaScript 代码。
- 「数据存储」 - 这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。
值得注意的是,和大多数浏览器不同,Chrome 浏览器的每个标签页都分别对应一个呈现引擎实例。每个标签页都是一个独立的进程。
3. 浏览器的跨域问题
3.1 为什么浏览器会禁止跨域
浏览器是很开放的,只要在地址栏里面输入网址或者点击某个链接就可以访问了。正是因为这种开放的形态,才需要对浏览器做出限制,保护用户的信息安全。为了防止网站遭到恶意攻击,导致信息被窃取,所以浏览器设计了同源策略。
3.2 什么是同源策略
同源政策由 Netscape 公司引 入浏览器。同源策略是一个安全策略。所谓的同源,指的是协议,域名,端口相同。
浏览器出于安全方面的考虑,只允许本域名下的接口交互,不同源的客户端脚本,在没有明确授权的情况下,不能读写对方的资源。
Cookie
、LocalStorage
和IndexDB
无法读取DOM
和JS
对象无法获取- 限制
XMLHttpRequest
请求
3.3 如何解决跨域问题
3.3.1 JSONP
JSONP
实现跨域的原理
浏览器的同源策略限制不允许跨域请求;但页面中的 script
、img
、iframe
标签是例外不受同源策略限制。
Jsonp
就是利用script
标签跨域特性进行请求,通过 <script>
标签指向一个需要访问的地址并提供一个回调函数来接收数据。
思路:客户端事先准备一个接收数据的全局函数,之后客户端解析 script 标签发出请求。服务端接受到请求之后,返回函数的调用。客户端接收数据,执行回调。
假设JSONP请求如下:
jsonp({
url: 'http://path/to/server/b',
params: {A: a, B: b},
success: function myCallback (response) {}
})
背后其实在进行:
拼接一个script标签,
<script src="http://path/to/server/b?A=a&B=b&callback=myCallback"></script>
从而触发对指定地址的GET请求.
服务器端对这个GET请求进行处理,并返回字符串 “myCallback(‘response value’)”,前端script加载完之后,其实就是在script中执行 myCallback(‘response value’),就完成了跨域的请求,因此就是只能用GET。
JSONP 只能发 GET 请求,因为本质上 script 加载资源就是 GET。
优缺点
优点:兼容性好,可以解决主流浏览器的跨域数据访问的问题。
缺点:只能进行 GET
请求,具有局限性,不安全。
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script>
<script>
jsonp({
url: 'http://localhost:3000',
params: {
a: 1,
b: 2
}
}).then(data => {
// 拿到数据进行处理
console.log(data); // 数据包
})
</script>
const jsonp = ({ url, params, callbackName }) => {
const generateUrl = () => {
let dataSrc = ''
for (let key in params) {
if (params.hasOwnProperty(key)) {
dataSrc += `${key}=${params[key]}&`
}
}
dataSrc += `callback=${callbackName}`
return `${url}?${dataSrc}`
}
return new Promise((resolve, reject) => {
const scriptEle = document.createElement('script')
scriptEle.src = generateUrl()
document.body.appendChild(scriptEle)
window[callbackName] = data => {
resolve(data)
document.removeChild(scriptEle)
}
})
}
3.3.2 CORS
CORS
是 跨域资源共享。它允许浏览器向跨源服务器,发出跨域请求,整个CORS
通信过程都是浏览器自动完成的。使用自定义 HTTP
头部让浏览器与服务器进行沟通,从而决定请求或响应是成功还是失败。
两种请求方式
简单请求
浏览器直接发出 CORS
请求。具体来说,就是在头信息之中,增加一个Origin
字段。Origin
中会指出当前请求属于哪个域(协议+域名+端口),服务器会根据这个值决定是否允许其跨域。
- 请求方法是以下三种方法之一:
HEAD,GET,POST
- HTTP的头信息不超出以下几种字段:
Accept
,Accept-Language
,Content-Language
,Last-Event-ID
Content-Type
:只限于三个值application/x-www-form-urlencoded
、multipart/form-data
、text/plain
如果服务器允许跨域,需要在返回的响应头中携带下面信息:
Access-Control-Allow-Origin: http://manage.leyou.com
Access-Control-Allow-Credentials: true
跨域请求要想操作cookie,需要满足3个条件:
- 服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。
- 浏览器发起ajax需要指定withCredentials 为true
- 响应头中的Access-Control-Allow-Origin一定不能为*,必须是指定的域名。
高级请求
必须首先使用 OPTIONS
方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。服务器对 AJAX
跨域请求设置限制条件。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
非简单请求会发出一次预检测请求,返回码是204,预检测通过才会真正发出请求,这才返回200。这里通过前端发请求的时候增加一个额外的headers来触发非简单请求。
与简单请求相比,除了Origin以外,多了两个头:
Access-Control-Request-Method:接下来会用到的请求方式,比如PUT
Access-Control-Request-Headers:会额外用到的头信息
预检请求的响应
服务的收到预检请求,如果许可跨域,会发出响应:
除了Access-Control-Allow-Origin和Access-Control-Allow-Credentials以外,这里又额外多出3个头:
Access-Control-Allow-Methods:允许访问的方式
Access-Control-Allow-Headers:允许携带的头
Access-Control-Max-Age:本次许可的有效时长,单位是秒,过期之前的ajax请求就无需再次进行预检了
如果浏览器得到上述响应,则认定为可以跨域,后续就跟简单请求的处理是一样的了。
3.3.3 webpack
代理
通过 webpack
中的 proxy
进行代理,从而解决跨域的问题。
module.exports = {
//...
devServer: {
proxy: {
'/api': {
target: 'http://www.baidu.com/',
pathRewrite: { '^/api': '' },
changeOrigin: true, // target是域名的话,需要这个参数,
secure: false, // 设置支持https协议的代理
},
'/api2': {
.....
}
}
}
};
原理
在开发阶段,webpack-dev-server
会自动启动一个本地开发服务器,所以我们的应用在开发阶段是独立运行在 localhost
的一个端口上的,而后端服务器又是运行在另一个地址上。所以在开发阶段中,由于浏览器的同源策略,当本地访问的时候就会出现跨域资源请求的问题,通过设置 webpack proxy
实现代理请求后,相当于浏览器和服务器之间添加了一个代理商。当本地发送请求的时候,中间服务器会接受这个情求,并将这个请求转发给目标服务器,目标服务器返回数据后,中间服务器又会将数据返回给浏览器,当中间服务器将数据返回给服务器的时候,它们两者是同源的,并不会存在跨域的问题。服务器和服务器之间是不会存在跨域资源的问题的。利用 http-proxy-middleware
这个 http
代理中间件,实现请求转发给其他的服务器。如下:在开发阶段,本地地址是 Http://loaclhost:3000 , 该浏览器发送一个前缀带有 /api 标识的向拂去器请求数据,但是这个服务器只是将这个请求转发给另一台服务器:
const express = require('express');
const proxy = require('http-proxy-middleware');
const app = express();
app.use('/api', proxy({target: 'http://www.example.org', changeOrigin: true}));
app.listen(3000);
// http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar
3.3.4 Nginx
Nginx 是一种高性能的反向代理
服务器,可以用来轻松解决跨域问题。
正向代理帮助客户端访问客户端自己访问不到的服务器,然后将结果返回给客户端。
反向代理拿到客户端的请求,将请求转发给其他的服务器,主要的场景是维持服务器集群的负载均衡,换句话说,反向代理帮其它的服务器拿到请求,然后选择一个合适的服务器,将请求转交给它。
因此,两者的区别就很明显了,正向代理服务器是帮客户端做事情,而反向代理服务器是帮其它的服务器做事情。
好了,那 Nginx 是如何来解决跨域的呢?
比如说现在客户端的域名为client.com,服务器的域名为server.com,客户端向服务器发送 Ajax 请求,当然会跨域了,那这个时候让 Nginx 登场了,通过下面这个配置:
server {
listen 80;
server_name client.com;
location /api {
proxy_pass server.com;
}
}
Nginx 相当于起了一个跳板机,这个跳板机的域名也是client.com
,让客户端首先访问 client.com/api
,这当然没有跨域,然后 Nginx 服务器作为反向代理,将请求转发给server.com
,当响应返回时又将响应给到客户端,这就完成整个跨域请求的过程。
4. 浏览器实现多个标签页之间通信的几种方法
4.1 websocket
协议
4.2 setInterval
+cookie
在页面A设置一个使用 setInterval 定时器不断刷新,检查 Cookies 的值是否发生变化,如果变化就进行刷新的操作。由于 Cookies 是在同域可读的,所以在页面 B 审核的时候改变 Cookies 的值,页面 A 自然是可以拿到的。
4.2 监听 localstorage
localstorage是浏览器多个标签共用的存储空间,可以用来实现多标签之间的通信(ps:session是会话级的存储空间,每个标签页都是单独的)。
直接在window对象上添加监听即可:
window.onstorage = (e) => {
console.log(e)
}
onstorage以及storage事件,针对都是非当前页面对localStorage进行修改时才会触发,当前页面修改localStorage不会触发监听函数。然后就是在对原有的数据的值进行修改时才会触发,比如原本已经有一个key会a值为b的localStorage,你再执行:localStorage.setItem(‘a’, ‘b’)代码,同样是不会触发监听函数的。
4.3 使用 html5
浏览器的新特性 SharedWorker
5. 回流和重绘
5.1 回流
对 DOM 结构的修改引发 DOM 几何尺寸变化的时候,就会发生回流
5.1.1 触发回流的条件
-
DOM 元素的几何属性变化,如 width,height,padding,margin,left,top,border等
-
DOM 节点发生增减或者移动
-
读写 offset,scroll,client 相关属性的时候,浏览器为了获取这些值,需要进行回流操作
-
调用
window.getComputedStyle
方法
5.1.2 回流的过程
因为 DOM 结构发生了改变,需要重新从生成 DOM 这一步开始。需要经过样式计算,生成布局树,建立图层树,生成绘制列表等,性能消耗很大
5.2 重绘
当 DOM 的修改导致了样式的变化,并且没有影响到几何属性的时候,就会导致重绘
5.2.1 触发重绘的条件
color
,background-color
,visiable
5.2.2 重绘的过程
没有 DOM 几何属性的变化,元素的位置信息不需要更新,因此会跳过生成布局树,建立图层树的阶段,直接生成绘制列表。
5.3 如何避免触发回流和重绘
- 避免频繁使用 style,而是采用修改
class
的方式。 - 复杂的动画效果会频繁地触发回流重绘,让其脱离文档流。
- 也可以先为元素设置
display: none
,操作结束后再把它显示出来。因为在display
属性为none
的元素上进行的DOM操作不会引发回流和重绘 - 使用
createDocumentFragment
进行批量的 DOM 操作。 - 对于 resize、scroll 等进行防抖/节流处理。
- 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
- 利用
CSS3
的transform
、opacity
、filter
这些属性,开启GPU
加速。
6. 网络攻击
6.1 XSS
XSS
全称是 Cross Site Scripting
(即跨站脚本)。XSS
是指黑客往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。
这些操作一般可以完成下面这些事情:
- 窃取
Cookie
。 - 监听用户行为,比如输入账号密码后直接发送到黑客服务器。
- 修改 DOM 伪造登录表单。
- 在页面中生成浮窗广告。
6.1.1 XSS
类型
-
存储型:即攻击被存储在服务端,常见的是在评论区插入攻击脚本,如果脚本被储存到服务端,那么所有看见对应评论的用户都会受到攻击。
-
反射型:攻击者将脚本混在URL里,服务端接收到URL将恶意代码当做参数取出并拼接在HTML里返回,浏览器解析此HTML后即执行恶意代码。
http://sanyuan.com?q=<script>alert("你完蛋了")</script>
这杨,在服务器端会拿到
q
参数,然后将内容返回给浏览器端,浏览器将这些内容作为HTML的一部分解析,发现是一个脚本,直接执行,这样就被攻击了。之所以叫它
反射型
, 是因为恶意脚本是通过作为网络请求的参数,经过服务器,然后再反射到HTML文档中,执行解析。和存储型
不一样的是,服务器并不会存储这些恶意脚本。 -
DOM型:将攻击脚本写在URL中,诱导用户点击该URL,如果URL被解析,那么攻击脚本就会被运行。和前两者的差别主要在于DOM型攻击不经过服务端。
6.1.2 如何预防 XSS
- 输入检查:对输入内容中的
script
和<iframe>
等标签进行转义或者过滤 - 设置
httpOnly
:很多XSS
攻击目标都是窃取用户cookie
伪造身份认证,设置此属性可防止JS
获取cookie
- 开启
CSP
,即开启白名单(该安全策略的实现基于一个称作Content-Security-Policy
的 HTTP 首部。),可阻止白名单以外的资源加载和运行。- 限制其他域下的资源加载。
- 禁止向其它域提交数据。
- 提供上报机制,能帮助我们及时发现 XSS 攻击。
6.2 CSRF
CSRF
攻击(Cross-site request forgery)
跨站请求伪造。它利用用户已登录的身份,在用户毫不知情的情况下,以用户的名义完成非法操作。
6.2.1 如何触发 CSRF
-
在图片当中嵌入恶意网址,当用户访问图片时,浏览器会自动向恶意网址发起请求。这个请求会自动带上关于 xxx.com 的 cookie 信息(这里是假定你已经在 xxx.com 中登录过)。
假如服务器端没有相应的验证机制,它可能认为发请求的是一个正常的用户,因为携带了相应的 cookie,然后进行相应的各种操作,可以是转账汇款以及其他的恶意操作。
-
自动提交的表单,访问页面后,表单自动提交,相当于模拟用户完成一次 POST 操作,同时携带用户的 cookie 信息。让服务器误以为是一个正常的用户在操作,让各种恶意的操作变为可能。
-
诱导用户点击,发送请求,同时携带用户的 cookie 信息。
6.2.2 预防策略
对于 CSRF
攻击而言,黑客只能借助受害者的 cookie
骗取服务器的信任。但黑客无法知晓 cookie
的内容。另外对于服务器返回的信息,黑客也是无法解析的。
- 验证
Token
:浏览器请求服务器时,服务器返回一个token
,每个请求都需同时带上token
和cookie
才会被认为是合法请求 - 验证
Referer
:通过验证请求头的Referer
来验证来源站点,但请求头很容易伪造 - 设置
SameSite
:设置cookie
的SameSite
,可以让cookie
不随跨站请求发出,但浏览器兼容不一 - 服务端添加
X-Frame-Options
响应头:这个HTTP
响应头是为了防御用iframe
嵌套的点击劫持攻击。 这样浏览器就会阻止嵌入网页的渲染 - 敏感操作使用更复杂的步骤,如输入验证码
6.3 中间人攻击(MITM
)
6.3.1 什么中间人攻击
指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容。
概括:请求或响应在传输途中,遭攻击者拦截并篡改内容的攻击
中间人攻击是一个(缺乏)相互认证的攻击。占据两个参与者之间的通信通道是中间人攻击的核心。
大多数的加密协议都专门加入了一些特殊的认证方法以阻止中间人攻击。例如,SSL协议可以验证参与通讯的一方或双方使用的证书是否是由权威的受信任的数字证书认证机构颁发,并且能执行双向身份认证。
6.3.2 中间人攻击过程
客户端发送请求到服务端,请求被中间人截获。
服务器向客户端发送公钥。
中间人截获公钥,保留在自己手上。然后自己生成一个伪造的公钥,发给客户端。
客户端收到伪造的公钥后,生成加密hash值发给服务器。
中间人获得加密hash值,用自己的私钥解密获得真秘钥。同时生成假的加密hash值,发给服务器。
服务器用私钥解密获得假密钥。然后加密数据传输给客户端。
6.3.3 中间人攻击类型
Wi-Fi
欺骗,HTTPS
欺骗,SSL
劫持,DNS
欺骗,电子邮件劫持
6.3.4 如何防止中间人攻击
- 使用
HTTPS
- 不要忽略警告
- 不要使用公共
Wi-Fi
- 运行并更新防病毒软件
6.4 SQL
注入原理及防范
6.4.1 SQL
注入原理
通过把 SQL
命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的 SQL
命令。具体来说,它是利用现有应用程序,将(恶意)的SQL命令注入到后台数据库引擎执行的能力,它可以通过在Web表单中输入(恶意)SQL语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行SQL语句。
6.4.2 防止注入
- 永远不要信任用户的输入,要对用户的输入进行校验,可以通过正则表达式,或限制长度,对单引号和双"-"进行转换等。
- 永远不要使用动态拼装SQL,可以使用参数化的SQL或者直接使用存储过程进行数据查询存取。
- 永远不要使用管理员权限的数据库连接,为每个应用使用单独的权限有限的数据库连接。
- 不要把机密信息明文存放,请加密或者hash掉密码和敏感的信息。
- 应用的异常信息应该给出尽可能少的提示,最好使用自定义的错误信息对原始错误信息进行包装,把异常信息存放在独立的表中。
7. DDOS
攻击
7.1 概念
DDOS
攻击,它在短时间内发起大量请求,耗尽服务器的资源,无法响应正常的访问,造成网站实质下线。DDOS 里面的 DOS 是 denial of service(停止服务)的缩写,表示这种攻击的目的,就是使得服务中断。最前面的那个 D 是 distributed (分布式),表示攻击不是来自一个地方,而是来自四面八方,因此更难防。你关了前门,他从后门进来;你关了后门,他从窗口跳起来。
7.2 种类
DDOS
不是一种攻击,而是一大类攻击的总称。它有几十种类型,新的攻击方法还在不断发明出来。网站运行的各个环节,都可以是攻击目标。只要把一个环节攻破,使得整个流程跑不起来,就达到了瘫痪服务的目的。
比较常见的一种攻击是 cc 攻击。它就是简单粗暴地送来大量正常的请求,超出服务器的最大承受量,导致宕机。
7.3 防范
- 备份网站:生产服务器万一下线了,可以立刻切换到备份网站,不至于毫无办法。
- HTTP 请求的拦截:如果恶意请求有特征,对付起来很简单:直接拦截它就行了。
- 带宽扩容,
CDN
7. 提交表单为什么不存在跨域
因为原页面用 form 提交到另一个域名之后,原页面的脚本无法获取新页面中的内容。所以浏览器认为这是安全的。而 AJAX 是可以读取响应内容的,因此浏览器不允许这样操做。但其实请求已经发送出去了,只是拿不到响应而已。
所以浏览器这个策略的本质是,一个域名的 JS
,在未经允许的情况下,不得读取另一个域名的内容。但浏览器并不阻止你向另一个域名发送请求。
8. 外链 CSS
要放在头部,JS
要放在尾部
8.1 为什么外链 CSS
为什么要放头部
首先整个页面展示给用户会经过html
的解析与渲染过程。而**外链css无论放在html的任何位置都不影响html的解析,但是影响html的渲染。**如果将css放在尾部,html的内容可以第一时间显示出来,但是会阻塞html行内css的渲染。
浏览器的这个策略其实很明智的,想象一下,如果没有这个策略,页面首先会呈现出一个行内css样式,待CSS下载完之后又突然变了一个模样。用户体验可谓极差,而且渲染是有成本的。
如果将css放在头部,css的下载解析是可以和html的解析同步进行的,放到尾部,要花费额外时间来解析CSS,并且浏览器会先渲染出一个没有样式的页面,等CSS加载完后会再渲染成一个有样式的页面,页面会出现明显的闪动的现象。
8.2 为什么script要放在尾部
因为当浏览器解析到script的时候,就会立即下载执行,中断html的解析过程,如果外部脚本加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。
具体的流程是这样的:
- 浏览器一边下载HTML网页,一边开始解析。
- 解析过程中,发现script标签
- 暂停解析,网页渲染的控制权转交给JavaScript引擎
- 如果script标签引用了外部脚本,就下载该脚本,否则就直接执行
- 执行完毕,控制权交还渲染引擎,恢复往下解析HTML网页
8.3 外链的script包含async或者defer如何处理
这两个属性只是script标签在header标签中使用的,如果你把它放在body后面是无效的。
script 的这两个属性主要用于其js文件没有操作DOM的情况,这时候就可以将该js脚本设置为异步加载,通过async或defer来标记代码。
async和defer的区别:
async
和defer
都仅对外部脚本有效,对于内置而不是连接外部脚本的script标签,以及动态生成的script标签不起作用。async
和defer虽然都是异步的,不过使用async
标志的脚本文件一旦加载完成就会立即执行;而使用defer标记的脚本文件,会在DOMContentLoaded
事件之前(也就是页面DOM加载完成时)执行。- 如果有多个js脚本文件,
async
标记不保证按照书写的顺序执行,哪个脚本先下载结束,就先执行那个脚本。而defer标记则会按照js脚本书写顺序执行。 - 一般来说,如果脚本之间没有依赖关系,就使用
async
属性,如果脚本之间有依赖关系,就使用defer属性。如果同时使用async
和defer
属性,后者不起作用,浏览器行为由async
属性决定。
对于async标记,浏览器的解析过程是这样的:
- 浏览器开始解析HTML网页
- 解析过程中,发现带有async属性的script标签
- 浏览器继续往下解析HTML网页,同时并行下载script标签中的外部脚本
- 脚本下载完成,浏览器暂停解析HTML网页,开始执行下载的脚本
- 脚本执行完毕,浏览器恢复解析HTML网页
对于defer标记,浏览器的解析过程是这样的:
- 浏览器开始解析HTML网页
- 解析过程中,发现带有defer属性的script标签
- 浏览器继续往下解析HTML网页,同时并行下载script标签中的外部脚本
- 浏览器完成解析HTML网页,此时再执行下载的脚本
8.4 header中script和外链css的位置顺序
如果在html的header中同时有js脚本和外链css,js脚本最好放外链css前面。
其实js的执行是依赖css样式
的。即只有css样式全部下载完成后才会执行js。
因为如果脚本的内容是获取元素的样式,宽高等CSS控制的属性,浏览器是需要计算的,也就是依赖于CSS。浏览器无法感知脚本内容到底是什么,为避免样式获取错误,因而只好等前面所有的样式下载完后,再执行JS。
但是如果css下载事件很长的话,js也无法正常运行,导致html无法正常解析出来。如果css的内容下载更快的话,是没影响的,但反过来的话,JS就要等待了,然而这些等待的时间是完全不必要的。
js执行会阻塞DOM树的解析和渲染,css并不会阻塞DOM树的解析,css加载会阻塞DOM树渲染
9. ajax
是什么
可以在不重新加载整个网页的情况下,对网页的某部分进行更新。(重新拍照理解为重新加载网页)
直白地说,就是没用AJAX的网页,你点一个按钮就要刷新一下页面,尽管新页面上只有一行字和当前页面不一样,但你还是要无聊地等待页面刷新。
用了AJAX之后,你点击,然后页面上的一行字就变化了,页面本身不用刷。AJAX只是一种技术,不是某种具体的东西。不同的浏览器有自己实现AJAX的组件。
9.1 手写 ajax
// 1. 创建 ajax 对象
var xhr = null
try {
xhr = new XMLHttpRequest()
} catch (error) {
xhr = new ActiveXObject('error')
}
// 2. 等待数据响应
//必须在调用open()方法之前指定onreadystatechange事件处理程序才能确保跨域浏览器兼容性问题
//只要readyState属性的值有变化,就会触发readystatechange事件
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
//判断本次下载的状态码都是多少 304表示请求的资源没有被修改
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
alert(xhr.responseText)
} else {
alert('Error' + xhr.status)
}
}
}
// 3.调用 open
xhr.open('get', 'http://musicapi.leanapp.cn/music/url?id=1', true)
// 4. 调用 send
xhr.send()
9.2 手写 ajax Promise 版
function ajax(method, url) {
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.open(method, url);
request.onreadystatechange = () => {
if (request.readyState === 4) {
if (request.status === 200) {
resolve(request.response);
} else {
reject(request);
}
}
};
request.send();
});
}
ajax("get", "http://musicapi.leanapp.cn/music/url?id=1").then(response => {
console.log(response);
});
9.3 POST 请求
// 1. 创建 ajax 对象
var xhr = null
try {
xhr = new XMLHttpRequest()
} catch (error) {
xhr = new ActiveXObject('error')
}
// 2.等待数据响应
//必须在调用open()方法之前指定onreadystatechange事件处理程序才能确保跨域浏览器兼容性问题
//只要readyState属性的值有变化,就会触发readystatechange事件
xhr.onreadystatechange = function() {
if (xhr.readySate === 4) {
//判断本次下载的状态码都是多少 304表示请求的资源没有被修改
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
alert(xhr.responseText)
} else {
alert('Error' + xhr.status)
}
}
}
// 3.调用open
xhr.open('post', '1.post.php', true)
// 4.必须在send方法之前,去设置请求格式
xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded')
// 5.调用send
// post请求必须通过send方法进行提交
//username=yyy&age=19&password=123abc qureystring
//?username=yyy&age=19&password=123abc search
xhr.send('username=yyy&age=19&password=123abc')
9.4 封装 Get,Post 请求
/*
method
url
data
success 数据下载成功以后执行的函数
error 数据下载失败以后执行的函数
*/
function $ajax({ method = "get", url, data, success, error }) {
//1、创建ajax对象
var xhr = null;
try {
xhr = new XMLHttpRequest();
} catch (error) {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
//判断如果数据存在
if (data) {
data = querystring(data);
}
if (method == "get" && data) {
url += "?" + data;
}
xhr.open(method, url, true);
if (method == "get") {
xhr.send();
} else {
//必须在send方法之前,去设置请求的格式
xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded");
xhr.send(data);
}
//4、等待数据响应
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
//判断本次下载的状态码都是多少
if (xhr.status === 200) {
/*
如何去处理数据操作不确定
回调函数
*/
if (success) {
// alert(2);
success(xhr.responseText);
}
} else {
if (error) {
error("Error:" + xhr.status);
}
}
}
}
}
function querystring(obj) {
var str = "";
for (var attr in obj) {
str += attr + "=" + obj[attr] + "&";
}
return str.substring(0, str.length - 1);
}
// 调用方法
window.onload = function () {
var oGetBtn = document.getElementById("getBtn");
var oPostBtn = document.getElementById("postBtn");
//1、get请求
oGetBtn.onclick = function () {
// alert(1);
$ajax({
url: "1.get.php",
data: {
username: "xxx",
age: 19,
password: "123abc"
},
success: function (result) {
// alert(3);
alert("GET请求下载到的数据:" + result);
},
error: function (msg) {
alert(msg);
}
})
}
//2、post请求
oPostBtn.onclick = function () {
$ajax({
method: "post",
url: "2.post.php",
data: {
username: "xxx",
age: 19,
password: "123abc"
},
success: function (result) {
alert("POST请求下载到的数据:" + result);
},
error: function (msg) {
alert(msg);
}
})
}
}
Service Worker
1.概念
Service Worker是浏览器在后台独立于网页运行的脚本,它打开了通向不需要网页和用户交互的功能大门。 现在它已包括如推送通知和后台同步等功能。 将来,Service Worker将会支持如定期同步或地理围栏等其他功能。
我们平常浏览器窗口中跑的页面运行的是主JavaScript线程,DOM和window全局变量都是可以访问的。而Service Worker是走的另外的线程,可以理解为在浏览器背后默默运行的一个线程,脱离浏览器窗体,因此,window以及DOM都是不能访问的。
2. Service Worker 的能力
- 丰富的离线体验
- 消息推送通知
- 管理资源缓存
- 拦截资源请求
- 发起资源请求
3. Service Worker 的意义
Progressive Web Apps
,是一种能给 Web 带来令人惊叹的用户体验的新方式。里面提到三种最核心的用户体验。
- Reliable:即使在不确定的场景下(比如,网络不稳定或用户停留时间不确定)依然能提供可靠的Web服务。
- Fast:提供极速流畅的用户体验。
- Engaging:提供与
Native App
一致的用户体验。
4. PWA
PWA
是为了解决传统 Web APP
的缺点:
- 没有桌面入口
- 无法离线使用
- 没有 Push 推送
PWA
是前端的一个概念,Service Worker 为 这个概念赋予可能性
- 和
CacheStorage
搭配,可以做离线应用 - 和
Notification
搭配,可以做像Native APP
那样的消息推送 - 再加上
Manifest
等,就差不多成PWA
了
从输入 URL 到页面加载发生了什么
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xwnNePc7-1634000207909)(D:\360Downloads\note\qq46446F040FB4BFDE9357D3B0575C7CA2\bf16fa16dccd483f8b227e5a48677c96\clipboard.png)]
- 浏览器进程主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- 网络进程面向渲染进程和浏览器进程提供网络下载功能。
- 渲染进程的核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
- GPU 进程。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
- 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
2. 详细过程
-
用户输入信息并回车
-
根据用户输入的信息判断是内容还是网址。如果是内容,进行搜索。如果输入的内容符合
URL
规则,根据URL
协议,构成完整的URL
。 -
浏览器进程通过进程间通信(IPC)把
URL
发送给网络进程 -
网络进程接收到
URL
请求后检查本地缓存是否缓存了该请求资源,如果有,则将该资源返回给浏览器进程 -
如果没有,网络进程向 web 服务器发起网络请求,请求流程如下:
- 进行
DNS
解析,获取服务器ip
地址,端口 - 利用
ip
地址和服务器建立TCP
连接 - 构建请求头并发送
- 服务器响应后,网络进程接收响应头和响应信息,并解析响应内容
- 进行
-
网络进程解析响应流程
- 检查状态码,如果是 301/302 ,则需要重定向,从
Location
自动中读取地址,重新进行网络资源判断,如果是200,则继续处理请求。 - 200响应处理:检查响应类型
Content-Type
,如果是字节流类型,则将该请求提交给下载管理器,导航流程结束,不再进行后续的渲染,如果是HTML
则通知浏览器进程准备渲染进程准备进行渲染。
- 检查状态码,如果是 301/302 ,则需要重定向,从
-
准备渲染进程
- 浏览器进程检查当前
URL
和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,反之则开启新的渲染进程。
- 浏览器进程检查当前
-
传输数据、更新状态
- 渲染进程准备好后,浏览器向渲染进程发起“提交文档”的消息,渲染进程接收到消息和网络进程建立传输数据的管道
- 渲染进程接收完数据后,向浏览器发送“确认提交”
- 浏览器进程接收到确认消息后更新浏览器界面状态:安全、地址栏
URL
、前进后退的历史状态、更新web页面。
-
渲染进程开启
-
构建DOM树
**转换:**浏览器从磁盘或网络读取 HTML 的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成各个字符。
**令牌化:**浏览器将字符串转换成 W3C HTML5 标准规定的各种令牌,例如,“”、“”,以及其他尖括号内的字符串。每个令牌都具有特殊含义和一组规则。
**词法分析:**发出的令牌转换成定义其属性和规则的“对象”。
DOM构建: 最后,由于 HTML 标记定义不同标记之间的关系(一些标记包含在其他标记内),创建的对象链接在一个树数据结构内,此结构也会捕获原始标记中定义的父项-子项关系:HTML 对象是 body 对象的父项,body 是 paragraph 对象的父项,依此类推。
-
计算每个DOM节点具体样式
计算规则:
继承
和层叠
继承
:每个子节点会默认去继承父节点的样式,如果父节点中找不到,就会采用浏览器默认的样式,也叫UserAgent样式
。层叠
:样式层叠,是CSS
一个基本特征,它定义如何合并来自多个源的属性值的算法。在计算完样式之后,所有的样式值会被挂在到window.getComputedStyle
当中,也就是可以通过JS
来获取计算后的样式。这个阶段,完成了DOM节点中每个元素的具体样式,计算过程中要遵循CSS
的继承
和层叠
两条规则,最终输出的内容是每个节点DOM
的样式,被保存在ComputedStyle
中。 -
生成布局树
上述过程已经完成DOM树(DOM树)构建,以及样式计算(DOM样式),接下来就是要通过浏览器的布局系统确定元素位置,也就是生成一颗布局树(Layout Tree),之前说法叫 渲染树。在DOM树上不可见的元素,head元素,meta元素等,以及使用display:none属性的元素,最后都不会出现在布局树上,所以浏览器布局系统需要额外去构建一棵只包含可见元素布局树。
为了构建布局树,浏览器布局系统大体上完成了下面这些工作:
- 遍历DOM树可见节点,并把这些节点加到布局树中
- 对于不可见的节点,head,meta 标签等都会被忽略。对于 body.p.span 这个元素,它的属性包含display:none,所以这个元素没有被包含进布局树。
-
生成图层树
最终看到的页面,就是由这些图层一起叠加构成的,它们按照一定的顺序叠加在一起,就形成了最终的页面。**浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。**通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。
那什么情况下,渲染引擎会为特定的节点创建新图层呢?
有两种情况需要分别讨论,一种是显式合成,一种是隐式合成。
显式合成
一、 拥有层叠上下文的节点。
层叠上下文也基本上是有一些特定的CSS属性创建的,一般有以下情况:
- HTML根元素本身就具有层叠上下文。
- 普通元素设置position不为static并且设置了z-index属性,会产生层叠上下文。
- 元素的 opacity 值不是 1
- 元素的 transform 值不是 none
- 元素的 filter 值不是 none
- 元素的 isolation 值是isolate
- will-change指定的属性值为上面任意一个。
二、需要剪裁(clip)的地方。
比如一个标签很小,50*50像素,你在里面放了非常多的文字,那么超出的文字部分就需要被剪裁。当然如果出现了滚动条,那么滚动条也会被单独提升为一个图层。
元素有了层叠上下文的属性或者需要被剪裁,满足其中任意一点,就会被提升成为单独一层。
隐式合成通俗意义上来说,就是
z-index
比较低的节点会提升为一个单独的途图层,那么层叠等级比它高
的节点都会成为一个独立的图层。
5.绘制
图层的绘制跟我们日常的绘制一样,每次都会把一个复杂的图层拆分为很小的绘制指令,然后再按照这些指令的顺序组成一个绘制列表。绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。绘制图层的操作在渲染进程中有着专门的线程,这个线程叫做合成线程。
6.分块
- 接下来我们就要开始绘制操作了,实际上在渲染进程中绘制操作是由专门的线程来完成的,这个线程叫合成线程。
- 绘制列表准备好了之后,渲染进程的主线程会给
合成线程
发送commit
消息,把绘制列表提交给合成线程。
你想呀,有时候,你的图层很大,或者说你的页面需要使用滚动条,然后页面的内容太多,多的无法想象,这个时候需要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。
- 基于上面的原因,合成线程会讲图层划分为图块(tile)
- 这些块的大小一般不会特别大,通常是 256 * 256 或者 512 * 512 这个规格。这样可以大大加速页面的首屏展示。
首屏渲染加速可以这么理解:
因为后面图块(非视口内的图块)数据要进入
GPU
内存,考虑到浏览器内存上传到GPU
内存的操作比较慢,即使是绘制一部分图块,也可能会耗费大量时间。针对这个问题,Chrome 采用了一个策略: 在首次合成图块时只采用一个低分辨率的图片,这样首屏展示的时候只是展示出低分辨率的图片,这个时候继续进行合成操作,当正常的图块内容绘制完毕后,会将当前低分辨率的图块内容替换。这也是 Chrome 底层优化首屏加载速度的一个手段。7.光栅化
有了图块之后,合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。
- 图块是栅格化执行的最小单位
- 渲染进程中专门维护了一个栅格化线程池,专门负责把图块转换为位图数据
- 合成线程会选择视口附近的图块(tile),把它交给栅格化线程池生成位图
- 生成位图的过程实际上都会使用 GPU 进行加速,生成的位图最后发送给
合成线程
通常,栅格化过程都会使用
GPU
来加速生成,使用GPU
生成位图的过程叫快速栅格化,或者GPU
栅格化,生成的位图被保存在GPU
内存中。GPU
操作是运行在GPU
进程中,如果栅格化操作使用了GPU
,那么最终生成位图的操作是在GPU
中完成的。渲染进程把生成图块的指令发送给GPU
,然后在GPU
中执行生成图块的位图,并保存在GPU
的内存中。合成和显示
栅格化操作完成后,合成线程会生成一个绘制命令,即"DrawQuad",并发送给浏览器进程。
浏览器进程中的
viz组件
接收到这个命令,根据这个命令,把页面内容绘制到内存,也就是生成了页面,然后把这部分内存发送给显卡,显示器显示图像的原理解释:无论是 PC 显示器还是手机屏幕,都有一个固定的刷新频率,一般是 60 HZ,即 60 帧,也就是一秒更新 60 张图片,一张图片停留的时间约为 16.7 ms。而每次更新的图片都来自显卡的前缓冲区。而显卡接收到浏览器进程传来的页面后,会合成相应的图像,并将图像保存到后缓冲区,然后系统自动将
前缓冲区
和后缓冲区
对换位置,如此循环更新。这个时候,心中就有点概念了,比如某个动画大量占用内存时,浏览器生成图像的时候会变慢,图像传送给显卡就会不及时,而显示器还是以不变的频率刷新,因此会出现卡顿,也就是明显的掉帧现象。
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TU7e9Tvm-1634000207916)(https://segmentfault.com/img/bVHJfZ?w=624&h=289)]
3. 如何根据浏览器渲染机制加快首屏速度
- 优化文件大小:
HTML
和CSS
的加载和解析都会阻塞渲染树的生成,从而影响首屏展示速度,因此我们可以通过优化文件大小、减少CSS
文件层级的方法来加快首屏速度。 - 避免资源下载阻塞文档解析:浏览器解析到
<script>
标签时,会阻塞文档解析,直到脚本执行完成,因此我们通常把<script>
标签放在底部,或者加上defer、async
来进行异步下载。