# Html

# 1. 输入URL到展示一个网页的过程?

# 1.1 查找缓存

  1. 浏览器进程通过进程间通信,把URL请求发送给网络进程。

  2. 网络进程首先寻找缓存:浏览器缓存->系统缓存->路由器缓存

  3. 在这里浏览器缓存有两种:

    1. 强缓存:
      • expires:本地过期时间
      • cache-control:优先级高,相对时间
    2. 协商缓存:
      • Last-Modified/If-Modified-Since:但是本地修改了也会改
      • Etag/If-None-Match:发给服务器去判断是否修改了

# 1.2 发送网络请求

  1. 根据主机名查找IP,操作系统先检查自己本地的hosts文件是否有这个网址的映射。没有则DNS域名解析得到对应的IP地址,本地DNS服务器(ISP)-> 根域名服务器 -> 顶级域名。递归和迭代查询
  2. 如果是HTTPS协议:443,SSL(安全套接字层),协商密钥、证书认证、数据传输。
  3. 三次握手,SYN同步,ACK表示收到。欢迎套接字,连接套接字。并且层层封装头部。
  4. 代理服务器,如果有的话就从代理服务器返回,没有就需要访问原服务器,并且代理服务器自己也会拷贝一份保存。
  5. 最后四次分手,FIN表示断开连接。
  6. 浏览器收到服务器返回的响应头之后,如果返回的状态码是301/302,还需要重定向到其他的URL,重新发起网络请求。

# 1.3 浏览器处理html

  1. 构建DOM树(根据html文件):

    1.1 词法分析:分词器将字节流(Bytes)转换为一个个Token。

    1.2 将Token解析为DOM节点,并将DOM节点添加到DOM树中。

  2. 样式计算(根据css文件):

    2.1 CSS文件转化为浏览器可以理解的styleSheets结构(这就是CSSOM)。

    2.2 转换样式表中的属性值em/rem/px/vw等,使其标准化

    2.3 根据CSS的继承规则和层叠规则,计算DOM树中每个节点的具体样式,得到CSSOM。

  3. 布局阶段:

    3.1 DOM + CSSOM => Render Tree

    3.2 Render Tree(渲染树)中的所有可见节点,把这些节点添加到布局树中。

    3.3 布局计算,将单位标准化,然后计算各个节点的坐标位置。

  4. 渲染阶段:

    4.1 进行页面的分层(构建图层树)

    4.2 根据绘制列表,图层绘制

    4.3 栅格化(raster)操作:合成线程会将图层划分为图块,将图块转换为位图。通常会使用GPU来加速生成。

    4.4 浏览器进程绘制图块,将页面内容绘制到内存中,然后显示在屏幕上。

# 2. DOM

# 2.1 什么是DOM

  • 从网络传给渲染引擎的 HTML 文件字节流是无法直接被渲染引擎理解的,所以要将其转化为渲染引擎能够理解的内部结构,这个结构就是 DOM。DOM 提供了对 HTML 文档结构化的表述。
  • DOM是表述HTML的内部数据结构,它会将Web页面和JavaScript脚本连接起来,并过滤一些不安全的内容。

# 2.2 DOM的作用

  • 从页面的视角来看,DOM 是生成页面的基础数据结构。
  • 从 JavaScript 脚本视角来看,DOM 提供给 JavaScript 脚本操作的接口,通过这套接口,JavaScript 可以对 DOM 结构进行访问,从而改变文档的结构、样式和内容。
  • 从安全视角来看,DOM 是一道安全防护线,一些不安全的内容在 DOM 解析阶段就被拒之门外了。

# 3.2 DOM树的构建

  1. 词法分析:分词器将字节流(Bytes)转换为一个个Token:
    • Tag Token(又分为StartTag和EndTag)
    • 文本Token。
  2. 将Token解析为DOM节点,并将DOM节点添加到DOM树中,整个解析过程持续下去,直到分词器将所有字节流分词完成:
    • 如果压入到栈中的是StartTag Token,HTML 解析器会为该 Token 创建一个 DOM 节点,然后将该节点加入到 DOM 树中,它的父节点就是栈中相邻的那个元素生成的节点。
    • 如果分词器解析出来是文本 Token,那么会生成一个文本节点,然后将该节点加入到 DOM 树中,文本 Token 是不需要压入到栈中,它的父节点就是当前栈顶 Token 所对应的 DOM 节点。
    • 如果分词器解析出来的是EndTag 标签,比如是 EndTag div,HTML 解析器会查看 Token 栈顶的元素是否是 StarTag div,如果是,就将 StartTag div 从栈中弹出,表示该 div 元素解析完成。
  3. 如果执行到内嵌的JavaScript代码(script标签),则阻塞整个DOM的解析,先下载JavaScript代码(如果是引用的js文件),然后执行JavaScript代码。不过Chrome有个预解析的优化:当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件。
  4. 浏览器显示下载信息:DOMContentLoaded,说明页面已经构建好 DOM了。

# 3. CSSOM

