# 浏览器知识总结
# 常见的浏览器内核有哪些?
主要记住:
chrome:webkit ~ Blink FileFox:Gecko(壁虎) ie:Trident(三叉戟)
# 浏览器的主要组成部分是什么?
- 用户界面 :除了浏览器主窗口显示的您请求的页面外
- 浏览器引擎:用户界面和呈现引擎之间传送指令
- 呈现引擎:负责显示请求的内容
- 网络:用于网络调用,比如 HTTP 请求
- 用户界面后端:用于绘制基本的窗口小部件
- JavaScript解释器:用于解析和执行 JavaScript 代码
- 数据存储:浏览器内数据库
# 浏览器是如何渲染UI的?
- 浏览器获取HTML文件,然后对文件进行解析,形成DOM Tree
- 与此同时,进行CSS解析,生成Style Rules
- 接着将DOM Tree与Style Rules合成为 Render Tree
- 接着进入布局(Layout)阶段,也就是为每个节点分配一个应出现在屏幕上的确切坐标
- 随后调用GPU进行绘制(Paint),遍历Render Tree的节点,并将元素呈现出来
# DOM Tree是如何构建的?
- 转码: 浏览器将接收到的二进制数据按照指定编码格式转化为HTML字符串
- 生成Tokens: 之后开始parser,浏览器会将HTML字符串解析成Tokens
- 构建Nodes: 对Node添加特定的属性,通过指针确定 Node 的父、子、兄弟关系和所属 treeScope
- 生成DOM Tree: 通过node包含的指针确定的关系构建出DOM Tree
# 能不能说一说浏览器的本地存储?各自优劣如何?
cookie: 为例弥补HTTP在状态管理上的不足。向同一个域名下发送请求,都会携带相同的
Cookie
,服务器拿到Cookie
进行解析,便能拿到客户端的状态。但是会有一系列的缺点容量缺陷:
Cookie
的体积上限只有4KB性能缺陷:不管域名下面的某一个地址需不需要这个 Cookie ,请求都会携带上完整的
Cookie
,请求携带了不必要的内容安全缺陷:
Cookie
以纯文本的形式在浏览器和服务器中传递,很容易被非法用户截获,然后进行一系列的篡改
localStorage:
容量。
localStorage
的容量上限为5M,相比于Cookie
的 4K 大大增加。当然这个 5M 是针对一个域名的,因此对于一个域名是持久存储的。只存在客户端,默认不参与与服务端的通信。这样就很好地避免了 Cookie 带来的性能问题和安全问题。
接口封装。通过
localStorage
暴露在全局,并通过它的 setItem 和 getItem等方法进行操作,非常方便。
sessionStroage:
sessionStorage
和localStorage
有一个本质的区别,那就是前者只是会话级别的存储,并不是持久化存储。会话结束,也就是页面关闭,这部分sessionStorage
就不复存在了。IndexedDB:
IndexedDB
是运行在浏览器中的非关系型数据库, 本质上是数据库,绝不是和刚才WebStorage的 5M 一个量级,理论上这个容量是没有上限的。- 键值对存储。内部采用对象仓库存放数据,在这个对象仓库中数据采用键值对的方式来存储。
- 异步操作。数据库的读写属于 I/O 操作, 浏览器中对异步 I/O 提供了支持。
- 受同源策略限制,即无法访问跨域的数据库。
# 能设置或读取子域的cookie吗
不行! 只能向当前域或者更高级域设置cookie
例如 client.com 不能向 a.client.com 设置cookie, 而 a.client.com 可以向 client.com 设置cookie
# 讲一下cookie的属性字段
- name字段:为一个cookie的名称。
- value字段:为一个cookie的值。
- domain字段:为可以访问此cookie的域名。
- path字段:为可以访问此cookie的页面路径。 比如domain是abc.com,path是/test,那么只有/test路径下的页面可以读取此cookie。
- expires/Max-Age字段:为此cookie超时时间。若设置其值为一个时间,那么当到达此时间后,此cookie失效。不设置的话默认值是Session,意思是cookie会和session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,此cookie失效。
- Size字段: 此cookie大小。
- http字段:cookie的httponly属性。若此属性为true,则只有在http请求头中会带有此cookie的信息,而不能通过document.cookie来访问此cookie。
- secure字段:设置是否只能通过https来传递此条cookie
# 重绘与回流
重绘:
当元素样式发生改变,但不影响布局时,浏览器将使用重绘进行元素更新,由于此时只需要UI层面的绘制,因此损耗较小
回流:
当元素尺寸、结构或者触发某些属性的时候,浏览器会重新渲染页面,这就叫回流。此时,浏览器需要重新计算,重新进行页面布局,所以损耗较大 一般有以下几种操作:
- 页面初次渲染
- 浏览器窗口大小改变
- 元素尺寸、位置、内容改变
- 元素字体大小改变
- 添加或删除可见的dom元素
- 触发CSS伪类,如:hover
- 查询某些属性或者调用某些方法
clientWidth, clientHeight, clientTop, clientLeft offsetWidth, offsetHeight, offsetTop, offsetLeft scrollWidth, scrollHeight, scrollTop, scrollLeft getComputedStyle() getBoundingClientRect() scrollTo()
回流必定触发重绘,重绘不一定触发回流,重绘代价小,回流代价大
# 如何避免重绘和回流
CSS:
- 避免使用table布局
- 尽可能再dom树的末端修改class
- 避免使用多层内联样式
- 将动画效果应用到position: absolute || fixed上
- 避免使用css表达式(例如calc)
- CSS3硬件加速(GPU加速)
JavaScript:
避免频繁操作样式,最好一次性修改style属性,或者将样式列表定义成class,并一次性更改class属性
避免频繁操作dom,创建一个documentFragment,在他上面应用所有的dom操作,最后再把他添加到文档中
也可以先为元素设置display: none,操作结束后再把它显示出来,因为再display为none的元素上进行dom操作不会引发重绘和回流
避免频繁读取会引发重绘回流的属性,如果需要多次使用,就用一个变量缓存起来
对具有复杂动画的元素使用绝对定位,使他脱离文档流,否则会引起父元素及后续元素频繁回流
使用cssText来更改样式
# 前端如何实现即时通讯
短轮询
短轮询的原理很简单,每隔一段时间客户端就发出一个请求,去获取服务器最新的数据。
- 优点:兼容性强,实现非常简单
- 缺点:延迟性高,非常消耗请求资源,影响性能
长轮询和长连接
comet
有两种主要实现手段,一种是基于 AJAX
的长轮询方式,另一种是基于 Iframe
及 htmlfile
的流方式,通常被叫做长连接。
长轮询优缺点:
- 优点:兼容性好,资源浪费较小
- 缺点:服务器
hold
连接会消耗资源,返回数据顺序无保证,难于管理维护
长连接优缺点:
- 优点:兼容性好,消息即时到达,不发无用请求
- 缺点:服务器维护长连接消耗资源
websocket
Websocket
是一个全新的、独立的协议,基于 TCP协议
,与 http协议
兼容、却不会融入http协议,仅仅作为html5的一部分,其作用就是在服务器和客户端之间建立实时的双向通信。
WebSocket 是一种网络通信协议,他可以让服务器将数据主动推送给客户端
- 优点:真正意义上的实时双向通信,性能好,低延迟
- 缺点:独立与http的协议,因此需要额外的项目改造,使用复杂度高,必须引入成熟的库,无法兼容低版本浏览器
SSE
SSE(Server-Sent Event,服务端推送事件)是一种允许服务端向客户端推送新数据的HTML5技术
- 优点:基于HTTP而生,因此不需要太多改造就能使用,使用方便,而websocket非常复杂,必须借助成熟的库或框架
- 缺点:基于文本传输效率没有websocket高,不是严格的双向通信,客户端向服务端发送请求无法复用之前的连接,需要重新发出独立的请求
# websocket的特点(重点:面试要考)
- 建立在 TCP 协议之上,服务器端的实现比较容易。
- 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
# 前端实现跨域
浏览器同源策略:
"协议+域名+端口"三者相同,不受同源限制的三个标签
<img src=XXX>
<link href=XXX>
<script src=XXX>
同源政策主要限制了三个方面
第一个是当前域下的 js 脚本不能够访问其他域下的 cookie、localStorage 和 indexDB。
第二个是当前域下的 js 脚本不能够操作访问其他域下的 DOM。
第三个是当前域下 ajax 无法发送跨域请求。
同源政策的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者 script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作。
跨域常见场景
jsonp
原理: 创建一个script标签, 再把需要请求的api地址放到src里. 这个请求只能用 GET
方法, 不可能是 POST
,所以容易遭到XSS攻击
为什么只能实现
get
如果看过
JSONP
库的源码就知道,常见的实现代码其实就是document.createElement(‘script’)
生成一个script
标签,然后插body
里而已。在这里根本没有设置请求格式的余地
JSONP和AJAX对比 JSONP 是非同源策略的AJAX
手写 JSONP 👍
JSONP函数一共需要三个参数: url,params,callback
- 声明一个挂在全局的函数,函数名为 callback,获取服务器的返回的
data
- 将
callback
和params
作为一个对象拼接参数 - 新建
script
标签,将src
设置为拼接好的参数,然后挂在到body
上
function JSONP({url,params,callback}){
return new Promise((resolve,reject)=>{
let script = document.createElement('script')
// 声明回调
window[callback] = function(data){
resolve(data)
document.body.removeChild(script)
}
// 拼接参数
params = {...params,callback}
let arr = []
for(let key in params){
arr.push(`${key}=${params[key]}`)
}
// 赋值src
script.src = `${url}?${arr.join('&')}`
document.body.appendChild(script)
})
}
// 使用实例
jsonp({
url: 'http://localhost:3000/say',
params: { wd: 'Iloveyou' },
callback: 'show'
}).then(data => {
console.log(data)
})
cors
cors 需要浏览器和后端同时支持,IE 8 和 9 需要通过 XDomainRequest
实现
cors 分为简单请求和非简单请求
简单请求
满足两大条件:
使用下列方法: GET、POST、HEAD 之一
Content-Type值权限于下列三者之一:
text/plain
、multipart/form-data
、application/x-www-form-urlencoded
简单请求实现跨域就是设置
Access-Control-Allow-Origin
非简单请求 不符合以上条件就是复杂请求了。复杂请求的跨域实现是通过
Options
方法对指定url发送嗅探请求,判断是否有权限访问
服务端设置 Access-Control-Allow-Origin
就可以开启 CORS
。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
Access-Control-Allow-Origin
:设置哪个源可以访问我Access-Control-Allow-Methods
:允许携带哪个方法访问我Access-Control-Allow-Headers
:允许携带哪个头访问我Access-Control-Allow-Credentials
:允许携带cookieAccess-Control-Max-Age
:预计的存活时间
//server2.js
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000'] //设置白名单
app.use(function(req, res, next) {
let origin = req.headers.origin
if (whitList.includes(origin)) {
// 设置哪个源可以访问我
res.setHeader('Access-Control-Allow-Origin', origin)
// 允许携带哪个头访问我
res.setHeader('Access-Control-Allow-Headers', 'name')
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 允许携带cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// 允许返回的头
res.setHeader('Access-Control-Expose-Headers', 'name')
if (req.method === 'OPTIONS') {
res.end() // OPTIONS请求不做任何处理
}
}
next()
})
app.put('/getData', function(req, res) {
console.log(req.headers)
res.setHeader('name', 'jw') //返回一个响应头,后台需设置
res.end('我不爱你')
})
app.get('/getData', function(req, res) {
console.log(req.headers)
res.end('我不爱你')
})
app.use(express.static(__dirname))
app.listen(4000)
nginx
反向代理的原理很简单,即所有客户端的请求都必须先经过nginx的处理,nginx作为代理服务器再讲请求转发给node或者java服务,这样就规避了同源策略。
实现思路: 通过nginx配置一个代理服务器(域名与 domain1
相同,端口不同)做跳板机,反向代理访问 domain2
接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录
就拿Vue项目举例,我当前的项目需要访问 https://novel.yangxiansheng.top
这个域的接口,但是项目部署之后访问的域名为 https://student-admin.yangxiansheng.top
,这显然是跨域的
这时我做如下操作即可实现跨域访问接口
- 配置 Vue 的 devServer,proxy 和 target
proxy: {
/**
* 首先部署一个Vue的项目如果需要联调本地的api时,但是跨域,这个时候可以使用proxy进行代理
*
* 如果需要上线,proxy就会变得无效的,这个时候首先proxy是不需要更改的,我们只需要做一步,那就是去更改nginx的location配置,将/api代理到开发环境的url即可、要知道https是无法请求非https的请求的,所以这一步很关键
*
*/
'/api': {
target: 'https://novel.yangxiansheng.top', // target表示代理的服务器url
// 这一步代表本地的/api会被代理到target+/下,也就是会被代理成线上的url
pathRewrite: {
'^/api': '/'
}
}}
- 配置nginx
server
{
listen 80;
listen 443 ssl http2;
server_name student-admin.yangxiansheng.top;
index index.php index.html index.htm default.php default.htm default.html;
root /www/wwwroot/student-admin.yangxiansheng.top/dist;
# 配置/api 代理到哪个路径,这里天上接口路径
location /api/ {
proxy_pass https://novel.yangxiansheng.top/;
}
}
另外还可以配置cookie写入
// proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}
最后重启nginx即可
WebSocket
WebSocket
是一种双向通信协议,在建立连接之后,WebSocket
的 server
与 client
都能主动向对方发送或接收数据,连接建立好了之后 client
与 server
之间的双向通信就与 HTTP
无关了,因此可以跨域。
原生WebSocket API使用起来不太方便,我们使用 Socket.io
,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容
// socket.html
<script>
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function () {
socket.send('我爱你');//向服务器发送数据
}
socket.onmessage = function (e) {
console.log(e.data);//接收服务器返回的数据
}
</script>
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//记得安装ws
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
ws.on('message', function (data) {
console.log(data);
ws.send('我不爱你')
});
})
postMessage()方法
HTML5 XMLHttpRequest
有一个API,postMessage()
,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
postMessage()
方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递
Node中间件代理(两次跨域)
实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。 代理服务器,需要做以下几个步骤:
- 将请求 转发给服务器。
- 接受客户端请求 。
- 拿到服务器 响应 数据。
- 将 响应 转发给客户端
# 输入URL发生了什么
加载过程
- URL解析,如果有非法字符,就转义
- 浏览器查找当前URL是否存在缓存,并比较缓存是否过期
- DNS解析域名,域名->IP地址
- 浏览器与服务器建立tcp链接(三次握手)
- 发送请求,分析url,设置请求头
- 服务器返回请求的文件(html)
渲染过程
- 根据
HTML
、Css
代码生成相应的DOMTree
,Style Rules
- 结合
DOMTree
和Style Rules
生成RenderTree
,然后将css
挂载在DOM
上 - 根据
RenderTree
渲染页面 - 页面遇到
<script></script>
标签停止渲染,执行完js
代码后再继续渲染 - layout布局渲染
- GPU像素绘制页面
- 直至渲染完成
可能会追问的问题
- 先说为什么url要解析(也就是编码)
因为网络标准规定了URL只能是字母和数字,还有一些其它特殊符号,如果不转义就会出现歧义,比如key=value,此时key就是=字符,就会出现歧义
- url编码的规则是什么呢
utf-8,中文的话用gb2312。浏览器通过 encodeURIComponent
统一编码格式
- encodeURIComponent 和 encodeURI有什么区别
区别就是 encodeURIComponent
编码范围更广,适合给参数编码,encodeURI
适合给URL本身(locaion.origin)编码,当然项目里一般都是用qs库去处理
dns 解析域名为ip地址全过程
前端dns优化手段
在html页面头部写入dns缓存地址
<meta http-equiv="x-dns-prefetch-control" content="on" />
<link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />
http三次握手
生成
DOMTree
的过程
- 浏览器将二进制编码转译为HTML字符串
- 根据
HTML
代码分析tag生成token,例如解析为startTag: p
等 - 对
Node
添加特定的属性,通过指针确定Node的子父关系以及treeScope
- 通过
Node
包含的指针关系确定DOMTree
# 渲染线程和 JS 引擎线程
浏览器中常见的线程有:渲染线程、JS 引擎线程、HTTP 线程等等。
例如,当我们打开一个 Ajax 请求的时候,就启动了一个 HTTP 线程。
同样地,我们可以用线程的只是解释:为什么直接操作 DOM 会变慢,性能损耗更大?因为 JS 引擎线程和渲染线程是互斥的。而直接操作 DOM 就会涉及到两个线程互斥之间的通信,所以开销更大。
除此之外,这还能解释为什么 <script>
标签为什么会阻塞 DOM 树渲染,毕竟 JS 是可以修改 DOM 的,如果 JS 执行的时候 UI 也工作,就有可能导致不安全的渲染。
# 页面生命周期
onload
和DOMContentLoaded
触发的先后顺序是什么?
页面声明周期的变化,会触发document
上的readystatechange
事件,用户可以通过document.readyState
拿到当前的状态。
// 初始时候的readyState
console.log(document.readyState);
// 每次改变都打印readyState
document.addEventListener("readystatechange", () =>
console.log(document.readyState);
);
上面的代码在 Chrome 中的输出是:
- loading:加载 document
- interactive:document 加载成功,DOM 树构建完成
- complete:图像,样式表和框架之类的子资源完成加载
所以,DOMContentLoaded
是在onload
前进行的。
【生命周期】:
DOMContentLoaded
事件在 DOM 树构建完毕后被触发,我们可以在这个阶段使用 js 去访问元素。async
和defer
的脚本可能还没有执行。- 图片及其他资源文件可能还在下载中。
load
事件在页面所有资源被加载完毕后触发,通常我们不会用到这个事件,因为我们不需要等那么久。beforeunload
在用户即将离开页面时触发,它返回一个字符串,浏览器会向用户展示并询问这个字符串以确定是否离开。
← 性能优化总结 计算机网络总结(附操作系统) →