总体思路
- 新建server.js充当服务器,返回response
- 新建browser文件夹充当浏览器,其下新建以下处理文件
- client.js 浏览器,用来发送Request,调用解析Response响应报文
- parser.js 解析器,使用状态机(FSM)解析响应报文body主体形在dom树,同时使用css包解析style内容生成css的ast树,为Dom树上的元素添加style
- layout.js 布局,这里采用Flex布局的计算方式进行编排,计算元素的宽高、留白及换行
- render.js 渲染器,简单的采用image包,将布局渲染成一张图片
报文的解析
以下是需要处理的响应报文
HTTP/1.1 200 OK
Content-Type: text/plain
X-Foo: bar
Date: Fri, 26 Jun 2020 01:28:31 GMT
Connection: keep-alive
Transfer-Encoding: chunked
277
<html meaa=a>
<head>
<style>
#container {
width: 500px;
height: 300px;
display: flex;
background-color: rgb(0,0,255);
}
#container #myid {
width: 200px;
height: 100px;
background-color: rgb(255,0,0);
}
#container .c1 {
flex: 1;
background-color: rgb(0,255,0);
}
</style>
</head>
<body>
<div id="container">
<div id="myid"></div>
<div class="c1"></div>
</div>
</body>
</html>
0
看完响应报文需要注意几个问题:
-
响应头中有句 Transfer-Encoding: chunked,其结构大致是: 返回的消息被分为多个数据块, 每个数据块有两部分, 长度 + 数据, 这两部分都以CRLF(即\r\n)结尾. 而终止块是一个特殊的数据块, 其长度为0,其报报文的格式如下图
-
Content-Length 的问题,如果报文中包含Transfer-Encoding: chunked首部, 那么Content-Length将被忽略,这是在说浏览器的处理,但长度还是有的,报文中的277就是body主体的length,但这是一个十六进制数,不是十进制需要注意。lenth的不准确会导致读取出错,无法结束或提前截断
Content-Length的问题的详解:blog.piaoruiqing.com/2019/09/08/…
- chunked的应用场景,MDN上是这么解释的:
MDN-Transfer-Encoding: developer.mozilla.org/zh-CN/docs/…
- 状态机(FSM)具体定义
我们需要对响应头以下的主体内容进行解析,这种解析的方式就使用状态。简单的来说就是,一个字一个字的读取,读取不到不同的字符执行不同的状态操作,直到EOF结束.
对于html来说大概就是开始标签、结束标签、文本字符
开始标签:html
开始标签:head
结束标签:head
开始标签:body
字符串:lagou
结束标签:body
结束标签:html
那么css呢? 当解析到结束标签时,会使用css包解析style组成ast树,重点关注rules下的selectors & declarations.property & declarations.value 我们例子会使用这几部分,在解析html的过程,用来匹配选择器,并计算css优先级
body {
background: #eee;
color: #888;
}
// 解析后如下:
{
"type": "stylesheet",
"stylesheet": {
"rules": [
{
"type": "rule",
"selectors": [
"body"
],
"declarations": [
{
"type": "declaration",
"property": "background",
"value": "#eee",
"position": {
"start": {
"line": 2,
"column": 3
},
"end": {
"line": 2,
"column": 19
}
}
},
{
"type": "declaration",
"property": "color",
"value": "#888",
"position": {
"start": {
"line": 3,
"column": 3
},
"end": {
"line": 3,
"column": 14
}
}
}
],
"position": {
"start": {
"line": 1,
"column": 1
},
"end": {
"line": 4,
"column": 2
}
}
}
]
}
}
- css的优先级表示及比较 可以看到 specificity 方法中,创建了一个一维数组[0,0,0,0],其每个位置分别表示内联、id、类、标签名,前者优先级高于后者,两个css的比较也是相同位置进行对比
function specificity(selector) {
// 内联, id, 类, 标签名
let p = [0, 0, 0, 0]
let selectorParts = selector.split(" ")
for (var part of selectorParts) {
if (part.charAt(0) === '#') { // index = 1
p[1] += 1
} else if (part.charAt(0) === '.') { // index = 2
p[2] += 1
} else { // index = 3
p[3] += 1
}
}
return p
}
function compare(sp1, sp2) {
if (sp1[0] - sp2[0]) {
return sp1[0] - sp2[0]
}
if (sp1[1] - sp2[1]) {
return sp1[1] - sp2[1]
}
if (sp1[2] - sp2[2]) {
return sp1[2] - sp2[2]
}
return sp1[3] - sp3[3]
}
总结
- CSS选择器匹配时,为什么从右向左进行的?
因为元素的匹配是自下而上,由内向外的,跟冒泡的方式一样。这一点在html解析过程中可以得到验证
- css放在body后面会发生什么事情?
toybrowser中style的解析,在标签结束的时候执行,可这时候元素的style已计算完成,要匹配上当下的style,必须重新跑一遍解析过程,确保样式的正确,重排或许就这么上场了吧?
- 解析html选择状态机而不选择正则?
个人觉得要用正则处理形成ast是件很麻烦的事情,试想要如何将闭合标签内的层级精确的放在父级上,且多层嵌套在一起,就相当不容易了。不如状态机来得明确,虽然看着if很多,有点low的感觉,但不确很适用。