JavaScript模板引擎原理与用法详解

作者:函数茶馆

时间:2026-10-22

浏览:300

本文详细解析JavaScript模板引擎的工作原理与实现方法。通过正则表达式提取模板变量,将模板字符串转换为可执行的JavaScript代码,并利用Function构造函数动态生成渲染函数。文章涵盖基础变量替换、逻辑控制处理及数据注入机制,并提供了实际应用场景与优化建议,帮助开发者理解前端模板引擎的核心逻辑。

文章目录

本文详细介绍JavaScript模板引擎的工作原理与实际应用方法,供读者参考学习。

一、前言

模板引擎的核心概念在于处理包含待定变量的字符串。例如:


var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.';

利用模板引擎函数将数据填充至模板中,


var data = {
  "name": "Barret Lee",
  "age": "20"
};
var result = tplEngine(tpl, data);
//Hei, my name is Barret Lee, and I'm 20 years old.

模板引擎的主要作用在于作为预处理器。对于熟悉PHP开发的开发者而言,Smarty模板引擎并不陌生。Smarty通过匹配tpl文件中的待处理字符并输出对应的HTML代码,结合高效的缓存技术,实现了优异的性能与易用性。JavaScript模板引擎遵循相同逻辑:数据库中存储大量数据时,每条数据通过统一模板生成输出。例如,无需在数据库中存储数千条完整的"Hei, my name..."字符串,只需保存对应的name和age字段,再通过模板引擎生成最终结果。

JavaScript模板引擎需要实现哪些功能?参考以下代码示例:


var tpl = '<% for(var i = 0; i < this.posts.length; i++) {' + 
  'var post = posts[i]; %>' +
  '<% if(!post.expert){ %>' +
    '<span>post is null</span>' +
  '<% } else { %>' +
    '<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ><% post.expert %> at <% post.time %></a>' +
  '<% } %>' +
'<% } %>';

一个基础的模板引擎需确保上述代码能正常解析。若输入数据为:


var data = {
  "posts": [{
    "expert": "content 1",
    "time": "yesterday"
  },{
    "expert": "content 2",
    "time": "today"
  },{
    "expert": "content 3",
    "time": "tomorrow"
  },{
    "expert": "",
    "time": "eee"
  }]
};

则应输出:


<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >content 1 at yesterday</a>
<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >content 2 at today</a>
<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >content 3 at tomorrow</a>
<span>post is null</span>

接下来具体探讨该模板引擎的实现原理。

二、JS模板引擎的实现原理

1.正则抠出要匹配的内容

针对上述代码,通过正则表达式提取匹配内容:


var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.';
var data = {
  "name": "Barret Lee",
  "age": "20"
};

最简单的方式是利用replace函数:


var result = tpl.replace(/<%([^%>]+)?%>/g, function(s0, s1){
  return data[s1];
});

通过正则替换,我们可以轻松获取result,读者可自行验证,这正是期望的结果。然而,若修改data和tpl,


