从零开始,编写一个HTML模版引擎(二)

今天在复盘的时候,发现上一节还是有些仓促,本节开始稍微细致一些。

然后发现上一节漏掉了一个attrParser()方法,它的作用主要是将收集到的字符串属性转换成Map结构,常规的JSON格式也是可以的,保证属性名称不重复,后声明的覆盖先声明的即可。

那么我们今天就先从attrParser()开始吧。

一、补充attrParser方法

首先还是让我们先看看使用场景,比如我们可以按如下方法声明属性:

  • style=“color: pink;”
  • class=“todo todo1 todo2”
  • x-for=“item in list” 循环方式 后期会用到
  • :todo=“todo” 向下传递props 后期会用到
  • x-if=“true”
  • x-show=“true”
  • key=“key”

然后呢,我会可能会得到一个组合后的属性字符串,如:

const attrStr = 'style="color: pink;" class="todo todo1 todo2" x-for="item in list" :todo="todo" x-if="true" key="key"'

接下来我们开始拆解字符串,大家应该也可以注意到,styleclass属性中均存在空格,所以单纯的通过空格或者引号split是行不通的。

使用双指针+词法分析可以很方便的将属性截取出来,此例我们偷个懒,还是上正则:

const attrReg = /(:?x?-?\w+)=?\s?(\"(.*?)\")?/g

使用正则的方式可以一次性将属性分割开来,然后基于等号split即可拿到所有属性,下面让我们用上边的attrStr来测试下效果

const attrStr = 'style="color: pink;" class="todo todo1 todo2" x-for="item in list" :todo="todo" x-if="true" key="key"'
const attrReg = /(:?x?-?\w+)=?\s?(\"(.*?)\")?/g
console.log(attrStr.match(attrReg))
// 操作后,我们将得到数据
// ['style="color: pink;"', 'class="todo todo1 todo2"', 'x-for="item in list"', ':todo="todo"', 'x-if="true"', 'key="key"']

通过上边测试,我们发现基础的用例是能跑通的,那剩下的问题就简单了,直接编码吧


// shared/index.ts
export const attrParser = (attrStr: string) => {
  if (!attrStr) return attrStr
  let ret: Map<string, any> = new Map()
  attrStr = attrStr.trim()
  // 上边介绍过的分割方法
  let attrs = attrStr.match(attrReg)
  if (attrs && attrs.length) {
  	const quot = /["']/g
    for (const iterator of attrs) {
      let attr = iterator.split('=')
      // 将value多余的引号去掉
      // 将没有value值的属性的value置为true
      ret.set(attr[0], attr[1] ? attr[1].replace(quot, '') : true)
    }
  }
  return ret
}

通过该方法,我们就得到了一个存放属性的Map结构,它看起来像下图一样:
在这里插入图片描述
到此,我们完成了属性的基础切割。接下来让我们用已经介绍过的内容,生成带层级的虚拟dom结构。


二、虚拟dom结构

进行到此,我们可能需要引入虚拟dom的概念了,但是上一期已经说好这期生成目标结构,所以虚拟dom相关的具体内容我们放到后边的章节来详细说明,本期先弱化虚拟dom概念,暂时还是称作目标结构吧。

还是一样,我们上来先思考我们需要的目标结构应该长什么样子的,或者说我们如何用最少的属性描述一个dom节点。

用最少的属性描述,是因为后期虚拟dom会常驻内存,较为精简的结构能节省内存,也可以提升diff的效率。

通过参考(抄袭)其他作者的结构,我们暂定我们的目标结构为:

interface VNode = {
	tag: string // 存放标签名称
	data: any // 用来存放标签的所有属性
	children: Array<VNode>
	elm: Node // 后期用来存放真实dom
	text: string // 若标签只有一个文本节点,则存放在此
	key: string | symbol | undefined
}

OK,有了目标结构,那我们还需要一个(不是)创建目标结构的方法,该方法主要负责精简结构,返回VNode

// virtual-dom/src/vnode.ts
export function vnode(
  tag: string | undefined,
  data: VNodeData | undefined,
  children: Array<VNode | string> | undefined,
  elm: Node | undefined,
  text: string | undefined
): VNode {
  let key = data === undefined ? undefined : data.key
  let ret: Record<string, any> = {
    tag,
    data,
    children,
    elm,
    text,
    key,
  }
  // 精简结构,去除空属性
  for (let k in ret) {
    if (ret.hasOwnProperty(k)) {
      if (ret[k] === undefined) Reflect.deleteProperty(ret, k)
    }
  }
  return ret
}

至此,准备工作已经做完,让我们开始生成目标结构吧


三、生成目标结构

首先呢我们先引出来一个概念,分析有开启有关闭的结构时(xml、html等), 如果需要生成带层级的嵌套结构,一般都是通过词法分析+栈实现的,实际上我们通过上一节,已经完成了词法分析的步骤,接下来,我们一起看看如何通过栈来收集结构。

3.1 栈的操作逻辑

在这里插入图片描述

3.1 代码实现

逻辑理清之后,编码就比较简单了,篇幅有限,我们本期先实现一个简单版的,下一期引入h函数,也就是react中的React.createElement

// x-template/src/createVNode.ts
/**
 * 根据模版字符串,创建虚拟节点
 * @param htmlStr 模版字符串
 * @param data 数据
 */
export const createVNode = (htmlStr: string, data: any) => {
  // 使用栈收集标签层级
  // 我们先在其中加入一个跟节点作为所有节点的父级,收集完成后,其下的children就是结果
  let stack: Array<VNode> = [vnode('root', undefined, [], undefined, undefined)]
  parser(htmlStr, {
    onStartTag(name: string, attrs: Map<string, any>) {
      let parent = stack[stack.length - 1]
      let _vnode = vnode(
        name,
        getTagData(attrs), // 获取tagdata的结构 可以先忽略
        undefined,
        undefined,
        undefined
      )
      parent.children = parent.children ? parent.children : []
      // 添加子集
      parent.children.push(_vnode)
      // 进栈
      stack.push(_vnode)
    },
    onText(text: string) {
      let parent = stack[stack.length - 1]
      parent.children = parent.children ? parent.children : []
      // 将文本标签作为当前标签的子集
      parent.children.push(
        vnode(undefined, undefined, undefined, undefined, text)
      )
    },
    onEndTag(name: string) {
      let parent = stack[stack.length - 1]
      if (parent.tag === name) {
      	// 若标签名一致,则出栈
        stack.pop()
      } else {
        throw new Error('not found close tag: ' + parent.tag)
      }
    },
  })

  let vnodes = stack[0].children
  // 拿到收集后的虚拟节点
  console.log(vnodes)
  return vnodes
}

本期完!

下期预告,下期我们引入h函数的概念,方便我们后期通过ts或者babel解析jsx生成虚拟dom使用。

然后若时间充裕,再加上简单的数据绑定、指令解析(x-for、x-if、x-show)等内容。


循序渐进,不忘初心,我们下期见~

有问题请留言 或发邮件: liujax@126.com

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值