5. Web标准与前端开发

77 阅读11分钟

5.1 关于前端开发

5.1.1 起源、架构、变迁

image.png

只读时代:客户端不能在不刷新的情况下刷新页面,需要获取整个文档来更新

体验时代:从简单的页面到web应用

敏捷时代:更加重视用户交互体验

5.1.2 前端应用的领域

  • To Business
  • To Customer
  • To Developer

5.1.3 语言、框架、工具

image.png

5.1.4 浏览器、网络、服务器

5.1.4.1 深入理解现代浏览器

本文分4个部分,对应下述4篇文章。

  • 架构:以Chrome为例,介绍现代浏览器的实现架构。
  • 导航:从输入URL到获到HTML响应称为导航。
  • 渲染:浏览器解析HTML、下载外部资源、计算样式并把网页绘制到屏幕上。
  • 交互:用户输入事件的处理与优化。

1、架构

Chrome最新的架构:最上层是浏览器进程,负责协调承担各项工作的其他进程,比如实用程序进程、渲染器进程、GPU进程、插件进程等,如下图所示。

image.png

渲染器进程对应新开的标签页,每新开一个标签页,就会创建一个新的渲染器进程。不仅如此,Chrome还会尽量给每个站点新开一个渲染器进程,包括iframe中的站点,以实现站点隔离。

下面详细了解一下每个进程的作用,可以参考下图。

  • 浏览器进程:控制浏览器这个应用的chrome(主框架)部分,包括地址栏、书签、前进/后退按钮等,同时也会处理浏览器不可见的高权限任务,如发送网络请求、访问文件。
  • 渲染器进程:负责在标签页中显示网站及处理事件。
  • 插件进程:控制网站用到的所有插件。
  • GPU进程:在独立的进程中处理GPU任务。之所以放到独立的进程,是因为GPU要处理来自多个应用的请求,但要在同一个界面上绘制图形。

image.png

Chrome的多进程架构的优点:

1、一个标签页即为一个渲染器进程,即使其中一个崩溃了,其他也不会受影响

2、多进程架构还有助于安全和隔离。因为操作系统有限制进程特权的机制,浏览器可以借此限制某些进程的能力。比如,Chrome会限制处理任意用户输入的渲染器进程,不让它任意访问文件

3、Chrome架构进化的目标是将整个浏览器程序的不同部分服务化,便于分割或合并。基本思路是在高配设备中,每个服务独立开进程,保证稳定;在低配设备中,多个服务合并为一个进程,节约资源。同样的思路也应用到了Android上。

2、导航

导航涉及浏览器进程与线程间为显示网页而通信。一切从用户在浏览器中输入一个URL开始。输入URL之后,浏览器会通过互联网获取数据并显示网页。从请求网页到浏览器准备渲染网页的过程,叫做导航。

导航的步骤:

  1. 处理输入 UI线程会判断用户输入的是查询字符串还是URL。因为Chrome的地址栏同时也是搜索框。
  2. 开始导航 如果输入的是URL,UI线程会通知网络线程发起网络调用,获取网站内容。此时标签页左端显示旋转图标,网络线程进行DNS查询、建立TLS连接(对于HTTPS)。网络线程可能收到服务器的重定向头部,如HTTP 301。此时网络线程会跟UI线程沟通,告诉它服务器要求重定向。然后,再发起对另一个URL的请求。
  3. 读取响应

服务器返回的响应体到来之后,网络线程会检查接收到的前几个字节。响应的Content-Type头部应该包含数据类型,如果没有这个字段,则需要MIME类型嗅探

如果响应是HTML文件,那下一步就是把数据交给渲染器进程。但如果是一个zip文件或其他文件,那就意味着是一个下载请求,需要把数据传给下载管理器。

  1. 联系渲染器进程 所有查检完毕,网络线程确认浏览器可以导航到用户请求的网站,于是会通知UI线程数据已经准备好了。UI线程会联系渲染器进程渲染网页。

由于网络请求可能要花几百毫秒才能拿到响应,这里还会应用一个优化策略。第二步UI线程要求网络线程发送请求后,已经知道可能要导航到哪个网站去了。因此在发送网络请求的同时,UI线程会提前联系或并行启动一个渲染器进程。这样在网络线程收到数据后,就已经有渲染器进程原地待命了。如果发生了重定向,这个待命进程可能用不上,而是换作其他进程去处理。

5.提交导航

数据和渲染器进程都有了,就可以通过IPC从浏览器进程向渲染器进程提交导航。渲染器进程也会同时接收到不间断的HTML数据流。当浏览器进程收到渲染器进程的确认消息后,导航完成,文档加载阶段开始。

  1. 初始加载完成

提交导航之后,渲染器进程将负责加载资源和渲染页面(具体细节后面介绍)。而在“完成”渲染后(在所有iframe中的onload事件触发且执行完成后),渲染器进程会通过IPC给浏览器进程发送一个消息。此时,UI线程停止标签页上的旋转图标。

初始加载完成后,客户端JavaScript仍然可能加载额外资源并重新渲染页面。

3、渲染

渲染器进程的核心任务是把HTML、CSS和JavaScript转换成用户可以交互的网页接下来,我们从整体上过一遍渲染器进程处理Web内容的各个阶段。

1. 解析HTML