# 3.1 什么是CSSOM

  • 由于渲染引擎也是无法直接理解 CSS 文件内容的,所以需要将其解析成渲染引擎能够理解的结构,这个结构就是 CSSOM。
  • CSSOM不是布局树,布局树是DOM树过滤掉诸如display:none的元素、head标签、script标签之后的结构。

# 3.2 CSSOM的作用

  • 提供给 JavaScript 操作样式表的能力
  • 为布局树的合成提供基础的样式信息

# 3.3 渲染流水线图示

image

  • 在接收到 HTML 数据之后的预解析过程中,HTML 预解析器识别出来了有 CSS 文件和 JavaScript 文件需要下载,然后就同时发起这两个文件的下载请求。

    需要注意的是,这两个文件的下载过程是重叠的,所以下载时间按照最久的那个文件来算。

  • 不管 CSS 文件和 JavaScript 文件谁先到达,都要先等到 CSS 文件下载完成并生成 CSSOM

  • 执行 JavaScript 脚本,最后再继续构建 DOM,构建渲染树。

  • 根据渲染树,布局计算,绘制页面。

# 3.4 渲染流水线后续步骤(分层与合成机制)

  1. 生成了渲染树之后,渲染引擎会根据渲染树 -> 转化 -> 层树
  2. 分层阶段:层树中的每个节点都对应一个图层
  3. 绘制阶段:将绘制指令组合成绘制列表:比如,背景黑色 -> 中间画圆 -> 圆心白色
  4. 光栅化阶段:按照绘制列表中的指令生成图片,每个图层对应一张图片
  5. 合成阶段:将图片们合成在一起,发送到显卡的后缓冲区,等待系统调用显示

合成操作是在合成线程上完成的,所以在执行合成操作时,是不会影响到主线程执行的。这就是为什么经常主线程卡住了,但是 CSS 动画依然能执行的原因。

# 3.5 渲染完毕后触发load事件

  • 当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片。 (譬如如果有async加载的脚本就不一定完成)
  • (load事件)当 onload 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了。 (渲染完毕了)

# 4. 浏览器将一个页面展示出来的大概流程

  1. 得到请求的html文件

    1.1 HTML解析器 -> 词法分析 -> Token -> 构建DOM节点 -> 构建DOM树

    1.2 (同时)预解析 -> 网络请求进程提前异步下载CSS/JS文件

  2. 渲染进程 + CSS文件 -> 构建CSSOM -> 属性值标准化(vw/vh/em/rem) -> 根据继承和层叠规则计算具体样式

  3. DOM + CSSOM -> 渲染树 -> 层树 -> 分层 -> 绘制指令组合为绘制列表 -> 根据绘制列表生成图片(光栅化阶段)-> 合成图片 -> 发送到后缓冲区,等待显示

# 5. Virtual DOM

# 5.1 主要思想

  • Virtual DOM的真正价值不完全在于性能,而是它:
    • 为函数式的 UI 编程方式打开了大门;
    • 可以渲染到 DOM 以外的 backend,比如 ReactNative。
  • Vritual DOM上定义了关于真实DOM的一些关键的信息,Vritual DOM完全是用JS去实现,和宿主浏览器没有任何联系,此外得益于js的执行速度。在JS层执行DOM的比对,减少了浏览器不必要的重绘,提高效率
    • 比较一下原生与Virtual DOM的重绘性能消耗:
      • innerHTML: render html string O(template size) + 重新创建所有 DOM 元素 O(DOM size)
      • Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)
      • 后者才是占比巨大的部分
  • 模拟DOM的树状结构,在内存中创建保存映射DOM信息的节点数据
  • 在由于交互等因素需要视图更新时,先通过对节点数据进行diff后得到差异结果后,再一次性对DOM进行批量更新操作,

# 5.2 特点

  • 将页面改变的内容应用到虚拟 DOM 上,而不是直接应用到 DOM 上。
  • 变化被应用到虚拟 DOM 上时,虚拟 DOM 并不急着去渲染页面,而仅仅是调整虚拟 DOM 的内部状态,这样操作虚拟 DOM 的代价就变得非常轻了。
  • 在虚拟 DOM 收集到足够的改变时,再把这些变化一次性应用到真实的 DOM 上。
  • 可以把虚拟 DOM 看成是 DOM 的一个 buffer,和图形显示一样,它会在完成一次完整的操作之后,再把结果应用到 DOM 上,这样就能减少一些不必要的更新,同时还能保证 DOM 的稳定输出。

# 5.3 Diff 算法

# 5.3.1 概念

  • 方式:只对同一层级的节点进行比较,而非对树进行层次搜索的遍历。
  • 作用:如何识别并保存新旧节点数据结构之间差异的方法

# 5.3.2 React实现方式

  • React:reconcile:受限于Fiber的单向结构采用按顺序直接替换的方式更新,但React优化的组件设计与Fiber的工作线程机制在整体渲染性能方面带来了效率提升
  • 策略:
    • tree diff:Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
    • component diff:拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
    • element diff:对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。

