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

Posted vcxiaohan2

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义一个简单的前端模板引擎相关的知识,希望对你有一定的参考价值。

前言

  • 一次在前端技术沙龙会议上,演讲者分享主题《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>

以上是关于自定义一个简单的前端模板引擎的主要内容,如果未能解决你的问题,请参考以下文章

前端学PHP之自定义模板引擎

PHP的自定义模板引擎

自定义php模板引擎

将 django 用作独立模板引擎时如何使用自定义过滤器

PHP 自定义 Smarty 模板引擎类 高洛峰 细说PHP

前端模板Juicer