渲染与排版
前言
上一周我们完成了 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 文件。
const layout = require('./layout.js');
/**
* 输出 HTML token
* @param {*} token
*/
function emit(token) {
// 如果是开始标签
if (token.type == 'startTag') {
//...
} else if (token.type == 'endTag') {
// 校验开始标签是 否被结束
// 不是:直接抛出错误,是:直接出栈
if (top.tagName !== token.tagName) {
throw new Error('Parse error: Tag start end not matched');
} else {
// 遇到 style 标签时,执行添加 CSS 规则的操作
if (top.tagName === 'style') {
addCSSRule(top.children[0].content);
}
layout(top); // <==== 这里加入 layout 函数
stack.pop();
} else if (token.type === 'text') {
// ...
}
currentTextNode = null;
}
文件:layout.js
/**
* 预处理元素中的 Style 属性
* @param {*} element
*/
function getStyle(element) {
if (!element.style) element.style = {};
for (let prop in element.computedStyle) {
let p = element.computedStyle[prop].value.toString();
element.style[prop] = p;
// 把 px 单位的数字用 parseInt 转成数字类型
let pxReg = /(\d+)px$/;
if (p.match(pxReg)) {
element.style[prop] = parseInt(p.match(pxReg)[0]);
}
// 纯数字的也用 parseInt 转成数字类型
let numReg = /^\d+$|^\d+\.$|^\d+\.\d+$/;
if (p.match(numReg)) {
element.style[prop] = parseInt(p.match(numReg)[0]);
}
}
return element.style;
}
/**
* 元素排版
* @param {*} element
*/
function layout(element) {
// 如果没有 computedStyle 的可以直接跳过
if (!element.computedStyle) return;
// 对 Style 做一些预处理
let elementStyle = getStyle(element);
// 我们的模拟浏览器只做 flex 布局,其他的跳过
if (elementStyle.display !== 'flex') return;
// 过滤掉所有文本节点
// 为了支持 flex 排版中的排序属性 order
let items = element.children
.filter(e => e.type === 'element')
.sort((a, b) => {
return (a.order || 0) - (b.order || 0);
});
let style = elementStyle;
// 把所有 auto 和空的宽高变成 null 方便我们后面判断
['width', 'height'].forEach(size => {
if (style[size] === 'auto' || style[size] === '') {
style[size] = null;
}
});
// 把 flex 排版的关键属性都给予默认值
if (!style.flexDirection || style.flexDirection === 'auto') style.flexDirection = 'row';
if (!style.alignItems || style.alignItems === 'auto') style.alignItems = 'stretch';
if (!style.justifyContent || style.justifyContent === 'auto') style.justifyContent = 'flex-start';
if (!style.flexWrap || style.flexWrap === 'auto') style.flexWrap = 'nowrap';
if (!style.alignContent || style.alignContent === 'auto') style.alignContent = 'stretch';
let mainSize, // 主轴长度
mainStart, // 主轴开始方向
mainEnd, // 主轴结束方向
mainSign, // 主轴标记 +1: 就是叠加,-1: 就是叠减
mainBase; // 主轴开始值
let crossSize, // 交叉轴长度
crossStart, // 交叉轴开始方向
crossEnd, // 交叉轴结束方向
crossSign, // 交叉轴标记 +1: 就是叠加,-1: 就是叠减
crossBase; // 交叉轴开始值
if (style.flexDirection === 'row') {
mainSize = 'width';
mainStart = 'left';
mainEnd = 'right';
mainSign = +1;
mainBase = 0;
crossSize = 'height';
crossStart = 'top';
crossEnd = 'bottom';
}
if (style.flexDirection === 'row-reverse') {
mainSize = 'width';
mainStart = 'right';
mainEnd = 'left';
mainSign = -1;
mainBase = style.width;
crossSize = 'height';
crossStart = 'top';
crossEnd = 'bottom';
}
if (style.flexDirection === 'column') {
mainSize = 'height';
mainStart = 'top';
mainEnd = 'bottom';
mainSign = +1;
mainBase = 0;
crossSize = 'width';
crossStart = 'left';
crossEnd = 'right';
}
if (style.flexDirection === 'column-reverse') {
mainSize = 'height';
mainStart = 'bottom';
mainEnd = 'top';
mainSign = -1;
mainBase = style.height;
crossSize = 'width';
crossStart = 'left';
crossEnd = 'right';
}
// 交叉轴反转的时候,直接调换 crossStart 和 crossEnd 的值
if (style.flexWrap === 'wrap-reverse') {
let dummy = crossStart;
crossStart = crossEnd;
crossEnd = dummy;
crossSign = -1;
} else {
crossBase = 0;
crossSign = +1;
}
}
module.exports = layout;