# 5.3.3 Vue实现方式

  • Vue:snabbdom:双端对比算法,使用了两端同时对比以及根据位置顺序进行移动的更新策略
    • 两个主要的函数:
      • sameVNode():tag、key、isComment是否都相同,若相同,则说明可以进行patchVNode操作
      • patchVNode()
        • 如果 newVNode / oldVNode 都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),那么只需要替换elm以及componentInstance即可。
        • 如果 newVNode / oldVNode 均有children子节点,则对子节点进行diff操作,调用updateChildren,这个updateChildren也是diff的核心。
        • 如果 oldVNode 没有子节点而 newVNode 存在子节点,先清空 oldVNode 的DOM的文本内容,然后为当前DOM节点加入子节点。
        • 如果 newVNode 没有子节点而 oldVNode 有子节点的时候,则移除该DOM节点的所有子节点。
        • 如果 newVNode / oldVNode 都无子节点的时候,只是文本的替换。
    • 双端对比:首先会对newVNode / oldVNode 的开始和结束位置进行标记:oldStartIdx / oldEndIdx、newStartIdx /newEndIdx
      • 其实简单一点说,就是两个数组,各自设置一个头尾指针,若数组old的某个指针 === 数组new的某个指针,则将该元素添加到新的数组中,然后对应的指针进行 ++ 或者 -- 操作。
      • 如果无法匹配,则直接将 new 中的元素加入到新的数组中
      • 举例:
        • old:A B C D
        • new:D C E A B F
        • diff:D C E A B F
      • 参见:Vue的diff算法
    • key:key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、快速。可以用来标记变化的节点
      • 更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
      • 更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快

# 5.4 优缺点

  • 优点:
    • 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
    • 无需手动操作 DOM:我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
    • 跨平台:虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。
  • 缺点:
  • 无法进行极致优化:虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。

# 6.跨域

# 6.1 概念

  • 两个URL的协议、域名、端口都相同,则称这两个URL同源。
  • 浏览器默认两个相同的源之间是可以相互访问资源和操作 DOM 的。
  • 两个不同的源之间若想要相互访问资源或者操作 DOM,那么会有一套基础的安全策略的制约,即同源策略 SOP(Same origin policy)
  • 具体来讲,同源策略主要表现在 DOM、Web 数据和网络这三个层面。
    • 第一个,DOM 层面。同源策略限制了来自不同源的 JavaScript 脚本对当前 DOM 对象读和写的操作。
    • 第二个,数据层面。同源策略限制了不同源的站点读取当前站点的 Cookie、IndexDB、LocalStorage 等数据。由于同源策略,我们依然无法通过第二个页面的 opener 来访问第一个页面中的 Cookie、IndexDB 或者 LocalStorage 等内容。
    • 第三个,网络层面。同源策略限制了通过 XMLHttpRequest 等方式将站点的数据发送给不同源的站点。
  • 同源策略是为了缓解XSS、CSFR等攻击:
    • XSS:Cross Site Scripting(跨站脚本),指黑客往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。浏览器无法区分脚本是不是恶意的。
    • CSRF: Cross-site request forgery(跨站请求伪造),指黑客引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。
  • 跨域:一个域下的文档或脚本,试图去请求另一个域下的资源。

# 6.2 跨域解决方案

  • jsonp
  • location.hash + iframe
  • CORS( Cross-origin resource sharing 跨域资源共享)
  • nginx反向代理
  • WebSocket协议

# 6.2.1 jsonp

  • 通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信
  • 关键点:script标签可以进行跨域请求。
  • 缺点:只能实现get一种请求。
  1. 原生实现

    // 原生
    function jsonp(url, data, callback) {
      let dataStr = '?';
      for (let key in data) {
        dataStr += `${key}=${data[key]}&`
      }
      dataStr += 'callback=' + callback.name;
    
      let script = document.createElement('script');
      script.src = url + dataStr;
      document.head.appendChild(script);
    }
    
  2. jquery ajax

    $.ajax({
        url: 'http://www.domain2.com:8080/login',
        type: 'get',
        dataType: 'jsonp',  // 请求方式为jsonp
        jsonpCallback: "handleCallback",    // 自定义回调函数名
        data: {}
    });
    
  3. vue.js

    this.$http.jsonp('http://www.domain2.com:8080/login', {
        params: {},
        jsonp: 'handleCallback'
    }).then((res) => {
        console.log(res); 
    })
    
  4. nodejs的后端实现

    var querystring = require('querystring');
    var http = require('http');
    var server = http.creatServer();
    
    server.on('request', function(req, res) {
      var params = qs.parse(req.url.split('?')[1]);
      var fn = params.callback;
      
      res.writeHead(200, {'Content-Type': 'text/javascript'});
      res.write(fn + '(' + HSON.stringify(params) + ')');
      
      res.end()
    });
    
    server.listen('8080');
    console.log('Server is running at port 8080...');
    

# 6.2.2 document.domain + iframe

  • 应用场景:主域相同,子域不同
  • 实现方式:两个页面都通过js强制设置document.domain为基础主域,实现了同域
// 父窗口 http://www.domain.com/a.html
<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
  document.domain = 'domain.com';
  var user = 'admin';
