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 = '<div><span>'+ data.name +'</span><span>'+ data.price +'</span></div>';
}

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]; %>
      <tr>
         <td><% post.uid %></td>
         <td><% post.uname %></td>
      </tr>  
<% } %>

思路是这样:
(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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容