这是我参与更文挑战的第7天,活动详情查看: 更文挑战
写作背景
一个小伙伴给的一道题, 把下列字符串解析成一个类似DOM树的存在;
应该是字符串的,但是为了获得高亮方便阅读,我就直接用标签了
<el-tabs v-model="activeName" type="card">
<el-tab-pane label="图" name="view">
<div class="code">
<input />
</div>
</el-tab-pane>
<el-tab-pane label="JSON" name="sourceCode">
<div class="code">
<json-viewer :value="jsonValue" copyable boxed sort class="json-editor" ></json-viewer>
</div>
</el-tab-pane>
</el-tabs>
<!-- 解析结果 -->
<script>
{
name: 'el-tabs',
children: [
{
name: 'el-tab-pane',
children: []
}
]
}
</script>
解题思路
这一段文字可能会有点点枯燥,但是我最真实的想法, 如果你暂时不想看没关系,可以看完全篇之后,想深入理解如果找到思路,可以回来看看我的脑回路。
在我看完这道题之后,我转手就艾特了小群里另一位小伙伴, 问他有没有什么思路, 他上来就是一句:“这不是虚拟DOM吗? 我以前用堆栈实现过。”
本来是想直接看看他的代码的, 但是并没有,他找不着也忘记怎么写了。(不然也没这篇文章了)
然后我就带着这道题是上班了,经过下午的汗撒工位之后,啥也没做,因为上班忙啊。
不过在下班回家的路上, 我就在思考这道题了,小伙伴提到过用堆栈,这不失为一个解题方法,但是我对这种解法来解这类题不是很擅长。
想挑战下的小伙伴可以自己试试用堆栈实现。
之后我又想到了正则匹配,正则匹配之后,用正则的match方法可以获取所有匹配到的字符串,然后结合堆栈来实现。
但是这方面的练习做的也少,脑子有点短路想不通。
最后的最后我想到了我之前写的一篇关于字符串解析成标题树的题,也是他提的,好像可以借助那道题的思路。
最后嘛,也是不了了之,因为还是有点差距的。
所以在最后,我想到了一个很黄很暴力的解法,遍历+正则+堆栈,来实现,不过最终代码中,堆栈没用上,就删掉了。
开始写了。
大概的思路有了以后,就可以了,毕竟我还做不到纯靠脑补就完成所有的逻辑。
需要实践来验错。
一、创建基础结构
这一块主要是参考了下之前标题树那道题的结构,正好差不多就拿来用了。
相比起文章开头的结果结构要多了一些属性,后续我会把他删除,但是如果需要,也可以保留。
class Node {
/**
* @param {String} aTag 标签名
* @param {Object} props 标签所含有的属性
* @param {String} value 可选 标签内容 待完善
* @param {Array} parent 当前标签的父级
*/
constructor({ aTag, props, parent }) {
this.aTag = aTag;
this.props = props;
// this.value = value;
this.parent = parent || [];
this.children = [];
}
}
二、创建主函数
function parseDOM(str){
let reg = /<([^<>]*)>/g; // 正则匹配标签 <div props="value">
let res, temp, tag,
parent = new Node({aTag: '', props: ''}); // 初始化父级
temp = str.match(reg);
return parent.children;
}
这个就是主函数一个基础架构了,先创建好需要的变量,以及匹配所有的标签。
最终代码肯定是需要完善主函数的, 先把其他工具函数写好,回头来完善这一部分的代码。
temp结果如下
三、解析标签函数
这个函数的主要作用在于将上图中的标签进行解析,拆分为标签名、标签属性、以及确认是否是单标签
function splitTag(tag){
let cut = tag.replace(/<|>|"|'/g, ''); // 将尖括号<> 单双引号"'都去除,免得影响美观
let res = {},
props = {},
temp = [];
res['tagName'] = cut.shift(); // 记录tagName, 用shift()可以不用计算
if(cut.length){ // 如果还有剩余值,表明还有属性需要处理
for(let item of cut){
if(temp[0] == '/'){ // 假设不会有用户用“/” 作为属性名
props['singleTag'] = true; // 标记为单标签
} else {
// Vue中有:value 的写法,去除冒号, 如果是单属性, 设置默认值为true
props[temp.shift().replace(':','')] = temp.shift() || true;
}
}
res.props = props;
}
return res; // {tagName: 'div', props: {name: "value"}}
}
写完这个工具函数,就完成了一大半了,这时候打印解析过的标签可以看到如下图
四、完善主函数,完成解析
能打印出如上图的解析结果,基本完成三分之一了。
剩下的三分之二就是用正确的父子顺序插入到对象中了。
function parseDOM(str){
let reg = /<([^<>]*)>/g; // 匹配所有的标签 <div props="value">
let res, temp, tag,
parent = new Node({aTag: '', props: ''}); // 初始化父级
temp = str.match(reg);
temp.map(item => {
tag = splitTag(item); // 调用解析函数
if(tag.tagName.indexOf('/') == 0 ){ // 如果有斜杠开头,说明是结束标签,向上一级
parent = parent.parent; // 重新指向父级
} else if(tag.props?.singleTag == true){ // 如果为单标签 存入当前子级,父级不变.
// 这里注意判断是要使用可选链,否则会出现singleTag未定义的情况
res = new Node({aTag: tag.tagName, props: tag.props, parent: parent});
parent.children.push(res)
} else { // 双标签的开始抱歉,存入当前子级,并将子级设置为新的父级
res = new Node({aTag: tag.tagName, props: tag.props, parent: parent});
parent.children.push(res)
parent = parent.children[parent.children.length - 1];
}
})
return parent.children;
}
完善了主函数之后,基本就大功告成了。
五、去除父级和属性
就在自我感觉良好的时候, 小伙伴来了一句有点乱,希望保留标签名和子级。
本来今天高高兴兴的。
看着我的代码,和打印结果,我陷入了沉思。
要不要打他一顿呢, 套麻袋还是套垃圾袋呢?
突然,我脑子一动,这个结构想什么? 因为最多只有两个子节点,像不像一颗二叉树。
但是考虑到字节点是会变的, 那最少也是一棵树。既然是书,那就可以先序、中序、后序遍历啊。
只见我电光火石见有了如下函数
function deleteParentProps(data){
if(+data) return // 如果是空数组 就return,不在向下
data.children.map(item => deleteParentProps(item))
delete data.parent
delete data.props // 想要保留解析的属性的话,可以不删
}
加上这个函数之后, 在主函数的return之前 加入下面的代码
deleteParentProps(parent);
return parent.children;
看最终效果
相对干净整洁了很多, 但是没有标签属性,没有灵魂。
六、文本节点
这个时候又要扩充一下了, 标签内怎么会没有文本呢? 所以我对原字符串进行了扩充
<el-tabs v-model="activeName" type="card">
<el-tab-pane label="图" name="view">
<div class="code">
这是一段文字
<input />
</div>
</el-tab-pane>
这里也是一段文字
<el-tab-pane label="JSON" name="sourceCode">
<div class="code">
<json-viewer :value="jsonValue" copyable boxed sort class="json-editor" ></json-viewer>
</div>
</el-tab-pane>
</el-tabs>
既然对文字进行扩充了,那么首当其冲的,就是主函数内的正则了
// 原正则为
let reg = /<([^<>]*)>/g;
// 修改为
let reg = /<([^<>]*)>|(\S.*)/g;
之后在主函数中添加对文本节点的处理
// 主函数中的代码不全部写出来了
temp.map(item => {
// 修改1
if(item.indexOf('<') == 0) tag.splitTag(item)
// 模拟splitTag的结果,添加文本标志,将文本作为tagName
else tag = {props: {'text': true}, tagName: item}
// 修改2
if(tag.tagName?.indexOf('/') === 0 ){
} else if(tag.props?.singleTag === true){
} else if(tag.props?.text === true){ // 增加了对文本的判断及处理
res = new Node({aTag: tag.tagName, props: null, type: "Text", parent: parent});
parent.children.push(res);
} else {
}
})
删除了props和parent,方便查看,最终效果图如下,
总结
想来应该是可以优化的, 甚至我这种写法也不算最优解吧。 只是实在太晚了,没时间优化了。
明天还得早起搬砖呢, 今天的砖格外烫手啊。
愿心情美丽,愿我爱之人、爱我之人一切安好。