</script>
// 子窗口 http://child.domain.com/b.html
<script>
  document.domain = 'domain.com';
  alert('get js data from parent ---> ' + window.parent.user);
</script>

# 6.2.3 location.hash + iframe

  • 三个网站,domain1/a.html、domain2/b.html、domain1/c.html
  • a.html 中嵌入iframe b.html,b.html 中嵌入 iframe c.html
  • 由于 a.html 和 c.html 同域,所以 c.html 可以通过 parent.parent 来访问 a.html 的所有对象
  • a.html 通过改变其 iframe 的 src 属性,可以向 b.html 传递 location.hash 值,然后 b.html 监听到其 location.hash 发生变化,将其传递给 iframe 的 src,即 c.html 也收到了这个属性,c.html 通过访问 a.html 中开放的接口/方法,让 a.html 也收到这个消息。
  • 至此,b.html -> iframe -> c.html -> same domain -> a.html,实现了跨域通信

# 6.2.4 window.name + iframe

  • window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值。
  • domain1/a.html 想要跨域访问 domain2/b.html
  • a.html 生成一个 iframe,其 src 指向 b.html,b.html修改其 window.name 属性,将需要传递给 a.html 的数据放在 window.name 中,然后 a.html 从这个 iframe 中取出 window.name 的属性值,然后销毁掉 iframe 标签/节点。

# 6.2.5 postMessage

  • domain1/a.html 嵌套了 iframe domain2/b.html
  • a.html 以 iframe 入口,通过iframe.content.postMessage(),向 b.html 跨域传送数据。
  • b.html 通过 window.parent.postMessage()返回数据。

# 6.2.6 CORS( Cross-origin resource sharing 跨域资源共享)

// 服务端
// 服务端设置Access-Control-Allow-Origin即可
res.writeHead(200, {
  'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
  'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
});

// 前端
// 前端设置是否带cookie
xhr.withCredentials = true;

# 6.2.7 nginx反向代理

反向代理服务器位于源服务器前面,用户的请求需要先经过反向代理服务器才能到达源服务器,反向代理服务器可以用来进行缓存,日志记录,负载均衡服务器。在负载均衡模式下,客户端不会直接请求源服务器,因为源服务器不需要外部地址,而反向代理需要配置内部和外部两套IP地址。

# 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;
    }
}

# image

# 6.2.8 nodejs中间件代理

原理与nginx大致相同,通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

# 6.2.9 WebSocket协议

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。

<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
    // 监听服务端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 监听服务端关闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>

