谈谈浏览器
# 从输入 URL 到展示经历了什么
# 丐版
- DNS 解析
- TCP 连接
- HTTP 请求
- 服务器响应
- 客户端渲染
- 处理 HTML 标记并构建 DOM 树。
- 处理 CSS 标记并构建 CSSOM 树。
- 将 DOM 与 CSSOM 合并成一个渲染树。
- 根据渲染树来布局,计算每个节点的几何信息,并绘制到屏幕上。
# 详版
# 重绘与回流
当元素的样式发生变化时,浏览器需要触发更新,重新绘制元素。这个过程中有两种类型的操作,即重绘与回流。
重绘(repaint): 当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新,此时由于只需要UI层面的重新像素绘制,因此 损耗较少
回流(reflow): 当元素的尺寸、结构或触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,因此是较重的操作。
触发回流的操作:
- 页面初次渲染
- 浏览器窗口大小改变
- 元素尺寸、位置、内容发生改变
- 元素字体大小变化
- 添加或者删除可见的 dom 元素
- 激活 CSS 伪类(例如::hover)
- 查询某些属性或调用某些方法( 手动触发刷新队列 )
- clientWidth、clientHeight、clientTop、clientLeft
- offsetWidth、offsetHeight、offsetTop、offsetLeft
- scrollWidth、scrollHeight、scrollTop、scrollLeft
- getComputedStyle()
- getBoundingClientRect()
- scrollTo()
回流必定触发重绘,重绘不一定触发回流。重绘的开销较小,回流的代价较高。
# 最佳实践:
- css
- 避免使用
table
布局,table部分的改变会引起整个table的重绘 - 将动画效果应用到
position
属性为absolute
或fixed
的元素上
- 避免使用
- javascript
- 避免频繁操作样式,可汇总后统一 一次修改
- 尽量使用
class
进行样式修改 - 减少
dom
的增删次数,可使用 字符串 或者documentFragment
一次性插入 - 极限优化时,修改样式可将其
display: none
后修改 - 避免多次触发上面提到的那些会触发回流的方法,可以的话尽量用 变量存住
# http 缓存
缓存可以减少网络 IO 消耗,提高访问速度。
- 强缓存 200 (优先级从高到低分别是 Pragma -> Cache-Control -> Expires)
- Pragma:支持 http1.0;可设置 no-cache,表示不缓存资源;优先级最高
- Cache-Control:支持 http1.1;可设置 no-cache、max-age = 3600 (s);优先级中等
- Expires:支持 http1.0;可设置一个格林尼治时间;优先级最低
- 协商缓存 304
- ETag/If-None-Match 高
- Last-Modified/If-Modified-Since 低
总结:
- 首先通过
Cache-Control
验证强缓存是否可用,如果强缓存可用,直接使用 - 否则进入协商缓存,即发送 HTTP 请求,服务器通过请求头中的
Last-Modified
或者ETag
字段检查资源是否更新- 若资源更新,返回资源和200状态码
- 否则,返回304,告诉浏览器直接从缓存获取资源
# get 和 post
# 多标签通信
- window.open + window.postMessage
- LocalStorage + window.onstorage 监听
- cookie + 定时器轮询(setInterval)
- Websocket
- BroadCast Channel
- Service Worker
- Shared Worker + 定时器轮询(setInterval)
- IndexedDB + 定时器轮询(setInterval)
# 跨域
浏览器出于安全考虑,有同源策略。即 协议、域名、端口 有一个不同就是跨域,Ajax 请求可以发,但响应会被拦截。浏览器只允许跨域加载三个标签:img、link、script
- JSONP
- 利用
script
标签不受跨域限制的特点 - 缺点是只能支持 get 请求
- Step1: 创建 callback 方法
- Step2: 插入 script 标签
- Step3: 后台接受到请求,解析前端传过去的 callback 方法,返回该方法的调用,并且数据作为参数传入该方法
- Step4: 前端执行服务端返回的方法调用
- 利用
- 设置 CORS
- 设置 Access-Control-Allow-Origin:*
- 通过"预检"请求来知道服务端是否允许跨域请求,请求方法option
- 支持所有类型的HTTP请求
- 浏览器—CORS 通信
- postMessage
- nginx 代理跨域:同源策略对服务器不加限制
- iframe跨域
- websocket
# 存储
# cookie
- 4kb
- 服务端通过 Set-Cookie 设置
- 添加 http-only 属性,不能通过 js 操作 cookie,减少 xss
- 同源 -- 协议、域名、端口
# localStorage
- 5Mb
- 长期存在。模拟实现过期功能
- 同源 -- 协议、域名、端口
# sessionStorage
- 5Mb
- 会话级、tab 级别
- 同源 -- 协议、域名、端口、窗口
# indexDB
# web 安全
# 1、XSS
XSS 全称是跨站脚本攻击(Cross Site Scripting),是一种代码注入攻击。
分类:
- 存储型 => 攻击者通过把代码提交到后台数据库中;当用户下次打开的时候就会从后台接收这些恶意的代码
- 反射型 => 恶意链接,通过在请求地址上加入恶心的 HTML 代码
- dom 型 => 通过一些 api 向网站注入一些恶意的 HTML 代码
防范:
- 字符转义
- 禁止 JavaScript 操作 cookie,设置 httponly
- CSP 白名单
# 2、CSRF
CSRF 全称是跨站请求伪造(Cross-site request forgery),是一种利用 cookie 特性的攻击。
引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。
防范:
- 利用 Cookie 的 SameSite 属性
- 验证请求的来源站点
- token 验证
# 3、SQL 注入
用户输入的数据存在拼接 SQL 语句从而出现访问数据库的操作。
防范:过滤
# 4、点击劫持
在某些操作的按钮上加了一层透明的 iframe
防范:
- 服务端添加 X-Frame-Options 响应头,这个 HTTP 响应头是为了防御用 iframe 嵌套的点击劫持攻击。这样浏览器就会阻止嵌入网页的渲染。
- JS 判断顶层视口的域名是不是和本页面的域名一致,不一致则不允许操作,top.location.hostname === self.location.hostname;
- 敏感操作使用更复杂的步骤(验证码、输入项目名称以删除)。
# tcp http https
# tcp 传输控制协议
传输层协议;定义的是数据传输和连接方式的规范
建立连接:三次握手
- 客户机首先发出一个SYN消息
- 服务器使用SYN+ACK应答表示接收到了这个消息
- 最后客户机再以ACK消息响应
- SYN:同步序列编号(Synchronize Sequence Numbers)
- ACK:确认字符 (Acknowledge character)
- (为了方便描述我们将主动发起请求的172.16.50.72:65076 主机称为客户端,将返回数据的主机172.16.17.94:8080称为服务器。)
- 第一次握手: 建立连接。客户端发送连接请求,发送SYN报文,将seq设置为0。然后,客户端进入SYN_SEND状态,等待服务器的确认。
- 第二次握手: 服务器收到客户端的SYN报文段。需要对这个SYN报文段进行确认,发送ACK报文,将ack设置为1。同时,自己还要发送SYN请求信息,将seq为0。服务器端将上述所有信息一并发送给客户端,此时服务器进入SYN_RECV状态。
- 第三次握手: 客户端收到服务器的ACK和SYN报文后,进行确认,然后将ack设置为1,seq设置为1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
断开连接:四次挥手
- 客户端-发送一个FIN,用来关闭客户端到服务器的数据传送
- 服务器-收到这个FIN,它发回一个ACK,确认序号为收到的序号加1 。和SYN一样,一个FIN将占用一个序号
- 服务器-关闭与客户端的连接,发送一个FIN给客户端
- 客户端-发回ACK报文确认,并将确认序号设置为收到序号加1
- 第一次挥手:客户端向服务器发送一个FIN报文段,将设置seq为160和ack为112,;此时,客户端进入 FIN_WAIT_1状态,这表示客户端没有数据要发送服务器了,请求关闭连接;
- 第二次挥手:服务器收到了客户端发送的FIN报文段,向客户端回一个ACK报文段,ack设置为1,seq设置为112;服务器进入了CLOSE_WAIT状态,客户端收到服务器返回的ACK报文后,进入FIN_WAIT_2状态;
- 第三次挥手:服务器会观察自己是否还有数据没有发送给客户端,如果有,先把数据发送给客户端,再发送FIN报文;如果没有,那么服务器直接发送FIN报文给客户端。请求关闭连接,同时服务器进入LAST_ACK状态;
- 第四次挥手:客户端收到服务器发送的FIN报文段,向服务器发送ACK报文段,将seq设置为161,将ack设置为113,然后客户端进入TIME_WAIT状态;服务器收到客户端的ACK报文段以后,就关闭连接;此时,客户端等待2MSL后依然没有收到回复,则证明Server端已正常关闭,客户端也可以关闭连接了。
# http 80/https 443 超文本传送协议
- 应用层协议;定义的是传输数据的内容的规范
- 基于tcp;客户端每次发送请求都需要服务器返回响应,在请求结束后,会主动释放连接,从建立连接到关闭连接的过程称为“一次连接”,短连接,无状态
- 何为无状态?指浏览器每次向服务器发起请求的时候,不是通过一个连接,而是每次都建立一个新的连接。如果是一个连接的话,服务器进程中就能保持住这个连接并且在内存中记住一些信息状态。而每次请求结束后,连接就关闭,相关的内容就释放了,所以记不住任何状态,成为无状态连接。
- HTTPS是在应用层和传输层之间,增加了一个安全套接层SSL/TLS
- HTTP + 加密 + 认证 + 完整性保护 = HTTPS
- 协议格式:请求和响应都是 起始行、消息头和消息体
# Http1 和 http2 区别
http1.0 | http1.1 | http2 |
---|---|---|
短连接 | 持久连接 | 多路复用 |
默认不支持长连接 需要设置keep-alive参数指定 | 强缓存和协商缓存 | 首部压缩 |
可以进行断点续传 | 二进制格式编码传输 | |
服务端推送(server push) |
# 现代浏览器在与服务器建立了一个 TCP 连接后是否会在一个 HTTP 请求完成后断开?什么情况下会断开?
- 默认情况下建立 TCP 连接不会断开,只有在请求报头中声明 Connection: close 才会在请求完成后关闭连接。
- 在 HTTP/1.0 中,一个服务器在发送完一个 HTTP 响应后,会断开 TCP 链接。但是这样每次请求都会重新建立和断开 TCP 连接,代价过大。HTTP/1.1 就把 Connection 头写进标准,并且默认开启持久连接,除非请求中写明 Connection: close,那么浏览器和服务器之间是会维持一段时间的 TCP 连接,不会一个请求结束就断掉。
# 一个 TCP 连接可以对应几个 HTTP 请求?
- 如果是 HTTP 1.0 版本协议,一般情况下,不支持长连接,因此在每次请求发送完毕之后,TCP 连接即会断开,因此一个 TCP 发送一个 HTTP 请求,但是有一种情况可以将一条 TCP 连接保持在活跃状态,那就是通过 Connection 和 Keep-Alive 首部,在请求头带上 Connection: Keep-Alive,并且可以通过 Keep-Alive 通用首部中指定的,用逗号分隔的选项调节 keep-alive 的行为,如果客户端和服务端都支持,那么其实也可以发送多条
- 而如果是 HTTP 1.1 版本协议,支持了长连接,因此只要 TCP 连接不断开,便可以一直发送 HTTP 请求,持续不断,没有上限; 同样,如果是 HTTP 2.0 版本协议,支持多用复用,一个 TCP 连接是可以并发多个 HTTP 请求的,同样也是支持长连接,因此只要不断开 TCP 的连接,HTTP 请求数也是可以没有上限地持续发送
# 一个 TCP 连接中 HTTP 请求发送可以一起发送么(比如一起发三个请求,再三个响应一起接收)?
- 在 HTTP/1.1 存在 Pipelining 技术可以完成这个多个请求同时发送,但是由于浏览器默认关闭,所以可以认为这是不可行的。在 HTTP2 中由于 Multiplexing 特点的存在,多个 HTTP 请求可以在同一个 TCP 连接中并行进行。
# 浏览器对同一 Host 建立 TCP 连接到数量有没有限制?
- 有。Chrome 最多允许对同一个 Host 建立六个 TCP 连接。不同的浏览器有一些区别。
面试官问我:一个 TCP 连接可以发多少个 HTTP 请求?我竟然回答不上来..
# http 状态码
(注 : 🌶 为常用)
# 1xx(临时响应)
表示临时响应并需要请求者继续执行操作的状态码。
- 100 : 继续。 请求者应当继续提出请求。服务器返回此代码表示已收到请求的第一部分,正在等待其余部分。
- 101 : 切换协议。 请求者已要求服务器切换协议,服务器已确认并准备切换。
# 2xx(成功)
表示成功处理了请求的状态码。
- 200 :🌶 成功。 服务器已经成功处理了请求。通常,这表示服务器提供了请求的网页。
- 201 : 已创建。 请求成功并且服务器创建了新的资源
- 202 : 已接受。 服务器已接受请求,但尚未处理
- 203 : 非授权信息。 服务器已经成功处理了请求,但返回的信息可能来自另一来源
- 204 : 无内容。 服务器成功处理了请求,但没有返回任何内容
- 205 : 重置内容。 服务器成功处理了请求,但没有返回任何内容
- 206 : 部分内容。 服务器成功处理了部分 GET 请求
# 3xx(重定向)
表示要完成请求,需要进一步操作。通常,这些状态代码用来重定向。
- 300 : 多种选择。 针对请求,服务器可执行多种操作。服务器可根据请求者(user agent)选择一项操作,或提供操作列表供请求者选择。
- 301 :🌶 永久移动。 请求的网页已永久移动到新位置。服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
- 302 :🌶 临时移动。 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求
- 303 : 查看其它位置。 请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码
- 304 :🌶 协商缓存。 自动上次请求后,请求的网页未修改过。服务器返回此响应,不会返回网页的内容
- 305 : 使用代理。 请求者只能使用代理访问请求的网页。如果服务器返回此响应,还表示请求者应使用代理
- 307 : 临时性重定向。 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有的位置来进行以后的请求
# 4xx(请求错误)
这些状态码表示请求可能出错,妨碍了服务器的处理。
- 400 :🌶 错误请求。 服务器不理解请求的语法
- 401 :🌶 未授权。 请求要求身份验证。对于需要登录的网页,服务器可能返回此响应
- 403 :🌶 禁止。 服务器拒绝请求
- 404 :🌶 未找到。 服务器找不到请求的网页
- 405 : 方法禁用。 禁用请求中指定的方法
- 406 : 不接受。 无法使用请求的内容特性响应请求的网页
- 407 : 需要代理授权。 此状态码与 401(未授权)类似,但指定请求者应当授权使用代理
- 408 : 请求超时。 服务器等候请求时发生超时
- 409 : 冲突。 服务器在完成请求时发生冲突。服务器必须在响应中包含有关冲突的信息。
- 410 : 已删除。 如果请求的资源已永久删除,服务器就会返回此响应
- 411 : 需要有效长度。 服务器不接受不含有效内容长度标头字段的请求
- 412 : 未满足前提条件。 服务器未满足请求者在请求者设置的其中一个前提条件
- 413 : 请求实体过大。 服务器无法处理请求,因为请求实体过大,超出了服务器的处理能力
- 414 : 请求的 URI 过长。 请求的 URI(通常为网址)过长,服务器无法处理
- 415 : 不支持媒体类型。 请求的格式不受请求页面的支持
- 416 : 请求范围不符合要求。 如果页面无法提供请求的范围,则服务器会返回此状态码
- 417 : 未满足期望值。 服务器未满足“期望”请求标头字段的要求
# 5xx(服务器错误)
这些状态码表示服务器在尝试处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求出错。
- 500 :🌶 服务器内部错误。 服务器遇到错误,无法完成请求
- 501 : 尚未实施。 服务器不具备完成请求的功能。例如,服务器无法识别请求方法时可能会返回此代码
- 502 :🌶 错误网关。 服务器作为网关或代理,从上游服务器无法收到无效响应
- 503 : 服务器不可用。 服务器目前无法使用(由于超载或者停机维护)。通常,这只是暂时状态
- 504 : 网关超时。 服务器作为网关代理,但是没有及时从上游服务器收到请求
- 505 : HTTP 版本不受支持。 服务器不支持请求中所用的 HTTP 协议版本
# 移动端 jsBridge
JSBridge 是一种 JS 实现的 Bridge,连接着桥两端的 Native 和 H5。它在 APP 内方便地让 Native 调用 JS,JS 调用 Native ,是双向通信的通道。
# 常用功能
JSBridge中实现的通用功能
- 自定义titleBar
- 自定义titleBar上左右两侧按钮的功能及样式
- 打开一个新的webview来承接跳转的url
- 关闭自身webview
- 关闭前n个webview
- 监听resume、pause事件
- 下拉刷新
- app唤起
JSBridge中实现的业务功能
- 页面分享(微信、微博分享)
- 登录SDK页面呼启
- 支付功能
- 调用相机、图片上
H5 与 Native 对比
name | H5 | Native |
---|---|---|
稳定性 | 调用系统浏览器内核,稳定性较差 | 使用原生内核,更加稳定 |
灵活性 | 版本迭代快,上线灵活 | 迭代慢,需要应用商店审核,上线速度受限制 |
受网速 影响 | 较大 | 较小 |
流畅度 | 有时加载慢,给用户“卡顿”的感觉 | 加载速度快,更加流畅 |
用户体验 | 功能受浏览器限制,体验有时较差 | 原生系统 api 丰富,能实现的功能较多,体验较好 |
可移植性 | 兼容跨平台跨系统,如 PC 与 移动端,iOS 与 Android | 可移植性较低,对于 iOS 和 Android 需要维护两套代码 |
# 双向通信 --- JS 调用 Native
- 拦截 URL Scheme
- 注入 API
- 重写 prompt 等原生 JS 方法
# 拦截 URL Scheme
scheme协议是什么
- 可以简单理解为自定义的url
- 形式如:
[scheme:][//domain][path][?query][#fragment]
- 举个栗子:
jsbridge://openPage?url=https%3A%2F%2Fwww.baidu.com
约定固定格式的scheme协议,例如:[customscheme:][//methodName][?params={data, callback}]
- customscheme:自定义需要拦截的scheme
- methodName:需要调用的native的方法
- params:传递给native的参数 和 回调函数名
Android 和 iOS 都可以通过拦截 URL Scheme 并解析 Scheme 来决定是否进行对应的 Native 代码逻辑处理。
- Android 的话,
Webview
提供了shouldOverrideUrlLoading
方法来提供给 Native 拦截 H5 发送的URL Scheme
请求。 - iOS 的
WKWebview
可以根据拦截到的URL Scheme
和对应的参数执行相关的操作。
这种方法的优点是不存在漏洞问题、使用灵活,可以实现 H5 和 Native 页面的无缝切换。
例如在某一页面需要快速上线的情况下,先开发出 H5 页面。某一链接填写的是 H5 链接,在对应的 Native 页面开发完成前先跳转至 H5 页面,待 Native 页面开发完后再进行拦截,跳转至 Native 页面,此时 H5 的链接无需进行修改。
但是使用 iframe.src 来发送 URL Scheme
需要对 URL 的长度作控制,使用复杂,速度较慢。
- scheme的请求不要使用location.href
- 如果webview为对scheme进行拦截,很可能会出现webview报错现象,原因是webview把自定义的scheme协议当正常的url去加载了
- 解决办法是在页面上添加一个iframe,给iframe的src赋值为自定义scheme,这样也能发送这个scheme请求
# 注入 API
基于 Webview
提供的能力,我们可以向 Window 上注入对象或方法。JS 通过这个对象或方法进行调用时,执行对应的逻辑操作,可以直接调用 Native 的方法。使用该方式时,JS 需要等到 Native 执行完对应的逻辑后才能进行回调里面的操作。
- Android 的
Webview
提供了 addJavascriptInterface 方法,支持 Android 4.2 及以上系统。 - iOS 的
UIWebview
提供了 JavaScriptScore 方法,支持 iOS 7.0 及以上系统。WKWebview
提供了 window.webkit.messageHandlers 方法,支持 iOS 8.0 及以上系统。UIWebview
在几年前常用,目前已不常见。
# 重写 prompt 等原生 JS 方法
Android 4.2 之前注入对象的接口是 addJavascriptInterface ,但是由于安全原因慢慢不被使用。一般会通过修改浏览器的部分 Window 对象的方法来完成操作。主要是拦截 alert、confirm、prompt、console.log 四个方法,分别被
Webview
的 onJsAlert、onJsConfirm、onConsoleMessage、onJsPrompt 监听。iOS 由于安全机制,
WKWebView
对 alert、confirm、prompt 等方法做了拦截,如果通过此方式进行 Native 与 JS 交互,需要实现WKWebView
的三个WKUIDelegate
代理方法。
使用该方式时,可以与 Android 和 iOS 约定好使用传参的格式,这样 H5 可以无需识别客户端,传入不同参数直接调用 Native 即可。剩下的交给客户端自己去拦截相同的方法,识别相同的参数,进行自己的处理逻辑即可实现多端表现一致。
另外,如果能与 Native 确定好方法名、传参等调用的协议规范,这样其它格式的 prompt 等方法是不会被识别的,能起到隔离的作用。
# 双向通信 --- Native 调用 JS
H5 将 JS 方法暴露在 Window 上给 Native 调用即可
- Android 中主要有两种方式实现。在 4.4 以前,通过 loadUrl 方法,执行一段 JS 代码来实现。在 4.4 以后,可以使用 evaluateJavascript 方法实现。loadUrl 方法使用起来方便简洁,但是效率低无法获得返回结果且调用的时候会刷新 WebView。evaluateJavascript 方法效率高获取返回值方便,调用时候不刷新WebView,但是只支持 Android 4.4+。
- iOS 在
WKWebview
中可以通过 evaluateJavaScript:javaScriptString 来实现,支持 iOS 8.0 及以上系统。
# JSBridge 的使用
由 H5 引用
- 采用本地引入 npm 包的方式进行调用。这种方式可以确定 JSBridge 是存在的,可直接调用 Native 方法。但是如果后期 Bridge 的实现方式改变,双方需要做更多的兼容,维护成本高
由 Native 注入
- 这样有利于保持 API 与 Native 的一致性,但是缺点是在 Native 注入的方法和时机都受限,JS 调用 Native 之前需要先判断 JSBridge 是否注入成功