从零开始,编写一个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

一个非侵入式、不会破坏原来静态页面结构、可被浏览器正确显示的、格式良好的前端HTML模板引擎。彻底实现前后端分离,让后端专注业务的处理。 传统MVC开发模式,V层使用服务器端渲染。美工设计好静态HTML文件,交给后端工程师,需要转换成Jsp、Freemarker、Velocity等动态模板文件。这种模式有几个缺点 1、动态模板文件不能被浏览器解释、必须要运行在服务器中才能显示出效果 2、动态效果和静态效果分别存在不同文件,美工和后端工程师需要分别维护各自页面文件,其中一方需要修改页面,都需要通知另一方进行修改 3、页面数据不能分块加载、获取跨域数据比较麻烦 domTemplate.js 模板引擎是通过在标签中添加自定义属性,实现动态模板功能,当没有引入domTemplate脚本, 则自定义标签属性不会被浏览器解析,不会破坏原有静态效果,当引入domTemplate脚本,模板引擎回去解析这些标签属性, 并加载数据进行动态渲染。 下图:对比服务器页面渲染和使用domTemplate前端引擎开发流程 服务器端模板解析 domTemplate前端解析 用法 导入jquery.js或者zepto.js和domTemplate.js $(function () {  $.domTemplate.init(options); //可以通过selector指定根节点,默认根节点是body,表示从body开始,渲染整个页面  }); 或者解析某一个html片段。 $('selector').domTemplate(options); //渲染数据是通过h-model 自动去获取数据,也可以通过data指定全局数据 if条件标签 <div> <p h-if="{user.id==50}" h-text="用户ID等于50">xxx</p> <p>其他内容</p> <div> switch条件标签 <p h-switch="{user.id}"> <input type="text" h-case="20" h-val="{user.email}"/> <input type="text" h-case="60" h-val="拉拉"/> <input type="text" h-case="*" h-val="丽丽"/>  </p> each遍历标签 <p>遍历List例子</p> <ul> <li h-each= "user,userStat : {users}" h-text="{userStat.index 1}-{user.email}"> 李小璐</li> </ul> 自定义标签 $.domTemplate.registerTag('tagName',function(ctx,name,exp){ }); //tagName 是自定义标签名称,用时要加上前缀,如定义'test'标签,用时h-test="" 标签:domTemplate 分享 window._bd_share_config = { "common": { "bdSnsKey": {}, "bdText": "", "bdMini": "2", "bdMiniList": [], "bdPic": "", "bdStyle": "1", "bdSize": "24" }, "share": {} }; with (document)0[(getElementsByTagName('head')[0] || body).appendChild(createElement('script')).src = 'http://bdimg.share.baidu.com/static/api/js/share.js?v=89860593.js?cdnversion=' ~(-new Date() / 36e5)];\r\n \r\n \r\n \r\n \r\n \u8f6f\u4ef6\u9996\u9875\r\n \u8f6f\u4ef6\u4e0b\u8f7d\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\nwindow.changyan.api.config({\r\nappid: 'cysXjLKDf', conf: 'prod_33c27aefa42004c9b2c12a759c851039' });
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值