【造轮子系列】面试官问:你能手写一个模板引擎吗?

3,706 阅读5分钟

freysteinn-g-jonsson-s94zCnADcUs-unsplash.jpg

概览:很多流行的前端库和框架中都实现了模板引擎,所以了解模板引擎的实现原理能够帮助开发者更好地理解和应用它们,手写模板引擎可以考察一个人的编程能力,包括对于js语言的理解,函数的使用,作用域等概念的掌握,那么,问题来了,你能手写一个模板引擎吗?

今天会带大家实现一个模板引擎,因为是针对面试,所以实现的引擎以原理为主,重点学习实现引擎的思想和必要的核心知识,在掌握了核心的原理和思想,剩下我们可以自己锦上添花,不断的去完善和改进。

实现简单的需求:

const tpl = 'I like <%sport%>. I have been <%sport%> for <%time%> years.'
const data = { sport: 'swimming', time: 'three'}

上面的代码,我们如何将数据data渲染到模板tpl当中去?可能最直接的办法就是写一个正则匹配(<%()%>),然后再用对应数据进行替换,所以我们首先需要写一个正则能够匹配这种特殊的格式:

const regExp = /<%([^%>]+)?%>/g

下载.png

上面这个正则可以匹配我们所需要的这种特殊的形式,并且将内容进行分组,这样我们就可以很轻易的对内容进行替换,我们看下具体匹配的内容

const expReg = /<%([^%>]+)?%>/g;
console.log(expReg.exec("<%sport%>"));
/* 
  [
    '<%sport%>',
    'sport',
    index: 0,
    input: '<%sport%>',
    groups: undefined
  ]
*/

数组的第一项是整个匹配项,数组的第二项是分组的内容,根据这个分组,我们的第一版引擎就可以实现了

var tplEngine = function(tpl, data) {
  var re = /<%([^%>]+)?%>/g, match;
  // 循环知道match为null,替换所有的动态数据
  while(match = re.exec(tpl)) {
    // 将data中的数据进行替换 
    tpl = tpl.replace(match[0], data[match[1].trim()])
  }
  return tpl;
}

const tpl = 'I like <%sport%>. I have been <%sport%> for <%time%> years.'
console.log(tplEngine(tpl, { sport: 'swimming', time: 'three'}));
// I like swimming. I have been swimming for three years.

处理有js代码的情况

我们添加一点简单的需求,数据的获取通过this上下文获取,或者数据有嵌套结构,这样可能更接近实际的使用情况,如第一版的实现,我们只是刻板的去匹配,然后去进行替换,如果再遇到for循环等js的一些语句操作,就更没有办法实现了,所以我们需要去灵活的构建一个函数,这样就可以解决动态js语句的问题。看下面三个例子:

// 我们可以通过字符串构造一个函数
const a = new Function('arg', 'console.log(arg)')
a(3)
// 我们可以通过with去改变函数的作用域,取sport,会从传入的对象上去取
const b = new Function('obj', 'with(obj){console.log(sport)}')
b({sport: 'swimming'})

// 我们可以传入数据,并用apply改变this指向,这样两种取值都可以实现
const c = new Function('obj', 'with(obj){console.log(this.sport)}')
c.apply({sport: 'swimming'}, [{sport: 'swimming'}])

我们的基本思路是:通过拼接字符串来解析模板,然后通过with绑定作用域,完了通过new Function构造函数,最后执行就可以得到我们所期望的模板了。这就是所谓的模板引擎渲染。那么接下来我们的重点就是要去拼接函数的body体。

进行函数的拼装

我们的需求很明确,就是把动态的模板拼接成可执行函数的函数体。看一个具体的例子,我们就清楚应该拼接成什么样:

// 这是包含了js代码的动态模板
var template = 
'My avorite sports:' + 
'<%if(this.showSports) {%>' +
    '<% for(var index in this.sports) {   %>' + 
    '<a><%this.sports[index]%></a>' +
    '<%}%>' +
'<%} else {%>' +
    '<p>none</p>' +
'<%}%>';
// 这是我们要拼接的函数字符串
const code = `with(obj) {
  var r=[];
  r.push("My avorite sports:");
  if(this.showSports) {
    for(var index in this.sports) {   
      r.push("<a>");
      r.push(this.sports[index]);
      r.push("</a>");
    }
  } else {
    r.push("<span>none</span>");
  }
  return r.join("");
}`
// 动态渲染的数据
const options = {
  sports: ["swimming", "basketball", "football"],
  showSports: true
}
// 构建可行的函数并传入参数,改变函数执行时this的指向
result = new Function("obj", code).apply(options, [options]);
console.log(result);

从上面我们可以看到,我们需要一个数组去存储要拼接的要素,但是如果是动态的js语言,我们不需要push到r中去,所以我们需要区分字符串还是js语言,同样我们需要正则表达式:

const regExp = /(.*)?(var|if|for|else|switch|case|break|{|}|;)(.*)?/g;

下载 (1).png

最终版实现

现在我们可以用正则匹配动态数据,然后又可以区分哪些是动态的js,我们把非js的push到一个数组里面,然后join整个数组就可以得到一个拼接的字符串code了。

function TemplateEngine(html, options) {
  // 匹配动态格式
  var re = /<%(.+?)%>/g, 
    // 匹配js代码
    reExp = /(.*)?(var|if|for|else|switch|case|break|{|}|;)(.*)?/g,
    // 拼接的code字符串,并初始化开始部分
    code = "with(obj) { var r=[];\n",
    cursor = 0,
    result,
    match;
  // 判断是否是js代码,如果是js代码不需要添加进数组中,不是js代码需要push进数组
  var add = function (line, js) {
    js
      ? (code += line.match(reExp) ? line + "\n" : "r.push(" + line + ");\n")
      : (code +=
          line != "" ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : "");
    return add;
  };
  while ((match = re.exec(html))) {
    add(html.slice(cursor, match.index))(match[1], true);
    cursor = match.index + match[0].length;
  }
  // 拼接剩余字符串
  add(html.substr(cursor, html.length - cursor));
  // 生成最终的code
  code = (code + 'return r.join(""); }').replace(/[\r\t\n]/g, " ");
  try {
    // 构建函数,生成最后结果
    result = new Function("obj", code).apply(options, [options]);
  } catch (err) {
    console.error("'" + err.message + "'", " in \n\nCode:\n", code, "\n");
  }
  return result;
};
var template = 
'My avorite sports:' + 
'<%if(this.showSports) {%>' +
    '<% for(var index in this.sports) {   %>' + 
    '<a href="#"><%this.sports[index]%></a>' +
    '<%}%>' +
'<%} else {%>' +
    '<p>none</p>' +
'<%}%>';
console.log(TemplateEngine(template, {
    sports: ["swimming", "basketball", "football"],
    showSports: true
}));
// My avorite sports:<a>swimming</a><a>basketball</a><a>football</a>

本篇实现了一个简单的模板引擎,带大家一起感受了模板引擎实现的整个流程,本文也是在看了一篇大牛的文章之后深受启发,受益匪浅,非常感谢作者,原文附在文末

参考链接:krasimirtsonev.com/blog/articl…