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

前言

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

知识点普及

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

      
      <script type="text/template">
        "xxx.png">
      script>
      
      
      <textarea style="display: none;">
        <img src="xxx.png">
      textarea>
      
      
      <xmp style="display: none;">
        <img src="xxx.png">
      xmp>
      
      
      <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">
      

    姓名:{{= it.name }}</p>

    年龄:{{= it.age }}</p>

    爱好:{{= it.hobbies[0] }}{{= it.hobbies[1] }}</p> {{ for(let {name, age} of it.friends) { }}

    姓名:{{= name }}</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))

    自定义一个简单的前端模板引擎_第2张图片

思考

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

源码

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


<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>demotitle>
  <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">
    

姓名:{{= it.name }}</p>

年龄:{{= it.age }}</p>

爱好:{{= it.hobbies[0] }}{{= it.hobbies[1] }}</p> {{ for(let {name, age} of it.friends) { }}

姓名:{{= name }}</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>

你可能感兴趣的:(javascript实例)