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

130 阅读2分钟

一、概览

从今天开始,让我们从头来写一个带diff的前端模版引擎,该功能会分成几期做完。

首先我们看看这个系列的目标是什么,如下图: 在这里插入图片描述

今天我们主要实现基础的模版解析。


二、场景分析

首先,假设一下使用场景:

  • 我们需要一个parser() 方法。
  • 将模版字符串传入。
  • parser()方法在解析html标签的各个阶段调用我们的回调函数,方便我们生成预期的结构。

下面用代码演示一下使用方法:

	import { parser } from '@jax/html-parser'
	// api参照 https://www.npmjs.com/package/htmlparser2
	parser('<div>Jax</div>', {
		onStart(tag: string, attr: string) {},
		onText(text) {},
		onEnd(tag) {}
	})

ok,知道怎么用了之后,我们就来实现parser()方法吧。

三、实现parser解析HTML

3.1 实现逻辑

解析html模版的方式有很多,此处我们采用正则匹配的方法,下面让我们图来简单说明下编码逻辑: 在这里插入图片描述 确定了代码逻辑,接下来我们准备需要的几个正则表达式

3.2 四个正则表达式

// 匹配起始标签和属性
const startReg = /^<(\w+)\s?(.*?)\/?>/
// 匹配结束标签属性
const endReg = /^<\/(\w+)>/
// 匹配标签内容
const textReg = /^>?(.*?)</
// 空字符
const blockReg = /^>?\s+\S/

正则表达式准备好后,接下来实现主方法

3.3 实现parser()

export const parser = (html: string, options: optionsType) => {
  // 获取传入的几个回调函数
  const { onStartTag, onText, onEndTag } = options
  html = html.trim()
  let matched = null
  // 扫描文本,默认我们认为模版字符串的最后一个字符一定是>,所以此处>1即可
  while (html.length > 1) {
    // 判断是否为结束标签
    if (html.startsWith('</')) {
      matched = matchByReg(endReg)
      onEndTag && onEndTag(matched[1]) // 执行回调
    } else if (html.startsWith('<')) {
      // 开始标签
      matched = matchByReg(startReg)
      onStartTag && onStartTag(matched[1], attrParser(matched[2])) // 执行回调
      
      // 特殊处理下自闭合标签
      if (matched[0].endsWith('/>')) {
        onEndTag && onEndTag(matched[1]) // 执行回调
      }
    } else if (html.startsWith('>')) {
      // 去掉可能存在的空格
      matchByReg(blockReg, false)
      if (!html.startsWith('<')) {
        // 获取标签中的内容
        matched = matchByReg(textReg, false)
        if (matched[1]) {
          onText && onText(matched[1]) // 执行回调
        }
      }
    }
  }
}

代码中用于匹配和截取的操作我们给提成matchByReg()方法。注意,此方法需要操作html模版字符串,所以该方法是声明在parser()方法中

const matchByReg = (
   reg: RegExp,
   isThrow: boolean = true
 ): RegExpMatchArray => {
   let matched = html.match(reg)
   if (matched === null) {
     if (!isThrow) {
       return []
     }
     return throwError()
   }
   // 匹配到了后处理字符串
   let len = matched.index! + matched[0].length
   html = html.substring(len - 1)
   return matched
 }

此时像我们在场景分析中那样使用,parser() 方法已经能够正常工作了,下一期我们来处理目标结构生成

循序渐进,不忘初心,我们明天见

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