手写简易浏览器之Html Parser 篇

系统 浏览器
这篇是简易浏览器中 html parser 的实现,少了自闭合标签的处理,就是差一个 if else,后面会补上。

 [[403967]]

本文转载自微信公众号「神光的编程秘籍」,作者神说要有光zxg。转载本文请联系神光的编程秘籍公众号。

思路分析

实现 html parser 主要分为词法分析和语法分析两步。

词法分析

词法分析需要把每一种类型的 token 识别出来,具体的类型有:

  • 开始标签,如 <div>
  • 结束标签,如 </div>
  • 注释标签,如 <!--comment-->
  • doctype 标签,如 <!doctype html>
  • text,如 aaa

这是最外层的 token,开始标签内部还要分出属性,如 id="aaa" 这种。

也就是有这几种情况:

第一层判断是否包含 <,如果不包含则是 text,如果包含则再判断是哪一种,如果是开始标签,还要对其内容再取属性,直到遇到 > 就重新判断。

语法分析

语法分析就是对上面分出的 token 进行组装,生成 ast。

html 的 ast 的组装主要是考虑父子关系,记录当前的 parent,然后 text、children 都设置到当前 parent 上。

我们来用代码实现一下:

代码实现

词法分析

首先,我们要把 startTag、endTag、comment、docType 还有 attribute 的正则表达式写出来:

正则

结束标签就是

  1. const endTagReg = /^<\/([a-zA-Z0-9\-]+)>/; 

注释标签是 中间夹着非 --> 字符出现任意次

  1. const commentReg = /^<!\-\-[^(-->)]*\-\->/; 

doctype 标签是 字符出现多次,加 >

  1. const docTypeReg = /^<!doctype [^>]+>/; 

attribute 是多个空格开始,加 a-zA-Z0-9 或 - 出现多次,接一个 =,之后是非 > 字符出多次

  1. const attributeReg = /^(?:[ ]+([a-zA-Z0-9\-]+=[^>]+))/; 

开始标签是 < 开头,接 a-zA-Z0-9 和 - 出现多次,然后是属性的正则,最后是 > 结尾

  1. const startTagReg = /^<([a-zA-Z0-9\-]+)(?:([ ]+[a-zA-Z0-9\-]+=[^> ]+))*>/; 

分词

