自定义一个简单的前端模板引擎

前言

  • 一次在前端技术沙龙会议上,演讲者分享主题《Node.js性能调优实践》时,提到了在node配合artTemplate渲染页面时,并发高且返回数据量大的情景下,会遇到渲染的性能瓶颈,当时演讲者有提到几种解决方法:说artTemplate在渲染模板时会使用到拷贝继承,这个效率明显低于原型继承,修改源码即可
    今天去看了下最新的artTemplate源码,发现继承方法已经改为原型继承了
    这里写图片描述
  • 台下有人发言说也可以使用一些诸如webpack的loader加载器,将渲染的模板进行加载并预编译为实际的方法缓存起来,这样渲染的时候直接调用方法,能极大的提高效率
  • 由于当时对模板引擎的原理不甚了解,听得有点蒙,事后得空来稍稍研究下

知识点普及

  • html嵌入模板的几种方法
    • 不同方法的优劣势请参考HTML5 标签元素简介
    • 本文选用的是方法1

      <!-- 方法1 -->
      <script type="text/template">
        <img src="xxx.png">
      </script>
      
      <!-- 方法2 -->
      <textarea style="display: none;">
        <img src="xxx.png">
      </textarea>
      
      <!-- 方法3 -->
      <xmp style="display: none;">
        <img src="xxx.png">
      </xmp>
      
      <!-- 方法4 -->
      <template>
        <img src="xxx.png">
      </template>
  • 正则表达式负向前瞻
    • 后文需要匹配字符串{{{{=,这时候需要用到负向前瞻
  • eval和new Function的区别
    • 把字符串解析为可执行的方法,据说2者运行时性能有差异,暂不讨论
  • 浅析js模板引擎
    • 不同模板间的格式以及效率对比

思路

  • 在html中嵌入混有着各种原生js的模板
  • 使用js读取这段模板为字符串
  • 将这段字符串中特殊标记处(指{{{{=}})进行相应的替换,最终转换为一段可执行的代码
  • 这段代码执行后将返回拼接的dom结构,之后将dom结构添加到页面

实现

  • 待渲染的数据

    let data = {
      name: 'hvb',
      age: 22,
      hobbies: ['reading', 'coding'],
      friends: [
        { name: 'hwj', age: 12 },
        { name: 'hwb', age: 24 }
      ]
    }
  • 待解析的模板(规定it为数据的来源,模板里面混入了es6语法)

    <script id="template" type="text/template">
      <p>姓名:{{= it.name }}</p>
      <p>年龄:{{= it.age }}</p>
      <p>爱好:{{= it.hobbies[0] }}{{= it.hobbies[1] }}</p>
      {{ for(let {name, age} of it.friends) { }}
        <p>姓名:{{= name }}</p>
        <p>年龄:{{= age }}</p>
      {{ } }}
    </script>
  • 解析函数(经过大量的拼接试错,得出能使被替换后的字符串正确运行的匹配替换规则,可自行研究)

    // 解析函数(注意模板中的数据来源均为it)
    function render(id, it) {
      // 获取模板字符串
      let html = $(`#${id}`).html()
      // 把换行符替换为空 把{{替换为xx 把{{=替换为xx 把}}替换为xx
      html = html.replace(/\n/g, '').replace(/{{(?!=)/g, '\';').replace(/{{=/g, '\'+').replace(/}}/g, ';str+=\'')
      // 进行简单的拼接,通过eval执行
      return eval(`let str='';str+='${html}'`)
    }
  • 高性能的解析函数(由于以上使用eval进行粗暴的解析,没有任何的优化,故运行效率低下,此处进行改进)

    // 使用单例模式来缓存解析后的方法(对比以上粗暴的eval解析,性能提升3倍左右)
    let render = (function () {
      let fn = null
      return function (id, it) {
        if (!fn) {
          let html = $(`#${id}`).html()
          html = html.replace(/\n/g, '').replace(/{{(?!=)/g, '\';').replace(/{{=/g, '\'+').replace(/}}/g, ';str+=\'')
          fn = new Function('it', `let str='';str+='${html}';return str;`)
        }
        return fn(it)
      }
    })()
  • 渲染结果

    // 调用方法
    $('#a').append(render('template', data))

    这里写图片描述

思考

  • 模板引擎的思路大同小异,特别是在中途发现自己的思路跟doT.js很相近
  • 至于模板引擎的其他功能,如自定义模板标签、过滤xss、include等等暂不考虑
  • 模板引擎最核心是渲染性能,可以参考高性能JavaScript模板引擎原理解析

源码

以下还小小的对比了下自定义模板和doT.js的渲染性能,当然这对doT.js并不公平,因为我只实现了很小的一个功能

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
  <title>demo</title>
  <script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
    crossorigin="anonymous"></script>
  <script src="https://cdn.bootcss.com/dot/2.0.0-beta.0/doT.min.js"></script>
  <style>
    * {
      margin: 0;
      padding: 0;
    }

    body,
    html {
      width: 100%;
      height: 100%;
    }

    div {
      border: 2px solid red;
    }
  </style>
</head>

<body>
  <h2>模板解析如下:</h2>
  <div id="a"></div>
  <h2>模板解析如下:(使用不同数据)</h2>
  <div id="b"></div>
  <script id="template" type="text/template">
    <p>姓名:{{= it.name }}</p>
    <p>年龄:{{= it.age }}</p>
    <p>爱好:{{= it.hobbies[0] }}{{= it.hobbies[1] }}</p>
    {{ for(let {name, age} of it.friends) { }}
      <p>姓名:{{= name }}</p>
      <p>年龄:{{= age }}</p>
    {{ } }}
  </script>
</body>
<script>
  $(() => {
    // 一组数据
    let data1 = {
      name: 'hvb',
      age: 22,
      hobbies: ['reading', 'coding'],
      friends: [
        { name: 'hwj', age: 12 },
        { name: 'hwb', age: 24 }
      ]
    }
    // 一组不同的数据
    let data2 = {
      name: 'hvb111',
      age: 22111,
      hobbies: ['reading111', 'coding111'],
      friends: [
        { name: 'hwj111', age: 12111 },
        { name: 'hwb111', age: 24111 }
      ]
    }

    // 使用单例模式来缓存解析后的方法(对比以上粗暴的eval解析,性能提升3倍左右)
    let render = (function () {
      let fn = null
      return function (id, it) {
        if (!fn) {
          let html = $(`#${id}`).html()
          html = html.replace(/\n/g, '').replace(/{{(?!=)/g, '\';').replace(/{{=/g, '\'+').replace(/}}/g, ';str+=\'')
          fn = new Function('it', `let str='';str+='${html}';return str;`)
        }
        return fn(it)
      }
    })()

    // 使用模板渲染一组数据
    $('#a').append(render('template', data1))
    // 使用模板渲染一组不同的数据
    $('#b').append(render('template', data2))

    // 自定义模板性能测试
    console.time()
    for (let i = 0; i < 1000; i++) {
      $('body').append(render('template', data1))
    }
    console.timeEnd()

    // doT模板性能测试
    var evalText = doT.template($('#template').text())
    console.time()
    for (let i = 0; i < 1000; i++) {
      $('body').append(evalText(data1))
    }
    console.timeEnd()
  })
</script>

</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值