var tpl = 'Hei, my name is <%name%>, and I\'m <%info.age%> years old.';
var data = {
  "name": "Barret Lee",
  "info": { age": "20"}
};

再次使用上述方式获取结果时,会发现失效。原因在于data["info.age"]本身为undefined,因此需要换一种处理方式,即将其转换为真正的JavaScript代码。例如:


return 'Hei, my name is ' + data.name + ', and I\'m ' + data.info.age' + ' years old.'

但随之出现新问题,当代码中包含for循环和if语句时,上述转换方式显然无效。例如:


var tpl = 'Posts: ' +
     '<% for(var i = 0; i < post.length; i++) {'+
      '<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ><% post[i].expert %></a>' +
     '<% } %>'

若继续采用上述方式,得到的结果为:


return 'Posts: ' +
    for(var i = 0; i < post.length; i++) { +
     '<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >' + post[i].exper + '</a>' +
    }

这并非期望结果。稍加观察,若能返回如下结构则更为理想:


'Posts: '
for(var i = 0; i < post.length; i++) {
  '<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >' + post[i].exper + '</a>'
}

但我们需要的是字符串而非零散片段,因此可将这些内容存入数组中。

2.装入数组


var r = [];
r.push('Posts: ' );
r.push(for(var i = 0; i < post.length; i++) {);
r.push('<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >');
r.push(post[i].exper);
r.push('</a>');
r.push(});

有人可能指出上述代码中第三行与最后一行逻辑存在错误,如何解决?处理很简单,即不将这些部分放入数组中:


var r = [];
r.push('Posts: ' );
for(var i = 0; i < post.length; i++) {
  r.push('<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >');
  r.push(post[i].exper);
  r.push('</a>');
}

如此逻辑便较为完善,漏洞较少。但转化过程如何实现?仍需编写模板解析函数。

3.分辨js逻辑部分


var r = [];
tpl.replace(/<%([^%>]+)?%>/g, function(s0, s1){
  //完蛋了,这里貌似又要回到上面那可笑的逻辑有错误的一步啦... 该怎么处理比较好?
});

此处似乎又回到之前逻辑错误的困境。如何处理更为妥当?我们知道,JavaScript提供了构造函数的“类”:


var fn = new Function("data",
  "var r = []; for(var i in data){ r.push(data[i]); } return r.join(' ')");
fn({"name": "barretlee", "age": "20"}); // barretlee 20

明确这一点后,可将逻辑部分与非逻辑部分的代码链接为字符串,再利用类似fn的函数直接编译代码。而/<%([^%>]+)?%>/g这一正则仅能匹配逻辑部分,要组合所有代码,还需匹配非逻辑部分。replace函数虽强大,但实现逻辑晦涩,因此采用其他方式处理。

先看一个简单示例:


var reg = /<%([^%>]+)?%>/g;
var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.';
var match = reg.exec(tpl);
console.log(match);

输出结果为:


[
  0: "<%name%>",
  1: name,
  index: 16,
  input: "Hei, my name is <%name%>, and I'm <%age%> years old."
  length: 2
]

我们期望获取所有匹配项,但上述方式仅获取name而忽略age。熟悉正则表达式的读者应知处理方式:


var reg = /<%([^%>]+)?%>/g;
while(match = reg.exec(tpl)) {
  console.log(match);
}

关于正则表达式的内容不再赘述,有兴趣者可深入了解match、exec、search等相关函数。此处主要依靠match的index属性定位遍历位置,并利用while循环获取所有内容。

4.引擎函数

因此,引擎函数的雏形大致如下:


var tplEngine = function(tpl, data){
  var reg = /<%([^%>]+)?%>/g,
      code = 'var r=[];\n',
      cursor = 0; //主要的作用是定位代码最后一截
  var add = function(line) {
    code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
  };
  while(match = reg.exec(tpl)) {
    add(tpl.slice(cursor, match.index)); //添加非逻辑部分
    add(match[1]); //添加逻辑部分 match[0] = "<%" + match[1] + "%>";
    cursor = match.index + match[0].length;
  }
  add(tpl.substr(cursor, tpl.length - cursor)); //代码的最后一截 如:" years old."
  code += 'return r.join("");'; // 返回结果,在这里我们就拿到了装入数组后的代码
  console.log(code);
  return tpl;
};

测试一个小demo:

 


var tpl = '<% for(var i = 0; i < this.posts.length; i++) {' + 
    'var post = posts[i]; %>' +
    '<% if(!post.expert){ %>' +
      '<span>post is null</span>' +
    '<% } else { %>' +
      '<a href="#" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ><% post.expert %> at <% post.time %></a>' +
    '<% } %>' +
  '<% } %>';
tplEngine(tpl, data);

返回结果令人满意:


var r=[];
r.push("");
r.push(" for(var i = 0; i < this.posts.length; i++) {var post = posts[i]; ");
r.push("");
r.push(" if(!post.expert){ ");
r.push("<span>post is null</span>");
r.push(" } else { ");
r.push("<a href=\"#\">");
r.push(" post.expert ");
r.push(" at ");
r.push(" post.time ");
r.push("</a>");
r.push(" } ");
r.push("");
r.push(" } ");
r.push("");
return r.join("");

但我们不需要将for、if、switch等逻辑代码push到r数组中,因此需改进上述代码。若line中包含JavaScript逻辑代码,则不应将其加入:


regOut = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g;
var add = function(line, js) {
  js? code += line.match(regOut) ? line + '\n' : 'r.push(' + line + ');\n' :
    code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
};

最后一步工作是将data注入。

5.把data扔进去

完成此部分并不复杂,通过前文对Function函数的讲解,读者应已掌握方法。


return new Function(code).apply(data);

使用apply的作用是将code中的变量作用域绑定到data上,避免作用域指向global导致数据索引出错。当然可进一步优化:


return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);

匹配并移除回车换行及tab键,使代码更简洁。最终代码如下:


var tplEngine = function(tpl, data) {
  var reg = /<%([^%>]+)?%>/g,
    regOut = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
    code = 'var r=[];\n',
    cursor = 0;
  var add = function(line, js) {
    js? (code += line.match(regOut) ? line + '\n' : 'r.push(' + line + ');\n') :
      (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
    return add;
  }
  while(match = reg.exec(tpl)) {
    add(tpl.slice(cursor, match.index))(match[1], true);
    cursor = match.index + match[0].length;
  }
  add(tpl.substr(cursor, tpl.length - cursor));
  code += 'return r.join("");';
  return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
};

三、应用场景

作为前端代码,模板引擎旨在服务前端。通常处理HTML模板,模板代码多置于script标签或textarea中。首先需获取这些内容,再进行解析。


var barretTpl = function(str, data) {
  //获取元素
  var element = document.getElementById(str);
  if (element) {
    //textarea或input则取value,其它情况取innerHTML
    var html = /^(textarea|input)$/i.test(element.nodeName) ? element.value : element.innerHTML;
    return tplEngine(html, data);
  } else {
    //是模板字符串,则生成一个函数
    //如果直接传入字符串作为模板,则可能变化过多,因此不考虑缓存
    return tplEngine(str, data);
  }
  var tplEngine = function(tpl, data) {
    // content above
  };
};

使用方式更为简便:barretTpl(str, data),其中str可以是模板代码,也可以是DOM元素的id。

四、优化以及功能拓展

代码仅三四十行,完成的是简洁版实现,但对于简单页面已足够。若需优化,可考虑以下方面:

  • 优化获取的模板代码,例如去除行尾空格等。
  • 符号转义。若需输出<span>hehe</span>等类似源代码,在push前必须进行转义处理。
  • 代码缓存。若模板频繁使用,可将其存入barretTpl闭包内的数组中缓存。
  • 允许用户自定义分隔符。

更多关于JavaScript相关内容可查看本站专题:《javascript面向对象入门教程》、《JavaScript切换特效与技巧总结》、《JavaScript查找算法技巧总结》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章