JavaScript模板引擎实现原理

一、我理解的模板引擎

一张网页,要经历怎样的过程,才能抵达用户面前?前端开发最主要的作用就是:在最短的时间内,从后端获取到正确的数据,然后通过浏览器渲染,最后将效果展示给用户。

比如有这样一条数据:

let template = 'Hello world, my name is <% name %>, and I\'m <% age %> years old.';

从后端获取的数据如下:

let data = {
    name: 'zhaoyiming',
    age: 18
};
let res = ztpl(tpl, data);

假如我们现在使用的模板引擎叫做ztpl,那么以上代码运行之后,最后渲染的结果应该是:

// Hello world, my name is zhaoyiming, and I\'m 18 years old.

之前使用PHP开发项目的时候,最常用的模板引擎就是Smarty。这几年随着AngularJs、VueJs、ReactJs等框架的涌现,模板引擎对于前端来说已不再陌生。

说白了,模板引擎就是将指定模板内容(字符串)中的特定标记(子字符串)替换生成最终需要的业务数据,实现界面与数据的分离,提升代码重用度。

二、动手实现一个简易JavaScript模板引擎

1、基本原理:通过正则表达式替换变量

let template = 'Hello world, my name is <% name %>, and I\'m <% age %> years old.';
let data = {
    name: 'zhaoyiming',
    age: 18
};
let res = template.replace/<%[\s\t\r]+?([^%>]+)?[\s\t\r]+?%>/g, function ($0, $1) {
  return data[$1];
});
console.log(res); // Hello world, my name is zhaoyiming, and I'm 18 years old.

但是如果把data改成如下格式:

let data = {
    baseInfo: {
      name: 'zhaoyiming',
      age: 18
    },
    work: 'FE'
};

上面的方法就不能用了,会提示data[baseInfo.name] is undefined,只能将它转换成JS对象方法来处理:

function fn () {
  return 'Hello world, my name is' + data.baseInfo.name + ', and I'm ' + data.baseInfo.age + ' years old';
}

没错,就是拼字符串,以前为了方便都是这么干,伪代码如下:

function strToDom(str) {
    var oDiv = document.createElement("div");
    oDiv.innerHTML = str;
    return oDiv.childNodes[0];
}

function createProductList (data) {
  var str = '
'+ data.name +''+ data.price +'
'; } function renderProductList (productList, banners) { if (!isArray(productList)) return; var frag = document.createDocumentFragment(); for (var i = 0, len = productList.length; i < len; i += 1) { var tmpNode = strToDom(createProductList(productList[i])); frag.appendChild(tmpNode); } document.querySelector('#hot-product-list').appendChild(frag); }

这种拼字符串,同样可以实现需求,但是html结构、css样式、js逻辑都耦合在一块,后期维护不方便,所以使用模板引擎是最好的。

但是有个问题,如果模板中有for、if等语句时,只靠replace是不行的,可以对比拼字符串的方式,代码总共分为两部分:字符串和变量,那模板引擎也是分为两部分:普通字符串代码和JS逻辑代码(for、if、else、break等等)。

2、使用正则区分普通字符串和JS逻辑代码

如下代码:

<% for(var i = 0; i < this.list.length; i++) {
     var post = this.list[i]; %>
      
         <% post.uid %>
         <% post.uname %>
        
<% } %>

思路是这样:
(1)定义一个字符串str和数组arr;
(2)从前到后全局匹配模板;
(3)如果匹配到了<% %>,将匹配到的这段字符串中的逻辑代码添加给str,不是逻辑代码,str连接'arr.push(非逻辑代码)'。
(4)循环执行第三步,直到匹配到模板最后一个字符。
(5)返回最终的str。

4、通过构造函数来编译字符串

JavaScript给我们提供的类(构造函数),不仅可以实现面向对象编程,也可以编译传入的字符串参数,如下:

var data = {
    name: 'zhaoyiming',
    age: 18
};
var fn = new Function ('data', 'var arr = []; for(var prop in data){ arr.push(data[prop]); } return arr.join(" ")');
console.log(fn(data)); // zhaoyiming 18

这样的话,我们可以把第三步中返回的str和从后端获取到的data作为构造函数的参数,这样的话,就可以返回我们想要的数据,然后append到页面中。

根据以上思路,实现一个基本的模板引擎,核心代码如下:

let str = 'var r=[];\n';
const REG_OUT = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g;

function ztpl (id, data) {
  const ELE = document.querySelector(id);
  if (!ELE) throw new Error('无效dom对象id'); 
  let html = ELE.value;
  
  const REG = /<%([^%>]+)?%>/g;
  let idx = 0;
  let match = null;

  while(match = REG.exec(html)) {
    addStr(html.slice(idx, match.index))(match[1], true);
    idx = match.index + match[0].length;
  }

  addStr(html.substr(idx, html.length - idx));
  str += 'return r.join("");';
  return new Function(str.replace(/[\r\t\n]/g, '')).apply(data);
}

function addStr (fragment, hasTplTab) {
  if (hasTplTab) {
    str += fragment.match(REG_OUT) ? (fragment + '\n') : ('r.push(' + fragment + ');\n');
  } else {
    str += fragment !== '' ? ('r.push("' + fragment.replace(/"/g, '\\"') + '");\n') : '';
  }
  return addStr;
}

已经将所有代码开源到了github:https://github.com/zymseo/ztpl,目前实现了一个简易的模板引擎,不断优化中。。。

最后再发个牢骚:前端每几个月都会有很大的变化,挨个学是真学不过来,理解代码为什么要这样写就行了。

参考资料:
https://github.com/BaiduFE/BaiduTemplate
https://www.awesomes.cn/repo/aui/arttemplate
https://blog.csdn.net/wxqee/article/details/76100732
https://github.com/jojoin/tppl

你可能感兴趣的:(JavaScript模板引擎实现原理)