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 = /[\:\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>
`
属性值转义
拿到字符串 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 招聘

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
类加载器中的双亲委派模型详解
在上一篇文章中,我们梳理了类加载器的基本概念:类的生命周期、类加载器的作用、类的加载和卸载的时机等等,这篇文章我们接着前文继续复习类加载器的知识,主要包括:JVM中有哪些类加载器?它们之间是什么关系?什么是双亲委派机制? 双亲委派模型 四种类加载器 从JVM的角度看,类加载器主要有两类:Bootstrap ClassLoader和其他类加载,Bootstrap ClassLoader是C++语言实现,是虚拟机自身的一部分;其他类加载器都是Java语言实现,不属于虚拟机,全部继承自抽象类java.lang.ClassLoader。 从Java开发者的角度看,需要了解类加载器的双亲委派模型,如下图所示: Bootstrap ClassLoader:启动类加载器,这个类加载器将负责存放在/lib目录中、被-Xbootclasspath参数所指定的路径中,并且是虚拟机会识别的jar类库加载到内存中。更直白点说,就是我们常用的java.lang开头的那些类,一定是被Bootstrap ClassLoader加载的。 Extension ClassLoader:扩展类加载器,这个类加载器由sun....
-
下一篇
《阿里云前端技术周刊》第十九期
作者: @语安 校对:@行剑 @牧曈 知乎:阿里云中台前端/全栈团队专栏 Github:阿里云前端技术周刊 给我们投稿:传送门 参与交流:传送门 前端速报 React 新的DevTools 带来新的改动,现在可以在Chrome,Firefox和(Chromium)Edge中使用。传送门 V8发布 V7.7。这次发布的主要一些亮点在于:1.性能(大小和速度)上:延迟反馈分配;可扩展WebAssembly后台编译以及堆栈跟踪改进。2. JavaScript语言特性:Intl.NumberFormat API在此版本中新增了功能。传送门 第5届 FEDAY 将于 9.21 号在成都举办,欢迎大家戳->传送门 趣前端 reveal.js,用来做 HTML 幻灯片的框架,支持 HTML 和 Markdown 语法。传送门 Lugia 是一整套面向云原生化大前端生态解决方案。希望把交互设计与前端应用代码开发有机的融为一体,形成一种跨时代的大前端生态技术规范。传送门 Demo!CSS 也能实现一个很酷的骏马效果。传送门 编者推荐 checkValidity 等 form 原生 JS 验证方法和...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8编译安装MySQL8.0.19
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- MySQL8.0.19开启GTID主从同步CentOS8
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Dcoker安装(在线仓库),最新的服务器搭配容器使用
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- MySQL数据库在高并发下的优化方案
- Docker容器配置,解决镜像无法拉取问题