[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. 开源的模板引擎
这里简单推荐几个较优秀的模板引擎,感兴趣的可以自己看一下源码继续深入学习~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。