背景:需求需要把 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 = /[\:\w\d_-]*?=(["].*?["]|[‘].*?[‘])/g const attrReg = /(?<=\s)([\:\w\d\-]+\=(["‘].*?["‘]|[\w\d]+)|\w+)/g const numReg = /^\d+$/ const clReg = /\n/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> `
属性值转义
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>`
形成内容数组
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>‘ },children: [ { attrs: {},children: [ { attrs: {},children: [],tagName: ‘text‘,data: ‘hello 2‘ } ],tagName: ‘p‘ },{ attrs: { type: ‘text‘,value: ‘hello 3‘ },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... 还算行,先用着。
最后
写代码么...开心就好
如果您对我们团队感兴趣,欢迎加入,期待您的加入,可以投递我的邮箱
[email protected] !
更多岗位可以查看
Tuya 招聘
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。