构建dom:其主线程开始解析文本字符串(HTML),并将它转换为DOM(Document Object Model,文档对象模型)

DOM是浏览器内部对页面的表示,也是JavaScript与之交互的数据结构和API。

加载子资源:网站都会用到图片、CSS和JavaScript等外部资源。浏览器需要从缓存或网络加载这些文件。主线程可以在解析并构建DOM的过程中发现一个加载一个,但这样效率太低。为此,Chrome会在解析同时并发运行“预加载扫描器”,当发现HTML文档中有<img><link>时,预加载扫描器会将请求提交给浏览器进程中的网络进程。

JavaScript可能阻塞解析。如果HTML解析器碰到<script>标签,会暂停解析HTML文档并加载、解析和执行JavaScript代码。因为JavaScript有可能通过document.write()修改文档,进而改变DOM结构**

为了更好地加载资源,可以通过很多方式告诉浏览器。如果JavaScript没有用到document.write(),可以在<script>标签上添加asyncdefer属性。这样浏览器就会异步运行JavaScript代码,不会阻塞解析。合适的话,可以考虑使用JavaScript模块(t.cn/RDXCctZ)。再比… image.png

计算样式

光有DOM还不行,因为并不知道页面应该长啥样。所以接下来,主线程要解析CSS并计算每个DOM节点的样式。这个过程就是根据CSS选择符,确定每个元素要应用什么样式。在Chrome开发工具“计算的样式”(computed)中可以看每个元素计算后的样式。

布局

布局,就是要找到元素间的几何位置关系。主线程会遍历DOM元素及其计算样式,然后构造一棵布局树,这棵树的每个节点将带有坐标和大小信息。布局树与DOM树的结构类似,但只包含页面中可见元素的信息。如果元素被应用了display: none,则布局树中不会包含它

渲染

有了DOM、样式和布局,仍然不足以渲染页面。还要解决先画什么后画什么,即绘制顺序的问题。比如,z-index影响元素叠放,如果有这个属性,那简单地按元素在HTML中出现的顺序绘制就会出错。

因此,主线程会遍历布局树并创建绘制纪录;如下图,渲染是一个流水线作业:前一道工序的输出是下一道工序的输入,这意味着布局树有变化时,将会导致绘制纪录重新生成

image.png

合成

知道了文档结构、每个元素的样式、页面的几何关系还有绘制顺序,接下来就该绘制页面,具体怎么绘制了?把上述信息转换为屏幕上的像素叫做栅格化。

最简单的方式,可能就是把页面在当前视口中的部分先转换为像素。然后随着用户滚动页面,再移动栅格化的画框

image.png

创建了分层树,确定了绘制顺序,主线程就会把这些信息提交给合成器线程。合成器线程接下来负责将每一层转换为像素——栅格化。一层有可能跟页面一样大,此时合成器线程会将它切成小片(tile),再把每一片发给栅格化线程。栅格化线程将每一小片转换为像素后将它们保存在GPU的内存中。

交互

当用户交互比如触摸事件发生时,浏览器进程首先接收到该手势。但是,浏览器进程仅仅知道手势发生在哪里,因为标签页中的内容是渲染器进程处理。因此浏览器进程会把事件类型(如touchstart)及其坐标发送给渲染器进程。渲染器进程会处理这个事件,即根据事件目标来运行注册的监听程序。

提到注册事件,有一个常见的问题要注意。很多人喜欢使用事件委托来注册处理程序。这是利用事件冒泡原理,把事件注册到最外层元素上,然后再根据事件目标决定是否执行任务。

document.body.addEventListener('touchstart', evt => {
    if (evt.target === area) {
        evt.preventDefault()
    }
})

一个事件处理程序就可以面向多个元素,这种高效的写法因此很流行。然而,从浏览器的角度来看,这样会导致整个页面被标记为“非快速滚动区”。这也就意味着,即便事件发生在那些不需要处理的元素上,合成器线程也要每次都跟主线程沟通,并等待它的回应。于是,合成器线程平滑滚动的优点就被抵销了

由于 touchstart 事件对象的 cancelable 属性为 true,也就是说它的默认行为可以被监听器通过 preventDefault() 方法阻止,那它的默认行为是什么呢,通常来说就是滚动当前页面(还可能是缩放页面),如果它的默认行为被阻止了,页面就必须静止不动。但浏览器无法预先知道一个监听器会不会调用 preventDefault(),它能做的只有等监听器执行完后再去执行默认行为,而监听器执行是要耗时的,有些甚至耗时很明显,这样就会导致页面卡顿。视频里也说了,即便监听器是个空函数,也会产生一定的卡顿,毕竟空函数的执行也会耗时。

为缓冲使用事件委托带来的副作用,可以在注册事件时传入passive: true。这个选项会提醒浏览器,你仍然希望主线程处理事件,但与此同时合成器线程也可以继续合成新的帧。

document.body.addEventListener('touchstart', evt => {
  ...
}, { passive: true })

5.1.4.2 An overview of Http

5.1.4.3 学习路线图

roadmap.sh/

5.2 关于Web标准

  • 标准组织

image.png

  • Ecma会员 Ecma的AM(Associate Member)会员目前有18家,如字节跳动、360、阿里、华为等5家

  • W3C规范指定流程

image.png