前言
继续我的自建博客之旅,在搭建网站的时候发现重复的模块太多了,每个页面都有大量的相同的功能和样式的HTML代码,我就一直复制粘贴,十分头疼🙃。于是我想能不能找個方法解決下呢🤔?看着我用的 vscode 里用的 Emmet,我陷入了沉思,嗯,有点意思,不如就这么办吧!
思路与原理
想要实现的功能
我想实现的功能就是把下面这样形式的一组字符串,解析成 HTML 代码,然后放到 Dom 树里,然后我只要在页面里引入相对应的函数,就能把对应的 Dom 添加上去,岂不是美滋滋😄。
"(div[class="user-img]>img[src="./favicon.ico])+h2[class="font-regular]{Raydaydayup}+p{👉好好学习,天天向上👈}"
<!-- 生成的DOM结构 -->
<div class="user-img">
<img src="./favicon.ico">
</div>
<h2 class="font-regular">Raydaydayup</h2>
<p>👉好好学习,天天向上👈</p>
复制代码
要实现这个功能得分两步:
- 把字符串解析成 Dom
- 把 Dom 添加到页面指定位置
如何把字符串变成DOM?
怎么办?我也不会啊🙃,毕竟小白一个,开始各种搜索解决思路,然后我发现了一个神奇的概念—— AST抽象语法树。具体什么概念的我也不太懂,我只需要知道,我可以用 js
来表示 HTML
结构了,像这样👇(和单链表相似):
let a = {
tagName: 'div',
textNode: '我是一个文本节点',
prop: {
class: 'box'
},
children: [
{
tagName: 'span',
textNode: '我是一个文本节点',
prop: {
class: 'span1'
}
},
{
tagName: 'span',
textNode: '我是一个文本节点',
prop: {
class: 'span2'
}
}
]
}
复制代码
表示下面这个 Html
结构
<div class="box">
我是一个文本节点
<span class="span1">我是一个文本节点</span>
<span class="span2">我是一个文本节点</span>
</div>
复制代码
我只要字符串解析成AST抽象语法树,然后用 createElement
,创建DOM元素,然后挂载到DOM树上去,就可以了
那么问题又来了,如何把固定格式的字符串表示成 AST抽象语法树呢?
如何把固定格式的字符串表示成 AST 抽象语法树呢?
就是上边那个文章(AST抽象语法树)里提到的词法分析,我的理解是,我定义一堆操作规则,符合定义的字符串就被分类,这个字符串就代表我对应的操作。如下:
- 符号含义:
>
父子关系+
兄弟关系[]
属性{}
文本()
优先运算
- 符号优先级:
()
大于>
等于+
大于[]
等于{}
- 要点:
- 任何符号之间的纯字母都是标签名
()
解析的函数(确定优先级)[]
解析的函数 (确定属性){}
解析的函数(确定文本内容)
那么对于一个字符串的解析思路为例):
- 从左到右取字符串的第一个字符进行判断
- 如果是字母,那么这个字母开始连续的字母组成的字符串是标签名,(以
div{level-0}>(a[href="#]{level-1}+span[class="text]{level-1}
为例),即为div
,截取div
设置tagName
,余下部分({level-0}>(a[href="#]{level-1}+span[class="text]{level-1})
)继续向下执行第二步 - 如果为
{
,那么截取{}
,其中内容设置textNode
,余下部分继续向下执行第二步 - 如果为
[
,那么截取[]
,其中内容以"=
为分隔符,设置prop
,余下部分继续向下执行第二步 - 如果为
>
,那么截去>
,当前层级标记level
加上1,余下部分执行第二步 - 如果为
+
,那么截去>
,当前层级标记level
不变,余下部分执行第二步 - 如果为
(
,那么截去(
,当前层级标记level
不变,余下部分执行第二步 - 如果为
)
,那么截去)
,当前层级标记level
减1,余下部分执行第二步
- 如果是字母,那么这个字母开始连续的字母组成的字符串是标签名,(以
- 判断字符串是否取完,没取完继续返回第一步执行,取完则结束
那么最终div{level-0}>(a[href="#]{level-1}+span[class="text]{level-1}
最终转换为
[
{
level: 0,
prop: {},
tagName: 'div',
textNode: 'level-0'
},
{
level: 1,
prop: { href: '#' },
tagName: 'a',
textNode: 'level-1'
},
{
level: 1,
prop: { class: 'text' },
tagName: 'span',
textNode: 'level-1'
}
]
复制代码
然后再通过遍历数组,判断 level
,将其转换为一开始所说的形式,即
[
{
level: 0,
prop: {},
tagName: 'div',
textNode: 'level-0',
children: [
{
level: 1,
prop: { href: '#' },
tagName: 'a',
textNode: 'level-1'
},
{
level: 1,
prop: { class: 'text' },
tagName: 'span',
textNode: 'level-1'
}
]
}
]
复制代码
最后再通过 createElement
,创建DOM元素,然后挂载到DOM树上。
注意
()
判断优先级时,连续的多个(
或)
,需要特殊处理
主要代码
- 字符串解析函数
const parse = function (str) {
const result = []
const head = []
let currentLevel = 0
let pointer = result
head.push(pointer)
let astArr = ast(str)
astArr.forEach((item, index) => {
if (item.level > currentLevel) {
currentLevel = item.level
astArr[index - 1].children = []
pointer = astArr[index - 1].children
head.push(pointer)
}
if (item.level < currentLevel) {
currentLevel = item.level
pointer = head[currentLevel]
head.splice(currentLevel + 1, Infinity)
}
pointer.push(item)
})
return result
function ast(str) {
let startLevel = 0
const stack = []
return match(str, '', startLevel)
function match(str, lastCharacter, startLevel) {
let level = startLevel
const result = []
var tagName = ''
var prop = {}
var textNode = ''
do {
if (str[0] === '+') {
lastCharacter = str[0]
str = str.replace('+', '')
}
if (str[0] === '>') {
lastCharacter = str[0]
str = str.replace('>', '')
level++
}
if (str[0] === '(') {
lastCharacter = str[0]
str = str.replace('(', '')
stack.push(level)
}
if (str[0] === ')') {
lastCharacter = str[0]
str = str.replace(')', '')
level = stack.pop()
}
if (/\w/.test(str[0]) && str !== '') {
lastCharacter = str[0]
tagName = str.match(/\w+/)[0]
str = str.replace(/\w+/, '')
}
if (str[0] === '{') {
lastCharacter = str[0]
textNode = str.match(/\{(.+?)\}/)[1]
str = str.replace(/\{(.+?)\}/, '')
}
if (str[0] === '[') {
lastCharacter = str[0]
let arr = str.match(/\[(.+?)\]/)[1].split(',')
arr.forEach((item) => {
let tempArr = item.split('="')
prop[tempArr[0]] = tempArr[1]
})
str = str.replace(/\[(.+?)\]/, '')
}
} while (str !== '' && str[0] !== '>' && str[0] !== '+' && str[0] !== ')')
if (tagName !== '') {
result.push({
level,
tagName,
prop,
textNode
})
}
if (str === '') {
return result
} else {
let tempArr = match(str, lastCharacter, level)
result.push(...tempArr)
}
return result
}
}
}
复制代码
createElement
函数
const parse = function (domObj) {
if (domObj.tagName) {
var el = document.createElement(domObj.tagName)
var textNode = domObj.textNode
? document.createTextNode(domObj.textNode)
: document.createTextNode('')
el.appendChild(textNode)
if (domObj.prop && JSON.stringify(domObj.prop) !== '{}') {
var props = domObj.prop
for (var propName in props) {
var propValue = props[propName]
el.setAttribute(propName, propValue)
}
}
if (domObj.children && domObj.children.length !== 0) {
domObj.children.forEach((item) => {
el.appendChild(this.render(item))
})
}
return el
} else {
throw new Error('No tagName!')
}
}
复制代码
总结
通过一系列操作,功能是实现了,能够充当模板引擎,本人也测试过了,但是拥有待优化的空间:
- 字符串解析那里,用了多次循环和递归,时间复杂度和空间复杂度应该很高(我也没推导,但不会低于
的),后期参考别人的优化一下,将过程简化。
- 可以尝试学习,加入数据绑定,检测数据变化来更新Dom。