# 6.3 淘宝中,访问天猫的数据,跨域的问题

  • 个人理解的大概的过程:

    1. 登陆淘宝,拿到淘宝的cookie
    2. 访问天猫页面:用淘宝的cookie + query中带上 tbpm=1,302跳转到jump
    3. 302跳转到add,服务端把cookie拼接到了跳转的url的query中
    4. set-cookie,相当于这次的cookie是tmall域名给浏览器设置的
    5. 这个时候应该基本就已经实现了cookie跨域共享
  • 图释:

    • image
    1. 访问原始url: tmcc.tmall.com
    2. 重定向,访问login.taobao.com/jump
    3. 重定向,访问pass.tmall.com
    4. 重定向,访问原始url(带有同步标识tbpm)
    5. 重定向,访问原始url(去掉同步标识tbpm)
  • 参见:[Taobao SSO 跨域登录过程解析](

# 7. html5新特性

# 7.1 新增元素 (语义化标签)

  • <header></header> 头部区域标签,块级标签
  • <footer></footer> 底部区域标签,块级标签
  • <nav></nav> 导航区域标签,块级标签
  • <time></time> 时间区域标签,内联标签
  • <article></article> 文章段落标签,块级标签
  • <aside></aside> 侧边栏区域标签,块级标签
  • <mark></mark> 标记记号标签,内联标签
  • <summary></summary> 单词翻译: 摘要,h5官方文档描述:定义 details 元素的标题,块级标签
  • <detailes></detailes> 单词翻译:细节,h5官方文档描述:定义元素的细节,块级标签
  • <section></section> 单词翻译:部分,h5官方文档描述:定义 section,块级标签

# 7.2 废除元素

  • 纯表现元素:font/center/u 用css代替
  • 部分浏览器支持的元素:applet/bgsound/blink
  • 对可用性产生负面影响的元素:frameset/frame/noframes,在html5中不支持 frame 框架,只支持 iframe 框架

# 7.3 其他新增标签

  • 表单类型:
    • <input type="email" /> e-mail 地址的输入域
    • <input type="number" /> 数字输入域
    • <input type="url" /> URL 地址的输入域
    • <input type="search" /> 用于搜索域
    • <input type="color" /> 用于定义选择颜色
    • <input type="tel" /> 电话号码输入域
    • <input type="date" /> date类型为时间选择器
  • 视频和音频:
    • <video></video>
    • <audio></audio>
  • Canvas绘图:<canvas></canvas>
  • SVG矢量绘图:<svg></svg>
  • 拖放属性:<div id="draggable" class="draggable" draggable="true">
  • 地理定位:getCurrentPosition()
  • Web存储:
    • localStorage
    • sessionStorage
  • 离线存储:在文档的<html>标签中包含 manifest 属性,每个指定了 manifest 的页面在用户对其访问时都会被缓存。

# 7.4 WebSocket

# 7.4.1 特点

  • 一个双向通信协议,全双工,解决了HTTP协议中服务器不能主动联系客户端的缺陷。
  • 握手阶段采用HTTP协议,默认端口80和443
  • 建立在TCP协议基础之上,WebSocket协议属于应用层
  • 可以发送文本,也可以发送二进制数据
  • 没有同源限制,客户端可以与任意服务器通信
  • 协议标识符为ws,如果加密则为wss

# 7.4.2 建立连接

  1. 客户端:申请协议升级

    GET / HTTP/1.1
    Host: localhost:8080
    Origin: http://127.0.0.1:3000
    Connection: Upgrade
    Upgrade: websocket
    Sec-WebSocket-Version: 13
    Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
    
    Sec-WebSocket-Key:与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。
    注意,上面请求省略了部分非重点请求首部。由于是标准的HTTP请求,类似Host、Origin、Cookie等请求首部会照常发送。在握手阶段,可以通过相关请求首部进行 安全限制、权限校验等。
    
  2. 服务端:响应协议升级

    HTTP/1.1 101 Switching Protocols
    Connection:Upgrade
    Upgrade: websocket
    Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
    
  3. 服务端是怎么计算 Sec-WebSocket-Accept 的

    • 收到客户端发来的Sec-WebSocket-Key
    • Sec-WebSocket-Key258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接
    • 通过SHA1计算出摘要,并转成base64字符串。
  4. Sec-WebSocket-Key / Sec-WebSocket-KeyAccept 有什么用

    • 避免服务端收到非法的socket连接,比如一不小心http客户端请求连接webSocket服务
    • 标识连接方式是webSocket,确保服务端理解webSocket连接
    • 主要目的是预防一些非故意的意外情况,保障安全性并不完全可靠,因为计算方式是公开的、简单的。

# 7.4.3 连接保持 + 心跳

  • WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费包括的连接资源。
  • 更贴切的解释:
    • 发送方 -> 接收方:ping,一方主动向另一方ping(心跳包),相当于告诉对方「我还活着」
    • 接收方 -> 发送方:pong,另一方相应pong,相当于回复「好哦,我也还活着」

# 7.4.4 数据分片

WebSocket的每条消息可能被切分成多个数据帧。当WebSocket的接收方收到一个数据帧时,会根据FIN的值来判断,是否已经收到消息的最后一个数据帧。

  • FIN=1,表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,可以对消息进行处理。
  • FIN=0,则接收方还需要继续监听接收其余的数据帧。

# 7.4.5 数据传输

  • 遵循特定的数据格式-数据帧(frame)
    • 发送端:将消息切割成多个帧,并发送给服务端;
    • 接收端:接收消息帧,并将关联的帧重新组装成完整的消息;
  • 数据帧字段:
    • FIN:这条消息是否是最后一个分片,1是0否。
    • RSV1 / RSV2 / RSV3:用于标识协商的扩展
    • opcode: 标识数据是什么:继续帧 / 文本帧 / 二进制帧 / 连接关闭 / ping /pong
    • masked:表示是否要对数据载荷进行掩码操作,按位做循环异或运算。
      • 从客户端向服务端发送数据时,需要对数据进行掩码操作;
      • 从服务端向客户端发送数据时,不需要对数据进行掩码操作。
    • masking-key:掩码键,由客户端挑选出来的32位的随机数
    • payload length:表示数据载荷的长度
    • payload data:载荷数据 = 扩展数据 + 应用数据。

# 7.4.6 为什么客户端发送数据的时候要进行掩码操作

  • 针对基础设施的攻击:
    • 一般形式的攻击是跟被攻击者控制的服务器建立连接,并构造一个类似WebSocket握手一样的UPGRADE请求,随后通过UPGRADE建立的连接发送看起来就像GET请求的frame去获取一个已知资源(在攻击场景中可能是一个点击跟踪脚本或广告服务网络中的资源)。
    • 之后远程服务器会返回某些东西,就像对于这个伪造GET请求的响应,并且这个响应会被很多广泛部署的网络中间设备缓存,从而达到了污染缓存服务器的目的。对于这个攻击的产生的效应,可能一个用户被诱导访问受攻击者操控的服务器,攻击者就有可能污染这个用户以及其他共享相同缓存服务用户的缓存服务器,并跨域执行恶意脚本,破坏web安全模型。
  • 应对措施:掩码
    • 为了避免面这种针对中间设备的攻击,以非HTTP标准的frame作为用户数据的前缀是没有说服力的,因为不太可能彻底发现并检测每个非标准的frame是否能够被非HTTP标准的中间设施识别并略过,也不清楚这些frame数据是否对中间设施的行为产生错误的影响。
    • 对此,WebSocket的防御措施是mask所有从客户端发往服务器的数据,这样恶意脚本(攻击者)就没法获知网络链路上传输的数据是以何种形式呈现的,所以他没法构造可以被中间设施误解为HTTP请求的frame。
  • 掩码的选择:
    • 客户端必须为发送的每一个frame选择新的掩码,要求是这个掩码无法被提供数据的终端应用(即客户端)预测。

# 7.4.7 客户端的API

  • readyState:
    • 0:Connecting,正在连接
    • 1:Open,连接成功,可以通信
    • 2:Closing,连接正在关闭
    • 3:Closed,连接已经关闭,或者打开连接失败
  • onopen:用于指定连接成功后的回调函数
  • addEventListener:指定多个回调函数
  • onclose:指定连接关闭后的回调函数
  • onmessage:指定收到服务器数据后的回调函数
  • binarayType:显式指定收到的二进制数据类型(blob或者Arraybuffer)
  • send:用于向服务器发送数据
  • bufferedAmount:表示还有多少字节的二进制数据没有发送出去,可以用来判断发送是否结束
  • onerror:指定报错时的回调函数

# 7.5 Web Worker(区别于PWA中的service worker)

# 7.5.1 简介

  • 这一规范定义了一套 API,它允许一段JavaScript程序运行在主线程之外的另外一个线程中。
  • 这个子线程是浏览器开的,并没有改变JS引擎是单线程的本质。
  • Web Worker 规范中定义了两类工作线程:
  • 专用线程Dedicated Worker:只能为一个页面所使用。
  • 共享线程 Shared Worker,可以被多个页面所共享。

# 7.5.2 Worker线程数据通信方式

  • Worker 与其主页面之间的通信
    • onmessage 事件
    • postMessage() 方法
  • Worker 与其主页面之间的数据传递是拷贝,不是共享

# 7.5.3 可转让对象:

  • 通过切换所属的上下文,实现将对象在Worker与主页面之间穿梭
  • 切换之后,之前的环境中的原始数据将被清除,无法继续使用,即这不是拷贝

# 7.5.4 Worker上下文

  • Worker执行的上下文,与主页面执行时的上下文并不相同,最顶层的对象并不是window。
  • 而是WorkerGlobalScope,所以无法访问window、以及与window相关的DOM API,但是可以与setTimeout、setInterval等协作。

# 8. src和href的区别?

  • 定义
    • href = Hypertext Reference,表示超文本引用,指向网络资源所在位置。eg. a标签、link标签引入css
    • src = source,目的是要把文件下载到html页面中去。eg. img标签、script标签
  • 浏览器解析方式
    • 当浏览器遇到href会并行下载资源并且不会停止对当前文档的处理。(同时也是为什么建议使用 link 方式加载 CSS,而不是使用 @import 方式)
    • 当浏览器解析到src ,会暂停其他资源的下载和处理,直到将该资源加载或执行完毕。(这也是script标签为什么放在底部而不是头部的原因)

# 9.script标签中defer和async的区别?

  • 默认情况下,脚本的下载和执行将会按照文档的先后顺序同步进行。当脚本下载和执行的时候,文档解析就会被阻塞,在脚本下载和执行完成之后文档才能往下继续进行解析。
  • 当script中有defer属性时,脚本的加载过程和文档加载是异步发生的,等到文档解析完(DOMContentLoaded事件发生)脚本才开始执行。
  • 当script有async属性时,脚本的加载过程和文档加载也是异步发生的。但脚本下载完成后会停止HTML解析,执行脚本,脚本解析完继续HTML解析。
  • 当script同时有async和defer属性时,执行效果和async一致。
  • image

# 10. 移动端点击延迟事件

# 10.1 原因

移动浏览器 会在 touchendclick 事件之间,等待 300 - 350 ms,判断用户是否会进行双击手势用以缩放文字。

# 10.2 解决方案

  1. 禁用缩放:<meta name = "viewport" content="user-scalable=no" > 缺点: 网页无法缩放

  2. 更改默认视口宽度: <meta name="viewport" content="width=device-width"> 缺点: 需要浏览器的支持

  3. css touch-action:默认为 auto,将其置为 none 即可移除目标元素的 300 毫秒延迟

    缺点: 新属性,可能存在浏览器兼容问题

  4. tap事件:zepto的tap事件, 利用touchstart和touchend来模拟click事件 缺点: 点击穿透

  5. fastclick 原理:在检测到touchend事件的时候,会通过DOM自定义事件立即出发模拟一个click事件,并把浏览器在300ms之后真正的click事件阻止掉 缺点: 脚本相对较大

# 11. 渲染层合并 (Composite)

# 11.1 基本概念

  • 渲染层合并:对于页面中 DOM 元素的绘制(Paint)是在多个层上进行的。
  • 在每个层上完成绘制过程之后,浏览器会将绘制的位图发送给 GPU 绘制到屏幕上,将所有层按照合理的顺序合并成一个图层,然后在屏幕上呈现。
  • 对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。

# 11.2 名词概念

image

  • 渲染对象(RenderObject): 一个 DOM 节点对应了一个渲染对象,渲染对象依然维持着 DOM 树的树形结构。一个渲染对象知道如何绘制一个 DOM 节点的内容,它通过向一个绘图上下文(GraphicsContext)发出必要的绘制调用来绘制 DOM 节点。
  • 渲染层(RenderLayer):这是浏览器渲染期间构建的第一个层模型,处于相同坐标空间(z轴空间)的渲染对象,都将归并到同一个渲染层中,因此根据层叠上下文,不同坐标空间的的渲染对象将形成多个渲染层,以体现它们的层叠关系。所以,对于满足形成层叠上下文条件的渲染对象,浏览器会自动为其创建新的渲染层。
  • 图形层(GraphicsLayer):一个负责生成最终准备呈现的内容图形的层模型,它拥有一个图形上下文(GraphicsContext),GraphicsContext 会负责输出该层的位图。存储在共享内存中的位图将作为纹理上传到 GPU,最后由 GPU 将多个位图进行合成,然后绘制到屏幕上,此时,我们的页面也就展现到了屏幕上。所以 GraphicsLayer 是一个重要的渲染载体和工具,但它并不直接处理渲染层,而是处理合成层。
  • 合成层(CompositingLayer):满足某些特殊条件的渲染层,会被浏览器自动提升为合成层。合成层拥有单独的 GraphicsLayer,而其他不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 的父层共用一个。
    • 某些条件:
      • 3D transforms:translate3d、translateZ 等
      • video、canvas、iframe 等元素
      • 通过 Element.animate() 实现的 opacity 动画转换
      • 通过 СSS 动画实现的 opacity 动画转换
      • position: fixed
      • 具有 will-change 属性
      • 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition

# 11.3 隐式合成

  • 满足某些显性的特殊条件时,渲染层会被浏览器提升为合成层。除此之外,在浏览器的 Composite 阶段,还存在一种隐式合成,部分渲染层在一些特定场景下,会被默认提升为合成层。
  • “一个或多个应按堆叠顺序出现在合成元素上方的非合成元素,将升级为合成层。”
  • 在出现交叠的时候,为了纠正错误的交叠顺序,浏览器必须让原本应该”盖在“它上边的渲染层也同时提升为合成层。
  • 参见浏览器层合成与页面渲染优化

# 11.4 层爆炸和层压缩

  • 层爆炸
    • 原因:一些产生合成层的原因太过于隐蔽了,尤其是隐式合成。
    • 形成:开发过程中,很容易产生一些不在预期范围内的合成层,当这些不符合预期的合成层达到一定量级时,就会变成层爆炸。
    • 结果:层爆炸会占用 GPU 和大量的内存资源,严重损耗页面性能,因此盲目地使用 GPU 加速,结果有可能会是适得其反。
  • 层压缩
    • 如果多个渲染层同一个合成层重叠时,这些渲染层会被压缩到一个 GraphicsLayer 中,即它们会处在同一个合成层中,以防止由于重叠原因导致可能出现的“层爆炸”。(避免了出现多个合成层)

# 11.3 合成层的优点

  • 一旦渲染层提升为合成层就会有自己的图像层和图形上下文,并且会开启硬件加速,有利于性能提升。
    • 合成层的位图,会交由 GPU 合成,比 CPU 处理要快 (提升到合成层后合成层的位图会交 GPU 处理。但请注意,仅仅只是合成的处理(把绘图上下文的位图输出进行组合)需要用到 GPU,生成合成层的位图处理(绘图上下文的工作)是需要 CPU。)
    • 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层 (当需要 repaint 的时候可以只 repaint 本身,不影响其他层,但是 paint 之前还有 style,layout,那就意味着即使合成层只是 repaint 了自己,但 style 和 layout 本身就很占用时间。)
    • 对于 transform 、 opacity 、filter效果,不会触发 layout 和 paint (仅仅是 transform 和 opacity 不会引发 layout 和 paint,其他的属性不确定。)
  • 一般一个元素开启硬件加速后会变成合成层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能。
  • 注意不能滥用 GPU 加速,一定要分析其实际性能表现。因为 GPU 加速创建渲染层是有代价的。
    • 每创建一个新的渲染层,就意味着新的内存分配和更复杂的层的管理。
    • 在移动端 GPU 和 CPU 的带宽有限制,创建的渲染层过多时,合成也会消耗跟多的时间,随之而来的就是耗电更多,内存占用更多。
    • 过多的渲染层来带的开销而对页面渲染性能产生的影响,甚至远远超过了它在性能改善上带来的好处。

# 12. SEO(Search Engine Optimization,搜索引擎优化)

# 12.1 搜索引擎算法

# 12.1.1 PageRank

  • 一个页面的“得票数”由所有链向它的页面的重要性来决定,到一个页面的超链接相当于对该页投一票。一个页面的PageRank是由所有链向它的页面(“链入页面”)的重要性经过递归算法得到的。一个有较多链入的页面会有较高的等级,相反如果一个页面没有任何链入页面,那么它没有等级。
  • 每个页面都只有一票大小的权重,但是这个权重可以拆分为更小的权重分配给多个其他的页面。

# 12.1.2 TrustRank

  • 一种新的反作弊机制,以确保高质量的站点来获得搜索引擎的青睐。

# 12.1.3 HITS

  • 一个网页重要性的分析的算法,根据一个网页的入度(指向此网页的超链接)和出度(从此网页指向别的网页)来衡量网页的重要性。
  • 其最直观的意义是如果一个网页的重要性很高,则他所指向的网页的重要性也高。

# 12.1.4 百度搜索引擎的算法

  • 绿萝算法
    • 一种搜索引擎反作弊的算法。该算法主要打击超链中介、出卖链接、购买链接等超链作弊行为。
    • 当spider发现大量的不良外链导出,即权重输出,必定会给降低该站点的权重值。
  • 清风算法
    • 旨在严惩网站通过网页标题作弊,欺骗用户并获得点击的行为。
    • 打击下载站标题作弊行为
  • 飓风算法
    • 旨在严厉打击恶劣采集行为和站群问题
  • 惊雷算法
    • 严厉打击通过“恶意制造作弊超链”和“恶意刷点击“的手段来提升网站搜索排序的作弊行为。
  • 烽火算法
    • 严厉打击危害用户隐私、恶意劫持站点的行为。
  • 蓝天算法
    • 严厉打击新闻源售卖软文、目录行为,还用户一片搜索蓝天。
  • 细雨算法
    • 打击B2B站点页面标题作弊及误导、正文出现低质受益行为
  • 冰桶算法
    • 针对强行弹窗app下载、用户登录、低质广告等影响用户体验的站点行为
  • 闪电算法
    • 主要针对的是移动网页首屏的加载时间
    • 首屏在2秒之内完成打开的,在移动搜索下将获得提升页面评价优待,得到更多展现机会;
    • 首屏加载完毕在2~3秒之内的页面,页面评价不升不降。
    • 首屏加载非常慢(3秒及以上)的网页将会被打压。

# 12.2 基本的SEO方法

  • 网页结构布局优化:
    • 控制首页的链接数量,不要过多/过少。
    • 扁平化的目录层次:尽量3次以内跳转,可以到达站内任何一个内页。
  • 网页代码优化:
    • 利用布局,将重要的内容放在HTML代码的前面
    • 语义化编写HTML代码:a、img标签加alt、使用h1、strong标签强调重点
    • 少使用iframe框架,spider一般不会读取其中的内容
  • 前端网站性能优化
    • 合并css、js文件,减少HTTP请求数量
    • 懒加载lazyload
    • 利用浏览器缓存、CDN缓存,加快用户访问速度(相关算法:百度的闪电算法)

# 13. localStorage & sessionStorage

# 13.1 localStorage

  • 作用:持久化存储与跨页面传数据
  • 作用域:
    • localStorage只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份localStorage数据。
    • sessionStoragelocalStorage更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)下。
  • 生存周期:localStorage不主动清除就不会消失。
  • 数据仅保存在客户端,不与服务器进行通信;
  • 大小:每个域名下5M(cookie:4k)
  • 无法跨域
  • localStorage存储满了怎么办?
    1. 划分域名。各域名下的存储空间由各业务组统一规划使用,因为localStorage是针对于不同的域名设置一个的,避免业务混杂在同一个域名下
    2. IndexDB
    3. 清除掉其他页面下的localStorage

