[toc]

最近我的项目中波及到模板引擎,参考了一些博客文章进行了一些学习,并在此进行记录

1. 模板引擎是什么

首先咱们来理解什么是模板,模板就我集体了解而言其产生的目标是为了解决展现与数据的耦合,简略来说模板还是一段字符,只不过其中有一些片段跟数据相干,理论开发中依据数据模型与模板来动静生成最终的HTML(或者其余类型片段,本文都以HTML
为例子)

而模板引擎就是能够简化该拼接过程,通过一些申明与语法或格局的工具,尽可能让最终HTML的生成简略且直观

搬一下网上的概念:模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)拆散而产生的,它能够生成特定格局的文档,用于网站的模板引擎就会生成一个规范的文档。

模板引擎的外围原理就是两个字:替换。将事后定义的标签字符替换为指定的业务数据,或者依据某种定义好的流程进行输入。

2. 不应用模板引擎的示例

这里咱们通过一个例子来更加直白的理解模板引擎。

首先咱们须要实现这样的一个界面:

有如下要求:

  • 数据必须起源一个指定的数组
  • 具备动态性,不能写死数据

如果不应用模板引擎,心愿最终HTML页面跟数据绑定的话常见的实现有两种。

字符串拼接

间接上相干代码,其实就是将HTML作为字符串一个个拼出来:

var songs =[        {name:'刚刚好', singer:'薛之谦', url:'http://music.163.com/xxx'},        {name:'最佳歌手', singer:'许嵩', url:'http://music.163.com/xxx'},        {name:'初学者', singer:'薛之谦', url:'http://music.163.com/xxx'},        {name:'绅士', singer:'薛之谦', url:'http://music.163.com/xxx'},        {name:'咱们', singer:'陈伟霆', url:'http://music.163.com/xxx'},        {name:'画风', singer:'后弦', url:'http://music.163.com/xxx'},        {name:'We Are One', singer:'郁可唯', url:'http://music.163.com/xxx'}     ]    //拼接字符串,有肯定歹意脚本注入危险 遍历    var html = '';    html +='<div class="song-list">'    html +='  <h1>热歌榜</h1>'    html +='  <ol>'    for(var i=0;i<songs.length;i++){        html += '<li>'+songs[i].name+' - '+songs[i].singer+'</li>'    }    html +='  </ol>'    html +='</div>'    document.body.innerHTML =html;

结构DOM对象

借助DOM对象和数据源来操作

// 结构DOM对象 遍历 毛病简单;       var elDiv = document.createElement('div')    elDiv.className = 'song-list';    var elH1 =document.createElement('h1')    elH1.appendChild(document.createTextNode('热歌榜'))    var elList = document.createElement('ol')    for(var i = 0; i<songs.length;i++){        var li = document.createElement('li')        li.textContent = songs[i].name +' - ' + songs[i].singer        elList.appendChild(li)    }    elDiv.appendChild(elH1);    elDiv.appendChild(elList);    document.body.appendChild(elDiv);

能够看到上述两种形式尽管能够达成需要,然而尤其繁琐且不足标准,很容易出错。

咱们这样思考,其实这些数据替换的中央都是固定的也有肯定的逻辑,那能不能将这个替换逻辑抽离进去造成标准,来对立进行解决呢?

3. 应用模板引擎的形式

置换型模板引擎

这种模板引擎原理比拟直观,实现也绝对简略,咱们先来看一下:

var template = '<p>Hello,my name is <%name%>. I am  <%age%> years old.</p>';        var data ={        name:'zyn',        age:31    }        var TemplateEngine = function (tpl,data){            var regex = /<%([^%>]+)?%>/g;        while(match = regex.exec(tpl)){            tpl = tpl.replace(match[0],data[match[1]])        }        return tpl    }    var string = TemplateEngine(template,data)    console.log(string);

这里其实就是把模板中须要替换的字符串做了个标记,这里是以<%...%>作为标记,而后替换时基于正则捕获该标记并进行数据源的替换(通过同一个key进行)

模板文件: var template = '<p>Hello,my name is <%name%>. I am  <%age%> years old.</p>';数据:  var data ={        name:'zyn',        age:31    }模板引擎: var TemplateEngine = function (tpl,data){        var regex = /<%([^%>]+)?%>/g;        while(match = regex.exec(tpl)){            tpl = tpl.replace(match[0],data[match[1]])        }        return tpl    }HTML文件: var string=TemplateEngine(template,data)    document.body.innerHTML= string 

JS代码函数型模板语法

上述形式存在一个问题,就是基本上以data["property"]形式来应用简略对象传递数据,然而如果对象是嵌套对象就有点难办:

var data ={        name:'zyn',        profile:{age:31}    }

在模板中应用<%profile.age%>的话,代码会被替换成data[‘profile.age’],后果是undefined,因为括号型没方法意识.符号,当然咱们能够改良Template函数来合成简单对象转换为[][]的模式。然而这里咱们换一个形式。

