阅读 233

如何模拟浏览器解析一段HTML

了解来源

浏览器通过HTTP协议接收到报文之后,我们可以通过split('\r\n');简单粗暴地把它分隔开,再取用我们需要的部分即可。
但是这样做有一个很大的问题:HTTP传输报文是分段的,我们不能保证每次分隔都作用于完整的报文,因此这种方法不可取,那么应该如何做呢?这里就不得不提到DOM树的概念了,也就是文档对象模型(Document object model),详解可以自行搜索

有穷状态机

浏览器在处理一段HTML字符串的时候,就应用到了有穷状态机的思想,我个人理解是:它将处理的流程分为几个状态,每个状态都负责执行一定的功能,就像工厂的流水线一样,到什么阶段就进行什么操作,只需要将它的逻辑组织好就能实现自动化处理

关于浏览器是如何将一段HTML解析,并且生成一颗DOM树,我想分享以下做法

let htmlStr = `
<html>
  <head></head>
  <body>
    <img />
    <span></span>
    <div></div>
  </body>
</html>
`
// 这段就是我们要处理的HTML字符串
let currentToken = null;
let stack = [{ type: 'document', children: [] }]
// 使用栈来操作匹配标签对,生成一颗DOM树
parse(htmlStr);
console.log(JSON.stringify(stack[0], null, 2));
function emit(token) {
  console.log(token);
  let top = stack[stack.length - 1];
  // console.log(top);
  if (token.type === 'startTag') {
    let element = {
      type: 'element',
      tagName: token.tagName,
      children: [],
      attribute: []
    }
    stack.push(element);
    // if (!top.children) top.children = [];
    // console.log(top.children);
    top.children.push(element);
  } else if (token.type === 'endTag') {
    if (token.tagName !== top.tagName) {
      throw new Error('tagname match error')
    } else{
      stack.pop(token);
    }
  } else if (token.type === 'selfCloseTag') {
    let element = {
      type: 'element',
      tagName: token.tagName,
      children: [],
      attribute: []
    }
    top.children.push(element);
  }
  currentToken = null;
}
复制代码

这部分就是有穷状态机的应用

function parse(str) {
  let state = start;
  for (let c of str) {
    state = state(c);
  }
}
function start(c) {
  if (c === '<') {
    return tagOpen
  } else {
    return start
  }
}
function tagOpen(c) {
  if (c === '/') {
    return tagClose
  } else if (c.match(/[a-zA-Z]/)) {
    currentToken = {
      type: 'startTag',
      tagName: c
    }
    return tagName
  }
}
function tagName(c) {
  if (c.match(/[a-zA-Z]/)) {
    currentToken.tagName += c;
    return tagName
  } else if (c.match(/[/t/r/n ]/)) {
    return beforeAttributeName
  } else if (c === '>') {
    // 拼接结束了
    emit(currentToken);
    return start
  }
}
function beforeAttributeName(c) {
  if (c === '/') {
    currentToken.type = 'selfCloseTag';
    return tagName
  }
}
function tagClose(c) {
  if (c.match(/[a-zA-Z]/)) {
    currentToken = {
      type: 'endTag',
      tagName: c
    }
    return tagName
  }
}
复制代码

这是分隔后结果

{ type: 'startTag', tagName: 'html' }
{ type: 'startTag', tagName: 'head' }
{ type: 'endTag', tagName: 'head' }
{ type: 'startTag', tagName: 'body' }
{ type: 'selfCloseTag', tagName: 'img' }
{ type: 'startTag', tagName: 'span' }
{ type: 'endTag', tagName: 'span' }
{ type: 'startTag', tagName: 'div' }
{ type: 'endTag', tagName: 'div' }
{ type: 'endTag', tagName: 'body' }
{ type: 'endTag', tagName: 'html' }
复制代码

这是处理完成的DOM树,其中element代表它是元素节点

{
  "type": "document",
  "children": [
    {
      "type": "element",
      "tagName": "html",
      "children": [
        {
          "type": "element",
          "tagName": "head",
          "children": [],
          "attribute": []
        },
        {
          "type": "element",
          "tagName": "body",
          "children": [
            {
              "type": "element",
              "tagName": "img",
              "children": [],
              "attribute": []
            },
            {
              "type": "element",
              "tagName": "span",
              "children": [],
              "attribute": []
            },
            {
              "type": "element",
              "tagName": "div",
              "children": [],
              "attribute": []
            }
          ],
          "attribute": []
        }
      ],
      "attribute": []
    }
  ]
}
复制代码

希望和大家交流

文章分类
前端
文章标签