# 13.2 IndexDB

  • IndexedDB 是一种底层 API,用于客户端存储大量结构化数据,包括文件、二进制大型对象。该 API 使用索引来实现对该数据的高性能搜索。虽然 Web Storage 对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太好用。IndexedDB 提供了一个解决方案。
  • IndexedDB 的特点:
    • 存储空间大:存储空间可以达到几百兆甚至更多;
    • 支持二进制存储:它不仅可以存储字符串,而且还可以存储二进制数据;
    • IndexedDB 有同源限制,每一个数据库只能在自身域名下能访问,不能跨域名访问;
    • 支持事务型:IndexedDB 执行的操作会按照事务来分组的,在一个事务中,要么所有的操作都成功,要么所有的操作都失败;
    • 键值对存储:IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以 “键值对” 的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。
    • 数据操作是异步的:使用 IndexedDB 执行的操作是异步执行的,以免阻塞应用程序。

# 14. Lazy-load原理

  1. v-lazy绑定的DOM节点会创建一个对应的listener
  2. 将listener添加到listener queue中,给对应的DOM节点注册DOM事件
  3. 在DOM事件的回调中,遍历listener queue中的listener,判断该listener绑定的DOM是否处于页面中的preload位置
  4. 若是,则异步加载当前位置的图片。

# 15. BFF(Backend for Frontend)

  • 起源:多个前端的UI(IOS、Android、PC)需要适配,但往往UI体验不同:屏幕尺寸、硬件性能(内存、CPU等)、用户交互(点击、触摸、弹窗等)
  • 个人理解:
    • 在前端和后端之间,抽象出一层,简单的逻辑统一化处理,然后再与后端进行交互,对于前、后端都提供相对统一的调用和接口。
    • 一个BFF只专注于一个UI
  • image