<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          有人問我能不能寫一個(gè) HTML Parser?

          共 9633字,需瀏覽 20分鐘

           ·

          2021-07-01 03:40

          上篇文章介紹了手寫簡(jiǎn)易瀏覽器整體的思路,這篇開始寫 html parser。

          思路分析

          實(shí)現(xiàn) html parser 主要分為詞法分析和語(yǔ)法分析兩步。

          詞法分析

          詞法分析需要把每一種類型的 token 識(shí)別出來,具體的類型有:

          • 開始標(biāo)簽,如 <div>
          • 結(jié)束標(biāo)簽,如 </div>
          • 注釋標(biāo)簽,如 <!--comment-->
          • doctype 標(biāo)簽,如 <!doctype html>
          • text,如 aaa

          這是最外層的 token,開始標(biāo)簽內(nèi)部還要分出屬性,如 id="aaa" 這種。

          也就是有這幾種情況:

          第一層判斷是否包含 <,如果不包含則是 text,如果包含則再判斷是哪一種,如果是開始標(biāo)簽,還要對(duì)其內(nèi)容再取屬性,直到遇到 > 就重新判斷。

          語(yǔ)法分析

          語(yǔ)法分析就是對(duì)上面分出的 token 進(jìn)行組裝,生成 ast。

          html 的 ast 的組裝主要是考慮父子關(guān)系,記錄當(dāng)前的 parent,然后 text、children 都設(shè)置到當(dāng)前 parent 上。

          我們來用代碼實(shí)現(xiàn)一下:

          代碼實(shí)現(xiàn)

          詞法分析

          首先,我們要把 startTag、endTag、comment、docType 還有 attribute 的正則表達(dá)式寫出來:

          正則

          • 結(jié)束標(biāo)簽就是 </ 開頭,然后 a-zA-Z0-9 和 - 出現(xiàn)多次,之后是 >
          const endTagReg = /^<\/([a-zA-Z0-9\-]+)>/;
          • 注釋標(biāo)簽是 <!-- 和 --> 中間夾著非 --> 字符出現(xiàn)任意次
          const commentReg = /^<!\-\-[^(-->)]*\-\->/;
          • doctype 標(biāo)簽是 <!doctype 加非 > 字符出現(xiàn)多次,加 >
          const docTypeReg = /^<!doctype [^>]+>/;
          • attribute 是多個(gè)空格開始,加 a-zA-Z0-9 或 - 出現(xiàn)多次,接一個(gè) =,之后是非 > 字符出多次
          const attributeReg = /^(?:[ ]+([a-zA-Z0-9\-]+=[^>]+))/;
          • 開始標(biāo)簽是 < 開頭,接 a-zA-Z0-9 和 - 出現(xiàn)多次,然后是屬性的正則,最后是 > 結(jié)尾
          const startTagReg = /^<([a-zA-Z0-9\-]+)(?:([ ]+[a-zA-Z0-9\-]+=[^> ]+))*>/;

          分詞

          之后,我們就可以基于這些正則來分詞,第一層處理 < 和 text:

          function parse(html, options{
              function advance(num{
                  html = html.slice(num);
              }

              while(html){
                  if(html.startsWith('<')) {
                      //...
                  } else {
                      let textEndIndex = html.indexOf('<');
                      options.onText({
                          type'text',
                          value: html.slice(0, textEndIndex)
                      });
                      textEndIndex = textEndIndex === -1 ? html.length: textEndIndex;
                      advance(textEndIndex);
                  }
              }
          }

          第二層處理 <!-- 和 <!doctype 和結(jié)束標(biāo)簽、開始標(biāo)簽:

          const commentMatch = html.match(commentReg);
          if (commentMatch) {
              options.onComment({
                  type'comment',
                  value: commentMatch[0]
              })
              advance(commentMatch[0].length);
              continue;
          }

          const docTypeMatch = html.match(docTypeReg);
          if (docTypeMatch) {
              options.onDoctype({
                  type'docType',
                  value: docTypeMatch[0]
              });
              advance(docTypeMatch[0].length);
              continue;
          }

          const endTagMatch = html.match(endTagReg);
          if (endTagMatch) {
              options.onEndTag({
                  type'tagEnd',
                  value: endTagMatch[1]
              });
              advance(endTagMatch[0].length);
              continue;
          }

          const startTagMatch = html.match(startTagReg);
          if(startTagMatch) {    
              options.onStartTag({
                  type'tagStart',
                  value: startTagMatch[1]
              });

              advance(startTagMatch[1].length + 1);
              let attributeMath;
              while(attributeMath = html.match(attributeReg)) {
                  options.onAttribute({
                      type'attribute',
                      value: attributeMath[1]
                  });
                  advance(attributeMath[0].length);
              }
              advance(1);
              continue;
          }

          經(jīng)過詞法分析,我們能拿到所有的 token:

          語(yǔ)法分析

          token 拆分之后,我們需要再把這些 token 組裝在一起,只處理 startTag、endTag 和 text 節(jié)點(diǎn)。通過 currentParent 記錄當(dāng)前 tag。

          • startTag 創(chuàng)建 AST,掛到 currentParent 的 children 上,然后 currentParent 變成新創(chuàng)建的 tag
          • endTag 的時(shí)候把 currentParent 設(shè)置為當(dāng)前 tag 的 parent
          • text 也掛到 currentParent 上
          function htmlParser(str{
              const ast = {
                  children: []
              };
              let curParent = ast;
              let prevParent = null;
              const domTree = parse(str,{
                  onComment(node) {
                  },
                  onStartTag(token) {
                      const tag = {
                          tagName: token.value,
                          attributes: [],
                          text'',
                          children: []
                      };
                      curParent.children.push(tag);
                      prevParent = curParent;
                      curParent = tag;
                  },
                  onAttribute(token) {
                      const [ name, value ] = token.value.split('=');
                      curParent.attributes.push({
                          name,
                          value: value.replace(/^['"]/'').replace(/['"]$/'')
                      });
                  },
                  onEndTag(token) {
                      curParent = prevParent;
                  },
                  onDoctype(token) {
                  },
                  onText(token) {
                      curParent.text = token.value;
                  }
              });
              return ast.children[0];
          }

          我們?cè)囈幌滦Ч?/p>

          const htmlParser = require('./htmlParser');

          const domTree = htmlParser(`
          <!doctype html>
          <body>
              <div>
                  <!--button-->
                  <button>按鈕</button>
                  <div id="container">
                      <div class="box1">
                          <p>box1 box1 box1</p>
                      </div>
                      <div class="box2">
                          <p>box2 box2 box2</p>
                      </div>
                  </div>
              </div>
          </body>
          `
          );

          console.log(JSON.stringify(domTree, null4));

          成功生成了正確的 AST。

          總結(jié)

          這篇是簡(jiǎn)易瀏覽器中 html parser 的實(shí)現(xiàn),少了自閉合標(biāo)簽的處理,就是差一個(gè) if else,后面會(huì)補(bǔ)上。

          我們分析了思路并進(jìn)行了實(shí)現(xiàn):通過正則來進(jìn)行 token 的拆分,把拆出的 token 通過回調(diào)函數(shù)暴露出去,之后進(jìn)行 AST 的組裝,需要記錄當(dāng)前的 parent,來生成父子關(guān)系正確的 AST。

          html parser 其實(shí)也是淘系前端的多年不變的面試題之一,而且 vue template compiler 還有 jsx 的 parser 也會(huì)用到類似的思路。還是有必要掌握的。希望本文能幫大家理清思路。

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

          瀏覽 74
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产77777 | 中文字幕成人免费视频 | 五月开心激情网 | 中文字幕无码不卡免费视频 | 无码一区二区四区 |