• HTML-Parser


    背景:需求需要把 html 字符串转成 DOM 对象树或者 js 对象树,然后进行一些处理/操作。htmlparser 这个库还行,但是对 attribute 上一些特殊属性值转换不行,同时看了看`开标签语法`(syntax-start-tag:whatwg)、`html-attribute 的支持规则`(attributes:whatwg) 和一些其他库的实现,在一些边界场景(特殊属性值和web component)处理还是缺少,算了... 自己撸了个 html parser 的函数么好了。

    本文主要是记录下实现过程,做个技术沉淀,有相关需求的可以做个参考。

    前期处理

    首先,定义一些正则表达式,用以匹配希望找到的内容
    const ltReg = /</g
    const gtReg = />/g
    const sqReg = /'/g
    const qReg = /"/g
    const sqAttrReg = /(?<==')[^']*?(?=')/g
    const qAttrReg = /(?<==")[^"]*?(?=")/g
    const qRegBk = /"/g
    const sqRegBk = /'/g
    const ltRegBk = /</g
    const gtRegBk = />/g
    const attrReplaceReg = /[:wd_-]*?=(["].*?["]|['].*?['])/g
    const attrReg = /(?<=s)([:wd-]+=(["'].*?["']|[wd]+)|w+)/g
    const numReg = /^d+$/
    const clReg = /
    /g
    const sReg = /s/g
    const spReg = /s+/g
    const tagReg = /<[^<>]*?>/
    const startReg = /<[^/!].*?>/
    const endReg = /</.*?>/
    const commentReg = /(?<=<!--).*?(?=-->)/
    const tagCheckReg = /(?<=<)[w-]+/

    开始处理逻辑,拿个简单的 html 字符串做例子。
    const str = `
    <div id="container">
      <div class="test" data-html="<p>hello 1</p>">
        <p>hello 2</p>
        <input type="text" value="hello 3" >
      </div>
    </div>
    `

    属性值转义

    拿到字符串 str,取各个开标签,并将标签内的 attribute 里的特殊字符做转义字符替换,返回字符串 str1
    const replaceAttribute = (html: string): string => {
      return html.replace(attrReplaceReg, v => {
        return v
          .replace(ltReg, '<')
          .replace(gtReg, '>')
          .replace(sqAttrReg, v => {
            return v.replace(qReg, '"')
          })
          .replace(qAttrReg, v => {
            return v.replace(sqReg, ''')
          })
      })
    }

    结果如下:
    ;`<div id="container">
      <div class="test" data-html="<p>hello 1</p>">
        <p>hello 2</p>
        <input type="text" value="hello 3" >
      </div>
    </div>`

    形成内容数组

    从上一步的字符串 str1 中截取出元素(元素是: 开标签、内容、闭合标签),放入新数组 arr。
    const convertStringToArray = (html: string) => {
      let privateHtml = html
      let temporaryHtml = html
      const arr = []
      while (privateHtml.match(tagReg)) {
        privateHtml = temporaryHtml.replace(tagReg, (v, i) => {
          if (i > 0) {
            const value = temporaryHtml.slice(0, i)
            if (value.replace(sReg, '').length > 0) {
              arr.push(value)
            }
          }
          temporaryHtml = temporaryHtml.slice(i + v.length)
          arr.push(v)
          return ''
        })
      }
      return arr
    }

    结果如下:
     ["<div id="container">", "<div class="test" data-html="<p>hello 1</p>">", "<p>", "hello 2", "</p>", "<input type="text" value="hello 3" >", "</div>", "</div>"]

    生成对象树

    循环上一步形成的 arr,处理成对象树
    // 单标签集合
    var singleTags = [
      'img',
      'input',
      'br',
      'hr',
      'meta',
      'link',
      'param',
      'base',
      'basefont',
      'area',
      'source',
      'track',
      'embed'
    ]
    // 其中 DomUtil 是根据 nodejs 还是 browser 环境生成 js 对象/ dom 对象的函数
    var makeUpTree = function(arr) {
      var root = DomUtil('container')
      var deep = 0
      var parentElements = [root]
      arr.forEach(function(i) {
        var parentElement = parentElements[parentElements.length - 1]
        if (parentElement) {
          var inlineI = toOneLine(i)
          // 开标签处理,新增个开标签标记
          if (startReg.test(inlineI)) {
            deep++
            var tagName = i.match(tagCheckReg)
            if (!tagName) {
              throw Error('标签规范错误')
            }
            var element_1 = DomUtil(tagName[0])
            var attrs = matchAttr(i)
            attrs.forEach(function(attr) {
              if (element_1) {
                element_1.setAttribute(attr[0], attr[1])
              }
            })
            parentElement.appendChild(element_1)
            // 单标签处理,deep--,完成一次闭合标记
            if (
              singleTags.indexOf(tagName[0]) > -1 ||
              i.charAt(i.length - 2) === '/'
            ) {
              deep--
            } else {
              parentElements.push(element_1)
            }
          }
          // 闭合标签处理
          else if (endReg.test(inlineI)) {
            deep--
            parentElements.pop()
          } else if (commentReg.test(inlineI)) {
            var matchValue = i.match(commentReg)
            var comment = matchValue ? matchValue[0] : ''
            deep++
            var element = DomUtil('comment', comment)
            parentElement.appendChild(element)
            deep--
          } else {
            deep++
            var textElement = DomUtil('text', i)
            parentElement.appendChild(textElement)
            deep--
          }
        }
      })
      if (deep < 0) {
        throw Error('存在多余闭合标签')
      } else if (deep > 0) {
        throw Error('存在多余开标签')
      }
      return root.children
    }

    结果如下:
    [
      {
        attrs: {
          id: 'container'
        },
        parentElement: [DomElement],
        children: [
          {
            attrs: {
              class: 'test',
              'data-html': '<p>hello 1</p>'
            },
            parentElement: [DomElement],
            children: [
              {
                attrs: {},
                parentElement: [DomElement],
                children: [
                  {
                    attrs: {},
                    parentElement: [DomElement],
                    children: [],
                    tagName: 'text',
                    data: 'hello 2'
                  }
                ],
                tagName: 'p'
              },
              {
                attrs: {
                  type: 'text',
                  value: 'hello 3'
                },
                parentElement: [DomElement],
                children: [],
                tagName: 'input'
              }
            ],
            tagName: 'div'
          }
        ],
        tagName: 'div'
      }
    ]
    

    组合

    组合以上的 3 个步骤
    const Parser = (html: string) => {
      const htmlAfterAttrsReplace = replaceAttribute(html)
      const stringArray = convertStringToArray(htmlAfterAttrsReplace)
      const domTree = makeUpTree(stringArray)
      return domTree
    }
    

    测试

    最后肯定的要测试一波。

    把 tuya / taobao / baidu / jd / tx 的首页或者新闻页都拷贝了 html 试了一波,基本在 `100ms` 内执行完,并且 dom 数量大概在几千的样子,对比了一番, html 字符串上的标签属性和对象的 attrs 对象,都还对应的上。
     
    emm... 还算行,先用着。

    最后

    写代码么...开心就好

    如果您对我们团队感兴趣,欢迎加入,期待您的加入,可以投递我的邮箱 liaojc@tuya.com !

    更多岗位可以查看 Tuya 招聘
  • 相关阅读:
    Django源码解析(1):启动程序
    python之importlib模块
    Django中间件:CsrfViewMiddleware
    Django的admin组件
    Linux学习之CentOS--CentOS6.4下Mysql数据库的安装与配置【转】
    C#读取Xml【转】
    在eclipse导入项目的步骤【转】
    Spring学习(一)——Spring中的依赖注入简介【转】
    Spring学习(二)——Spring中的AOP的初步理解[转]
    Spring之AOP
  • 原文地址:https://www.cnblogs.com/ys-ys/p/11668981.html
Copyright © 2020-2023  润新知