之后,我们就可以基于这些正则来分词,第一层处理 < 和 text:

  1. function parse(html, options) { 
  2.     function advance(num) { 
  3.         html = html.slice(num); 
  4.     } 
  5.  
  6.     while(html){ 
  7.         if(html.startsWith('<')) { 
  8.             //... 
  9.         } else { 
  10.             let textEndIndex = html.indexOf('<'); 
  11.             options.onText({ 
  12.                 type: 'text'
  13.                 value: html.slice(0, textEndIndex) 
  14.             }); 
  15.             textEndIndex = textEndIndex === -1 ? html.length: textEndIndex; 
  16.             advance(textEndIndex); 
  17.         } 
  18.     } 

第二层处理 <!-- 和 <!doctype 和结束标签、开始标签:

  1. const commentMatch = html.match(commentReg); 
  2. if (commentMatch) { 
  3.     options.onComment({ 
  4.         type: 'comment'
  5.         value: commentMatch[0] 
  6.     }) 
  7.     advance(commentMatch[0].length); 
  8.     continue
  9.  
  10. const docTypeMatch = html.match(docTypeReg); 
  11. if (docTypeMatch) { 
  12.     options.onDoctype({ 
  13.         type: 'docType'
  14.         value: docTypeMatch[0] 
  15.     }); 
  16.     advance(docTypeMatch[0].length); 
  17.     continue
  18.  
  19. const endTagMatch = html.match(endTagReg); 
  20. if (endTagMatch) { 
  21.     options.onEndTag({ 
  22.         type: 'tagEnd'
  23.         value: endTagMatch[1] 
  24.     }); 
  25.     advance(endTagMatch[0].length); 
  26.     continue
  27.  
  28. const startTagMatch = html.match(startTagReg); 
  29. if(startTagMatch) {     
  30.     options.onStartTag({ 
  31.         type: 'tagStart'
  32.         value: startTagMatch[1] 
  33.     }); 
  34.  
  35.     advance(startTagMatch[1].length + 1); 
  36.     let attributeMath; 
  37.     while(attributeMath = html.match(attributeReg)) { 
  38.         options.onAttribute({ 
  39.             type: 'attribute'
  40.             value: attributeMath[1] 
  41.         }); 
  42.         advance(attributeMath[0].length); 
  43.     } 
  44.     advance(1); 
  45.     continue

经过词法分析,我们能拿到所有的 token:

语法分析

token 拆分之后,我们需要再把这些 token 组装在一起,只处理 startTag、endTag 和 text 节点。通过 currentParent 记录当前 tag。

  • startTag 创建 AST,挂到 currentParent 的 children 上,然后 currentParent 变成新创建的 tag
  • endTag 的时候把 currentParent 设置为当前 tag 的 parent
  • text 也挂到 currentParent 上
  1. function htmlParser(str) { 
  2.     const ast = { 
  3.         children: [] 
  4.     }; 
  5.     let curParent = ast; 
  6.     let prevParent = null
  7.     const domTree = parse(str,{ 
  8.         onComment(node) { 
  9.         }, 
  10.         onStartTag(token) { 
  11.             const tag = { 
  12.                 tagName: token.value, 
  13.                 attributes: [], 
  14.                 text: ''
  15.                 children: [] 
  16.             }; 
  17.             curParent.children.push(tag); 
  18.             prevParent = curParent; 
  19.             curParent = tag; 
  20.         }, 
  21.         onAttribute(token) { 
  22.             const [ name, value ] = token.value.split('='); 
  23.             curParent.attributes.push({ 
  24.                 name
  25.                 value: value.replace(/^['"]/, '').replace(/['"]$/, ''
  26.             }); 
  27.         }, 
  28.         onEndTag(token) { 
  29.             curParent = prevParent; 
  30.         }, 
  31.         onDoctype(token) { 
  32.         }, 
  33.         onText(token) { 
  34.             curParent.text = token.value; 
  35.         } 
  36.     }); 
  37.     return ast.children[0]; 

我们试一下效果:

  1. const htmlParser = require('./htmlParser'); 
  2.  
  3. const domTree = htmlParser(` 
  4. <!doctype html> 
  5. <body> 
  6.     <div> 
  7.         <!--button--> 
  8.         <button>按钮</button> 
  9.         <div id="container"
  10.             <div class="box1"
  11.                 <p>box1 box1 box1</p> 
  12.             </div> 
  13.             <div class="box2"
  14.                 <p>box2 box2 box2</p> 
  15.             </div> 
  16.         </div> 
  17.     </div> 
  18. </body> 
  19. `); 
  20.  
  21. console.log(JSON.stringify(domTree, null, 4)); 

成功生成了正确的 AST。

总结

这篇是简易浏览器中 html parser 的实现,少了自闭合标签的处理,就是差一个 if else,后面会补上。

我们分析了思路并进行了实现:通过正则来进行 token 的拆分,把拆出的 token 通过回调函数暴露出去,之后进行 AST 的组装,需要记录当前的 parent,来生成父子关系正确的 AST。

html parser 其实也是淘系前端的多年不变的面试题之一,而且 vue template compiler 还有 jsx 的 parser 也会用到类似的思路。还是有必要掌握的。希望本文能帮大家理清思路。

代码在 github:https://github.com/QuarkGluonPlasma/tiny-browser

 

责任编辑:武晓燕 来源: 神光的编程秘籍
相关推荐

2021-06-04 05:16:33

浏览器js源码

2018-07-31 11:20:26

2012-05-07 14:24:15

HTML 5Web App

2012-05-28 13:09:12

HTML5

2012-04-23 13:43:02

HTML5浏览器

2012-03-20 11:31:58

移动浏览器

2012-03-19 17:25:22

2012-03-20 11:41:18

海豚浏览器

2012-03-20 11:07:08

2009-07-29 08:50:10

Windows 7浏览器欧洲版

2013-11-20 10:47:57

浏览器渲染html

2022-01-24 13:46:24

框架

2021-02-06 12:25:42

微软Chromium浏览器

2012-06-21 15:38:02

猎豹浏览器

2010-04-05 21:57:14

Netscape浏览器

2012-03-19 17:17:00

移动浏览器欧朋

2012-03-20 11:22:02

QQ手机浏览器

2012-05-17 09:45:30

2012-03-20 11:35:32

傲游手机浏览器

2013-08-16 17:50:13

点赞
收藏

51CTO技术栈公众号