前言

继续我的自建博客之旅,在搭建网站的时候发现重复的模块太多了,每个页面都有大量的相同的功能和样式的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> 
复制代码

要实现这个功能得分两步

  1. 把字符串解析成 Dom
  2. 把 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抽象语法树)里提到的词法分析,我的理解是,我定义一堆操作规则,符合定义的字符串就被分类,这个字符串就代表我对应的操作。如下:

  • 符号含义:
    • > 父子关系
    • + 兄弟关系
    • [] 属性
    • {} 文本
    • () 优先运算
  • 符号优先级:() 大于 > 等于 + 大于 [] 等于 {}
  • 要点:
    • 任何符号之间的纯字母都是标签名
    • () 解析的函数(确定优先级)
    • [] 解析的函数 (确定属性)
    • {} 解析的函数(确定文本内容)

那么对于一个字符串的解析思路为例):

  1. 从左到右取字符串的第一个字符进行判断
    • 如果是字母,那么这个字母开始连续的字母组成的字符串是标签名,(以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,余下部分执行第二步
  2. 判断字符串是否取完,没取完继续返回第一步执行,取完则结束

那么最终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树上。

注意

  1. ()判断优先级时,连续的多个(),需要特殊处理

主要代码

  • 字符串解析函数
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。