这里咱们思考是否肯定要在标记中写key或者惯例字符,能不能写一段有逻辑的JS代码进去,相似这样:

var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>'

这里为了之后的示范,咱们补充一下对于new Function的常识,这个函数的构造函数能够依据传入参数来动静生成一个函数,包含函数入参,函数体等:

var fn = new Function("num", "console.log(num + 1);");fn(2); //3

等同于:

var fn = function(num) {    console.log(num + 1);}fn(2); // 3

这里咱们思路根本明确了,就是心愿构建一个函数体字符串,而后利用JS代码执行过程帮咱们把数据绑定到模板下面。

这里咱们把所有字符串对立放到一个数组中,在程序最初将其拼接起来,而后借助new Function帮忙咱们解决JS逻辑:

var Arr=[];Arr.push("<p>Hello,my name is");Arr.push(this.name);Arr.push("i am");Arr.push(this.proflie.age)Arr.push("years old</p>")return  Arr.join('')

接下来须要做的还是去寻找模板中的标记位,即<%...%>片段,而后遍历所有的匹配项将其push到字符串数组中去,最初借助new Function实现。

咱们来看下初步的代码:

var TemplateEngine = function(tpl, data) {    // 正则全局匹配    // code用于保留函数体字符串    // cursor是游标,用于记录tpl解决的地位    var re = /<%([^%>]+)?%>/g,        code = 'var Arr=[];\n',        cursor = 0;        // 函数add负责将解析的代码行增加到code函数体中    // 前面的replace是将code蕴含的双引号进行本义    var add = function(line) {        code += 'Arr.push("' + line.replace(/"/g, '\\"') + '");\n';    }    // 循环解决模板,每当存在匹配项就进入循环体    while(match = re.exec(tpl)) {        add(tpl.slice(cursor, match.index));        add(match[1]);        cursor = match.index + match[0].length;    }    add(tpl.substr(cursor, tpl.length - cursor));    code += 'return Arr.join("");'; // <-- return the result    console.log(code);    return tpl;}var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';var data = {    name: "zyn",    profile: { age: 29 }}console.log(TemplateEngine(template, data));

循环过程:

第一次循环:match=[         0:<%this.name%>",         1:"this.name",         index:21,         input:"<p>Hello, my name is<%this.name%>.I'm<%this.profile.age%>years old.</p>",        length:2       ]tpl.slice(cursor, match.index) = "<p>Hello, my name is "执行函数add("<p>Hello, my name is ")code="var Arr=[];Arr.push("<p>Hello, my name is ");"在执行add(match[1]);match[1]="this.name"code ="var Arr=[];Arr.push("<p>Hello, my name is ");Arr.push("this.name");" cursor = match.index + match[0].length;cursor = 21+13=34;//就是<%this.name%>最初一位的地位;第二次循环跟第一次一样持续把模板文件增加到code上;两次循环实现后code = "var Arr[];Arr.push("<p>Hello, my name is ");Arr.push("this.name");Arr.push(". I'm ");Arr.push("this.profile.age")"cursor =60 ;而后执行: add(tpl.substr(cursor, tpl.length - cursor));cursor =60 ; tpl.length=75 tpl.substr(cursor, tpl.length - cursor)截取最初一段模板文件 years old.</p>code += 'return Arr.join("");'code = "var Arr[];Arr.push("<p>Hello, my name is ");Arr.push("this.name");Arr.push(". I'm ");Arr.push("this.profile.age")Arr.push("years old </p>")return Arr.join("")"如果还不明确能够复制代码在代码上打几个断点看下执行的过程,很快就能明确;

最初咱们会在控制台外面看见如下的内容:

var Arr[];Arr.push("<p>Hello, my name is ");Arr.push("this.name");Arr.push(". I'm ");Arr.push("this.profile.age")Arr.push("years old </p>")return Arr.join("")<p>Hello, my name is <%this.name%>. I'm <%this.profile.age%> years old.</p>

这里还存在一些问题:

  • this.name和this.profile.age不应该存在引号
  • 还没有创立函数
  • 是否能够反对更多简单的语句

最初欠缺之后的如下:

var TemplateEngine = function(html, options) {    var   re = /<%([^%>]+)?%>/g,           reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,          code = 'var Arr=[];\n',           cursor = 0;    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 += 'return r.join("");';    return new Function(code.replace(/[\r\t\n]/g, ''));}

这里感兴趣的能够基于下面的示例本人尝试去实现一下上一大节的例子~

dom-based模板引擎

dom-based模板引擎根本用于HTML相干畛域,输入模板间接是dom了(当然,很多dom-based模板引擎也能够很不便的挂载string输入端,从而在服务端也能输入)

而输出是没有具体规定的,你能够基于也有的DOM树,也能够是字符串(例如Angular)。还能够是本人定义的语言,只有你的模板引擎意识就行了(例如React的JSX,JSX能够说是AST-based的,因为其不依赖DOM,这里就不辨别那么细了)。

前者是须要模板引擎把字符串解析为AST,而后者就是定义了一套语法,给你语法糖让你本人去写AST了。失去AST后再解析失去模板语法,例如变量bind,循环,条件判断等。

dom-based模板引擎基本上不思考输入HTML/XML以外的货色

目前前端MVVM框架根本都内置了相干的模板引擎用于疾速且最小化实现DOM更新操作

4. 前端与后端的模板引擎渲染倒退变动

下面介绍的根本围绕模板引擎的实现原理和概念,上面次要剖析一下目前模板引擎的利用和倒退阶段,以及区别

倒退阶段

后端模板引擎渲染

最后模板引擎是放在后端的,那个时候动态网页居多,根本返回的都是后端拼接好的HTML,前端拿来间接渲染,而后再用JS进行一些交互解决就行。

该形式存在一些有余:

  • 前后端是在一个工程,不不便开发调试,与自动化测试
  • 前端没方法应用本人的生态
  • 前后端职责混同

然而该形式也领有页面渲染快,SEO敌对,当下不少纯展现性网页依然应用该形式进行解决

客户端渲染

随着后续前端工程化以及前后端职责拆散概念明确后,一系列前端MVVM框架也呈现了,客户端进行模板渲染慢慢成为支流。

此时后端只负责Model层解决,不再关怀任何渲染相干内容。

前后端解耦,数据通过ajax形式进行交互

劣势不言而喻:

  • 前端独立进去,能够充沛应用各个生态与工具
  • 更好治理
  • 职责明确

仍有有余:

  • 首屏加载迟缓,因为要等JS加载结束之后能力解决模板,渲染最终页面
  • SEO能力弱,因为html中根本都是模板信息,没有啥理论内容

node中间层

为了解决上述有余,便呈现了node中间层概念。

整个流程变为:浏览器 -> node -> 后端服务器 -> node -> 浏览器

一个典型的 node 中间层利用就是后端提供数据、node 层渲染模板、前端动静渲染。

这个过程中,node 层由前端开发人员掌控,页面中哪些页面在服务器上就渲染好,哪些页面在客户端渲染,由前端开发人员决定

这样做,达到了以下的目标:

  • 保留后端模板渲染、首屏疾速响应、SEO 敌对
  • 保留前端后拆散、客户端渲染的性能(首屏服务器端渲染、其余客户端渲染)

但这种形式也有一些有余:

  • 减少了一个中间层,利用性能有所升高
  • 减少了架构的复杂度、不稳定性,升高利用的安全性

对开发人员要求高了很多

服务器端渲染(SSR)

大部分状况下,服务器端渲染(SSR)与 node 中间层是同一个概念。只不过是在上文的根底上加上前端组件化技术,优化服务器端的渲染,例如针对react或vue

react、vue、angular 等框架的呈现,让前端组件化技术深入人心,但在一些须要首屏疾速加载与 SEO 敌对的页面就陷入了两难的地步了。
因为前端组件化技术天生就是给客户端渲染用的,而在服务器端须要被渲染成 html 文本,这的确不是一件很容易的事,所以服务器端渲染(ssr)就是为了解决这个问题。

好在社区始终在一直的摸索中,让前端组件化可能在服务器端渲染,比方 next.js、nuxt.js、razzle、react-server、beidou 等。

个别这些框架都会有一些目录构造、书写形式、组件集成、我的项目构建的要求,自定义属性可能不是很强。

以 next.js 为例,整个利用中是没有 html 文件的,所有的响应 html 都是 node 动静渲染的,包含外面的元信息、css, js 门路等。渲染过程中,next.js 会依据路由,将首页所有的组件渲染成 html,余下的页面保留原生组件的格局,在客户端渲染

应用倡议

把模板引擎渲染的过程放在前端(客户端)还是后端是要看具体利用场景的。

如果你的网页只是传统展现型网页,且须要SEO优化,很少须要实时刷新,交互少,那么传统的后端渲染模式还是能够应用的,再配合缓存,那么前端间接申请能够拿到最终页面了。

另一方面,如果你不须要首屏疾速加载,也不须要SEO优化,那么能够抉择全客户端渲染,开发方式最直观

又或者你能够尝试在须要首屏疾速渲染与SEO的中央不实用react、vue等框架技术,而在其余页面应用这些框架进行纯客户端渲染。

最终如果你的技术团队杰出且反对,而且又须要疾速渲染和SEO优化,且用了react,vue等技术,那你能够尝试搭建SSR渲染架构

5. 开源的模板引擎

这里简略举荐几个较优良的模板引擎,感兴趣的能够本人看一下源码持续深刻学习~

  • Art-template
  • Jinja
  • pug

6. 参考博客

  • 五分钟理解模板引擎原理,浏览后做出本人的模板引擎
  • 细说后端模板渲染、客户端渲染、node 中间层、服务器端渲染(ssr)