模板可以大大提升开拓效率,如果没有模板开拓职员怕是要手动拼写字符串。
var tpl = '<p>' + user.name + '</p>';
$('body').append(tpl);
在近些年前端发展过程中,模板也随着变革:
1. php模板 JSP模板
早期还没有前后端分离时期,前端只是后端项目中的一个文件夹,这期间的php和java都供应了各自的模板引擎。以JSP为例:java web运用的页面常日是一个个.jsp的文件,这个文件内容是大部分的html以及一些模板自带语法,实质上是纯文本,但是既不是html也不是java。
JSP语法:index.jsp
<html>
<head><title>Hello World</title></head>
<body>
Hello World!<br/>
<%
out.println(\"大众Your IP address is \"大众 + request.getRemoteAddr());
%>
</body>
</html>
这个期间的模板引擎,每每是做事端来编译模板字符串,天生html字符串给客户端。
2. handlebar mustache通用模板
09年node发布,JavaScript也可以来实现做事真个功能,这也大大的方便了开拓职员。mustache和handlebar模板的出身方便了前端开拓职员,这两个模板均利用JavaScript来实现,从此前端模板既可以在做事端运行,也可以在客户端运行,但是大多数利用场景都是js根据做事端异步获取的数据套入模板,天生新的dom插入页码。 对前端后端开拓都非常有利。
mustache语法:index.mustache
<p>Username: {{user.name}}</p>
{{#if (user.gender === 2)}}
<p>女</p>
{{/if}}
3. vue中的模板 React中的JSX
接下来到了新生代,vue中的模板写法跟之前的模板有所不同,而且功能更加强大。既可以在客户端利用也可以在做事端利用,但是利用场景上差距非常大:页面每每根据数据变革,模板天生的dom发生变革,这对付模板的性能哀求很高。
vue语法:index.vue
<p>Username: {{user.name}}</p>
<template v-if=\"大众user.gender === 2\公众>
<p>女</p>
</div>
模板实现的功能无论是从JSP到vue的模板,模板在语法上越来越简便,功能越来越丰富,但是基本功能是不能少的:
变量输出(转义/不转义):出于安全考虑,模板基本默认都会将变量的字符串转义输出,当然也实现了不转义输出的功能,慎重利用。条件判断(if else):开拓中常常须要的功能。循环变量:循环数组,天生很多重复的代码片段。模板嵌套:有了模板嵌套,可以减少很多重复代码,并且嵌套模板集成浸染域。以上功能基本涵盖了大多数模板的根本功能,针对这些根本功能就可以探究模板如何实现的。
模板实现事理正如标题所说的,模板实质上都是纯文本的字符串,字符串是如何操作js程序的呢?
模板用法上:
var domString = template(templateString, data);
模板引擎获得到模板字符串和模板的浸染域,经由编译之后天生完全的DOM字符串。
大多数模板实现事理基本同等:
模板字符串首先通过各种手段剥离出普通字符串和模板语法字符串天生抽象语法树AST;然后针对模板语法片段进行编译,期间模板变量均去引擎输入的变量中查找;模板语法片段天生出普通html片段,与原始普通字符串进行拼接输出。
实在模板编译逻辑并没有特殊繁芜,至于vue这种动态绑天命据的模板有韶光可以参考文末链接。
快速实现大略的模板
现在以mustache模板为例,手动实现一个实现基本功能的模板。
模板字符串模板:index.txt
<!DOCTYPE html>
<html>
<head>
<meta charset=\"大众utf-8\"大众 />
<meta http-equiv=\"大众X-UA-Compatible\"大众 content=\"大众IE=edge\公众>
<title>Page Title</title>
<meta name=\"大众viewport\"大众 content=\"大众width=device-width, initial-scale=1\"大众>
<link rel=\公众stylesheet\"大众 type=\"大众text/css\"大众 media=\公众screen\"大众 href=\公众main.css\"大众 />
<script src=\"大众main.js\"大众></script>
</head>
<body>
<h1>Panda模板编译</h1>
<h2>普通变量输出</h2>
<p>username: {{common.username}}</p>
<p>escape:{{common.escape}}</p>
<h2>不转义输出</h2>
<p>unescape:{{&common.escape}}</p>
<h2>列表输出:</h2>
<ul>
{{#each list}}
<li class=\"大众{{value}}\"大众>{{key}}</li>
{{/each}}
</ul>
<h2>条件输出:</h2>
{{#if shouldEscape}}
<p>escape{{common.escape}}</p>
{{else}}
<p>unescape:{{&common.escape}}</p>
{{/if}}
</body>
</html>
模板对应数据:
module.exports = {
common: {
username: 'Aus',
escape: '<p>Aus</p>'
},
shouldEscape: false,
list: [
{key: 'a', value: 1},
{key: 'b', value: 2},
{key: 'c', value: 3},
{key: 'd', value: 4}
]
};
模板的利用方法:
var fs = require(\"大众fs\公众);
var tpl = fs.readFileSync('./index.txt', 'utf8');
var state = require('./test');
var Panda = require('./panda');
Panda.render(tpl, state)
然后来实现模板:
1. 正则切割字符串模板引擎获取到模板字符串之后,常日要利用正则切割字符串,区分出那些是静态的字符串,那些是须要编译的代码块,天生抽象语法树(AST)。
// 将未处理过的字符串进行分词,形成字符组tokens
Panda.prototype.parse = function (tpl) {
var tokens = [];
var tplStart = 0;
var tagStart = 0;
var tagEnd = 0;
while (tagStart >= 0) {
tagStart = tpl.indexOf(openTag, tplStart);
if (tagStart < 0) break;
// 纯文本
tokens.push(new Token('text', tpl.slice(tplStart, tagStart)));
tagEnd = tpl.indexOf(closeTag, tagStart) + 2;
if (tagEnd < 0) throw new Error('{{}}标签未闭合');
// 细分js
var tplValue = tpl.slice(tagStart + 2, tagEnd - 2);
var token = this.classifyJs(tplValue);
tokens.push(token);
tplStart = tagEnd;
}
// 末了一段
tokens.push(new Token('text', tpl.slice(tagEnd, tpl.length)));
return this.parseJs(tokens);
};
这一步分割字符串通常利用正则来完成的,后面检索字符串会大量用到正则方法。
在这一步常日可以检讨出模板标签闭合非常,并报错。
2. 模板语法的分类天生AST之后,普通字符串不须要再管了,末了会直接输出,专注于模板语法的分类。
// 专门处理模板中的js
Panda.prototype.parseJs = function (tokens) {
var sections = [];
var nestedTokens = [];
var conditionsArray = [];
var collector = nestedTokens;
var section;
var currentCondition;
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
var value = token.value;
var symbol = token.type;
switch (symbol) {
case '#': {
collector.push(token);
sections.push(token);
if(token.action === 'each'){
collector = token.children = [];
} else if (token.action === 'if') {
currentCondition = value;
var conditionArray;
collector = conditionArray = [];
token.conditions = token.conditions || conditionsArray;
conditionsArray.push({
condition: currentCondition,
collector: collector
});
}
break;
}
case 'else': {
if(sections.length === 0 || sections[sections.length - 1].action !== 'if') {
throw new Error('else 利用缺点');
}
currentCondition = value;
collector = [];
conditionsArray.push({
condition: currentCondition,
collector: collector
});
break;
}
case '/': {
section = sections.pop();
if (section && section.action !== token.value) {
throw new Error('指令标签未闭合');
}
if(sections.length > 0){
var lastSection = sections[sections.length - 1];
if(lastSection.action === 'each'){
collector = lastSection.chidlren;
} else if (lastSection.action = 'if') {
conditionsArray = [];
collector = nestedTokens;
}
} else {
collector = nestedTokens;
}
break;
}
default: {
collector.push(token);
break;
}
}
}
return nestedTokens;
}
上一步我们天生了AST,这个AST在这里便是一个分词token数组:
[
Token {},
Token {},
Token {},
]
这个token便是每一段字符串,分别记录了token的类型,动作,子token,条件token等信息。
/
token类表示每个分词的标准数据构造
/
function Token (type, value, action, children, conditions) {
this.type = type;
this.value = value;
this.action = action;
this.children = children;
this.conditions = conditions;
}
在这一步要将循环方法中的子token嵌套到对应的token中,以及条件渲染子token嵌套到对应token中。
这步完成之后,一个标准的带有嵌套关系的AST完成了。
3. 变量查找与赋值现在开始根据token中的变量查找到对应的值,根据相应功能天生值得字符串。
/
解析数据构造的类
/
function Context (data, parentContext) {
this.data = data;
this.cache = { '.': this.data };
this.parent = parentContext;
}
Context.prototype.push = function (data) {
return new Context(data, this);
}
// 根据字符串name找到真实的变量值
Context.prototype.lookup = function lookup (name) {
name = trim(name);
var cache = this.cache;
var value;
// 查询过缓存
if (cache.hasOwnProperty(name)) {
value = cache[name];
} else {
var context = this, names, index, lookupHit = false;
while (context) {
// user.username
if (name.indexOf('.') > 0) {
value = context.data;
names = name.split('.');
index = 0;
while (value != null && index < names.length) {
if (index === names.length - 1) {
lookupHit = hasProperty(value, names[index]);
}
value = value[names[index++]];
}
} else {
value = context.data[name];
lookupHit = hasProperty(context.data, name);
}
if (lookupHit) {
break;
}
context = context.parent;
}
cache[name] = value;
}
return value;
}
为了提高查找效率,采取缓存代理,每次查找到的变量存储路径方便下次快速查找。
不同于JavaScript编译器,模板引擎在查找变量的时候找不到对应变量即终止查找,返回空并不会报错。
4. 节点的条件渲染与嵌套
这里开始讲模板语法token和普通字符串token开始统一编译天生字符串,并拼接成完全的字符串。
// 根据tokens和context稠浊拼接字符串输出结果
Panda.prototype.renderTokens = function (tokens, context) {
var result = '';
var token, symbol, value;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
value = undefined;
token = tokens[i];
symbol = token.type;
if (symbol === '#') value = this.renderSection(token, context);
else if (symbol === '&') value = this.unescapedValue(token, context);
else if (symbol === '=') value = this.escapedValue(token, context);
else if (symbol === 'text') value = this.rawValue(token);
if (value !== undefined) result += value;
}
return result;
}
5. 绘制页面页面字符串已经解析完成,可以直接输出:
Panda.prototype.render = function (tpl, state) {
if (typeof tpl !== 'string') {
return new Error('请输入字符串!
');
}
// 解析字符串
var tokens = this.cache[tpl] ? tokens : this.parse(tpl);
// 解析数据构造
var context = state instanceof Context ? state : new Context(state);
// 渲染模板
return this.renderTokens(tokens, context);
};
输出页面字符串被浏览器解析,就涌现了页面。
以上只是大略的模板实现,并没有经由系统测试,仅供学习利用,源码传送门。成熟的模板引擎是有完全的非常处理,变量查找解析,浸染域更换,优化渲染,断点调试等功能的。
总结前端模板这块能做的东西还很多,很多框架都是集成模板的功能,合营css,js等稠浊编译天生解析好样式和绑定成功事宜的dom。
其余实现模板的办法也有很多,本文的实现办法参考了mustache源码,模板标签内的代码被解析,但是是通过代码片段分类,变量查找的办法来实行的,将纯字符串的代码变成了被阐明器实行的代码。
其余向vue这种可以实现双向绑定的模板可以抽空多看一看。