实战中学习浏览器工作原理 — 排版与渲染
我是三钻,一个在《技术银河》中等你们一起来终生漂泊学习。
点赞是力量,关注是认可,评论是关爱!下期再见 👋!
前言
上一周我们完成了 CSS 的规则计算,其实就是计算了每个元素匹配中了那些 CSS 规则,并且把这些规则挂载到元素的 ComputedStyle 上面。
这一周我们就要继续我们的浏览器开发之旅,然后我们的下一个目标就是根据浏览器的属性来进行排版(英文是 Layout
,有时候也翻译成布局)。
我们来看看目前我们的浏览器实现到哪里。在一开始的 URL 请求获得 HTML,通过对代码的解析,然后构建 DOM树,再通过分析 style 中的选择器,接着匹配上元素并且给 DOM 树中的属性加入对应的 ComputedStyles 之后,最后我们拥有一个一颗带有 CSS 的 DOM 树。
最后剩下的两个步骤,就与我们真正的浏览器可以看到的效果越来越接近了。那我们就来一起开始继续实现我们的浏览器吧!
排版
根据浏览器属性进行排版
在我们开始编写我们的排版逻辑之前,我们是有必要了解一些概念的。
我们的浏览器的排版选择用 flex
为例来实现排版算法,至于为什么选择这个排版呢?因为它包含了三代的排版技术。
第一代就是正常流 —— 包含了
position
,display
,flow
;第二代就是
flex
—— 这个就比较接近人的自然思维;第三代就是
grid
—— 是一种更强大的排版模式;"第四代"可能是
Houdini
—— 是一组底层API,它们公开了 CSS 引擎的各个部分,从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展CSS。
那为什么我们选择了第二代来讲解这一部分呢?那是因为第二代的排版技术比较容易实现,而它的能力又不太差。既然我们只是实现一个模拟浏览器,也就让大家感受一下浏览器中的排版是怎么实现的即可。如果想要实现完成的浏览器排版,那里面的逻辑就非常复杂了,这里就不一一实现了。
在 flex 排版里面我们有纵排和横排两种,这两种排版方式都是受 flex 属性限制的,所以我们需要在宽高上做抽象
排版里面有一个主轴,它是我们排版的时候主要的延伸方向(如果我们有多个元素,这些元素就会往这个主轴的方向排列)
跟这个主轴相垂直就有一个交叉轴的方向,所以跟主轴垂直方向的属性都叫做交叉轴属性
所以在 flex 排版,里面我们就需要更具
flex-direction
属性去设置主轴的延伸方向,然而交叉轴就是主轴垂直的另外的方向我们根据上面的图,如果 flex-direction 是
row
, 那么主轴的属性就是width x left right
等属性,而**交叉轴**就是:height y top bottom
等属性;而如果 flex-direction 是column
的时候,就刚好是相反的。根据上面的这个抽象,我们在编写这个排版算法的时候就能去掉大量的 if 和 else 判断
这也是一种 web 标准中也采用的一种抽象的描述方式
好,那么我们开始编写代码,首先讲讲我们的思路:
在开始编写所有的逻辑前,我们这一部分先做一个非常重要的准备工作
其实就是帮我们处理掉了 flex-direction 和 wrap 相关的属性
这部分的重点就是把具体的 width, height, left, right, top, bottom 等属性给抽象成 main (主轴) 和 cross (交叉轴) 等属性
文件:parser.js 的
emit
方法中,找到endTag
的判断,然后在stack.pop()
之前加入layout(top)
函数。因为我们的parser.js
中的代码已经比较多了,所以我们把排版的代码放在 layout.js 中进行编写。这里我们就要在头部引入这个 JavaScript 文件。
文件:layout.js
收集元素进行
收集元素进行,是为了我们后面计算元素而做的准备。因为 flex 布局是把元素放到一行空间里面的,当一行的中所有子元素相加的尺寸超出了父级元素的尺寸时,因为空间不足就会把元素放入下一行里面。所以这里这个步骤就是把元素 “收集进每一行里面的逻辑”。
这里要注意的是,flex 排版中还有一个 no-wrap
的属性可以控制分行特性的。如果设置了 no-wrap
我们收集元素进行时直接强行分配进第一行即可了。但是大部分情况默认都不是 no-wrap
的, 所以都是需要我们去动态分行的。
看到上面的效果中,第一行中首先依次加入元素 1,2,3。当我们加入到第 4 个元素的时候,我们发现元素超出了当前行,所以我们需要建立一个新行,然后把元素 4 放入新的一行中。所以在这个部分的代码逻辑就是这样,以此类推把所有元素按照这个逻辑放入 flex 的行中。接下来我们来看看实际的代码是怎么写的。
文件:layout.js 中的
layout
函数
计算主轴
计算主轴方向:
找出所有 Flex 元素
把主轴方向的剩余尺寸按比例分配给这些元素
若剩余空间为负数,所有 flex 元素为 0, 等比例压缩剩余元素
等比例分配的效果:
计算交叉轴
这部分我们来计算交叉轴的尺寸,假设我们现在的 flex-direction 属性值是 row
, 那么我们主轴已经计算出来元素的 width
,left
和 right
,那么交叉轴我们就算它的 height
,top
和 bottom
。如果这六个都确定了,这个元素的位置就完全确定了。
计算交叉轴方向:
根据每一行中最大元素尺寸来计算行高
根据行高 flex-align 和 item-align,确定元素具体位置
服务端代码
为了可以使用我们浏览器中的 flex 排版,我们需要对我们服务端代码中的 HTML 进行一些修改:
渲染
这里就是浏览器原理的最后一个步骤了。浏览器工作原理中,我们从 URL 发起 HTTP 请求,然后通过解析 HTTP 获得 HTML 代码。通过解析 HTML 代码构建了 DOM 树,然后通过分析 style 属性和选择器匹配给 DOM 树上加上 CSS 属性。上一步我们通过分析每个元素的 CSS 属性来排版信息。
到了这里我们只要完成 “绘制” 我们就已经把整个完成的浏览器渲染流程走完了。当然浏览器肯定还有很多其他的功能,而且它会有持续的绘制和事件监听。我们这里的模拟浏览器就只做到绘制为止。
在编写绘制的代码之前,我们需要准备一下环境的。在我们的模拟浏览器当中,我们用绘制图片代替了真正浏览器的绘制屏幕。在 node.js 当中是没有自带的图形绘制的功能的。所以这里我们用 images
包来代替。
安装:
包的 npm 地址:https://npmjs.com/package/images
绘制单个元素
在实现绘制之前,我们要给 server.js 中的 HTML 代码加上一些属性:
+ 上面我们给
#container
加上了background-color: rgb(255,255,255)
+ 给
#container #mid
加上了height: 100px
和background-color: rgb(255,0,0)
+ 给
#container .c1
加上了background-color: rgb(0,0,255)
逻辑思路:
绘制需要依赖一个图形环境
这里我们采用了 npm 包 images
绘制一个 viewport 上进行
与绘制相关的属性: background-color、border、background-image 等
如果我们想做 gradient 这种更复杂的绘制的话就需要依赖 webGL 了
首先在
client.js
中的请求方法里常见viewport
,并且调用render
函数来渲染。
然后我们来实现
render.js
用 debug 或者 node 执行我们的 client.js 后,会在当前目录中看到
viewport.png
,然后图片的效果如下:
渲染 DOM
上一部分我们完成了单个元素的绘制,但是其实我们想从单个元素的绘制进行到 DOM 绘制的话,难度其实不难,我们只需要递归的调用 render 函数就可以完成整个 DOM 的绘制了。
思路:
递归调用子元素的绘制方法完成 DOM 树的绘制
忽略一些不需要绘制的节点
实际浏览器中,文字绘制是难点,需要依赖字体库,我们这里忽略了
实际浏览器中,还会对一些图层做 compositing,我们这里也忽略了
我们来看看代码怎么实现的:
首先我们把
client.js
中的render
函数的参数改为 dom
然后修改
render.js
中的代码
最后我们得出的一个渲染结果如下图:
最后
到这里我们浏览器的实现就到此告一段落了。各位同学,如果大家跟着这个系列的文章做到这里,我们已经把浏览器的全部流程都实现了。从开始的 URL,获取到 HTML,再从 HTML 代码又把它变成一个课 DOM 树。再把这个什么都没有的 DOM 树加上了 CSS 的属性。然后我们把所有这些 CSS 属性的计算出来找到每个元素的位置。最后我们在一张图片上把网页的内容画了出来。这个过程已经是一个浏览器从一个 URL 到呈现在页面上的一个整体过程了。
通过这个实战,我们应该都对浏览器的整个工作原理有一个更深和感知的理解。在实现这个浏览器的过程中,我们还是采用了很多比较省略的实现方法,如果同学们想真正的去理解浏览器的话,那么还是有很长的路要走。但是还是希望大家在这个实战的过程有一定的收获的。
版权声明: 本文为 InfoQ 作者【三钻】的原创文章。
原文链接:【http://xie.infoq.cn/article/c569ec7e713cebaba6ee9f90a】。文章转载请联系作者。
评论