早期数据渲染的几种方式

在模板引擎没有诞生之前,为了用JS把数据渲染到页面上,诞生了一系列数据渲染的方式。

最最基础的,莫过于直接使用DOM接口创建所有节点。

<div id="root"></div>
<script>
var root = document.getElementById('root'); var title = document.createElement('h1');
var titleText = document.createTextNode('Hello World!'); title.appendChild(titleText);
root.appendChild(title);
</script>

这种方式需要手动创建所有节点,再依次添加到父元素中,手续繁琐,基本不具有实际意义。

当然,也可以采用innerHTML的方式添加,上树:

var root = document.getElementById('root');
root.innerHTML = '<h1>Hello World!</h1>';

对于数据简单,嵌套层级较少的html代码块来说,这种方式无疑方便了许多,但是,若代码嵌套层级太多,会对代码可读性造成极大影响,因为''或者""都是不能换行的(ES6才有反引号可以换行),在一行代码里进行标签多层嵌套(想想现在看被转译压缩后的代码),这对编写和维护都会造成极大的困难。

直到有一个天才般的想法横空出世。

var root = document.getElementById('root');

var person = {
name: 'Wango',
age: 24,
gender: '男'
} root.innerHTML = [
'<ul>',
' <li>姓名: ' + person.name + '</li>',
' <li>年龄: ' + person.age + '</li>',
' <li>性别: ' + person.gender + '</li>',
'</ul>',
].join('');

这个方法将不可换行的多行字符串转换为数组的多个元素,再利用数组的join方法拼接字符串。使得代码的可读性大大提升。

当然,在ES6的模板字符串出来之后,这种hack技巧也失去了用武之地。

root.innerHTML = `
<ul>
<li>姓名: ${person.name}</li>
<li>年龄: ${person.age}</li>
<li>性别: ${person.gender}</li>
</ul>
`;

但是同样的,数据通常不是简单的对象,当数据更加复杂,数组的嵌套层次更深的时候,即便是模板字符串也是力不从心。

于是,mustache库诞生了!

实现mustache

接触过JavaJSP或者PythonDTL(The Django template language)等模板引擎的同学对{{}}语法一定不会陌生,模板引擎从后端引入前端后得到了更广泛的支持,而如今,已经快成为前端框架的标配了。

更多关于mustache的信息可以查看GitHub仓库:

janl/mustache.js

这个mustache.js库暴露的对象只有一个render方法,接收模板和数据。

<script type="text/template" id="tplt">
<ul>
<li>{{name}}</li>
<li>{{age}}</li>
<li>{{gender}}</li>
</ul>
</script>
<script>
var person = {
name: 'Wango',
age: 24,
gender: '男'
} var root = document.getElementById('root'); root.innerHTML = Mustache.render(
document.getElementById('tplt').innerHTML,
person
)
</script>

朴素的实现

没有编译思想的同学可能想到的第一种实现方式就是使用正则表达式配合replace方法来进行替换,而对于上面一个例子使用正则确实也是可以实现的。

var root = document.getElementById('root');

function render(tplt, data) {
// 捕获变量并使用数据进行替换
return tplt.replace(/{{\s*(\w+)\s*}}/g, function(match, $1) {
return data[$1];
});
} root.innerHTML = render(
document.getElementById('tplt').innerHTML,
person
)

对于简单的,单层无嵌套的结构来说,确实可以使用正则进行替换,但mustache还可以支持数组的遍历,多重嵌套遍历,对象属性的打点调用等,对于这些正则就捉襟见肘了。

编译思想的应用

在数据注入之前,我们需要在模板字符串编译为tokens数组,再将数据注入,将tokens拼接为最终的字符串,然后返回数据,这样做的好处是可以更方便地处理遍历和嵌套的问题。

于是,我们的模板引擎的render方法如下:

render(tplt, data) {
// 转换为tokens
const tokens = tokenizer(tplt);
// 注入数据,让tokens转换为DOM字符串
const html = tokens2dom(tokens, data);
// 返回数据
return html;
}

那么,tokenizertokens2dom又该如何实现呢?

首先来看tokenizer

这个函数的作用是将模板字符串转换为tokens数组,那么什么是token?简单来说,token指的是由类型、数据、嵌套结构等组成的数组,所有tokens就是一个二维数组,在本例中表现为

var tplt = `
<div>
<ol>
{{#students}}
<li>
学生{{name}}的爱好是
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/students}}
</ol>
</div>
`;

转换为:

[
["text", "<div><ol>"],
["#", "students", [
["text", "<li>学生"],
["name", "name"],
["text", "的爱好是<ol>"],
["#", "hobbies", [
["text", "<li>"],
["name", "."],
["text", "</li>"]
]],
["text", "</ol></li>"]
]],
["text", "</ol></div>"]
]

由上例可以看出,token有代表文本的text类型,代表循环的#类型,代表变量的name类型,此为token的第一个元素,token的第二个元素为这个类型的值,如果有第三个元素,那么第三个元素为嵌套的结构,当然,在janl/mustache.js中还有更多的类型,这里只是简单的列举几项。

Scanner对象

要从模板字符串转换为tokens,第一步我们应该想得到的应该是遍历真个模板字符串,找到其中的本文,变量和嵌套结构等类型,于是可以创建一个Scanner对象,专门负责遍历模板字符串和返回找到的文本。

同时,token中是不包含{{}}的,所有还需要定义一个方法跳过这两个字符串。

class Scanner {
constructor(tplt) {
this.tplt = tplt;
// 指针
this.pos = 0;
// 尾巴 剩余字符
this.tail = tplt;
} /**
* 路过指定内容
*
* @memberof Scanner
*/
scan(tag) {
if (this.tail.indexOf(tag) === 0) {
// 直接跳过指定内容的长度
this.pos += tag.length;
// 更新tail
this.tail = this.tplt.substring(this.pos);
}
} /**
* 让指针进行扫描,直到遇见指定内容,返回路过的文字
*
* @memberof Scanner
* @return str 收集到的字符串
*/
scanUnitl(stopTag) {
// 记录开始扫描时的初始值
const startPos = this.pos;
// 当尾巴的开头不是stopTg的时候,说明还没有扫描到stopTag
while (!this.eos() && this.tail.indexOf(stopTag) !== 0 ) {
// 改变尾巴为当前指针这个字符到最后的所有字符
this.tail = this.tplt.substring(++this.pos);
}
// 返回经过的文本数据
return this.tplt.substring(startPos, this.pos).trim();
} /**
* 判断指针是否到达文本末尾(end of string)
*
* @memberof Scanner
*/
eos() {
return this.pos >= this.tplt.length;
}
}

扫描到了相关内容,我们就可以将数据收集起来,并转换为不含嵌套结构的token,于是定义一个collectTokens函数:

function collectTokens(scanner) {
const tokens = [];
let word = '';
// 当scanner没有到头的就持续将获取的token加入数组中
while (!scanner.eos()) {
// 收集文本
word = scanner.scanUnitl('{{');
word && tokens.push(['text', word]);
scanner.scan('{{'); // 收集变量
word = scanner.scanUnitl('}}'); // 对不同类型结构进行分类标识
switch (word[0]) {
case '#':
tokens.push(['#', word.substring(1)]);
break;
case '/':
tokens.push(['/', word.substring(1)]);
break;
default:
word && tokens.push(['name', word]);
} scanner.scan('}}');
} return tokens;
}

这时,我们得到了一个这样的数组:

[
["text", "<div>↵ <ol>"],
["#", "students"],
["text", "<li>↵ 学生"],
["name", "name"],
["text", "的爱好是↵ <ol>"],
["#", "hobbies"],
["text", "<li>"],
["name", "."],
["text", "</li>"],
["/", "hobbies"],
["text", "</ol>↵ </li>"],
["/", "students"],
["text", "</ol>↵ </div>"]
]

可以看到,除了嵌套结构外,tokens的基本特征已经具备了。那么嵌套结构该如何加入呢?我们可以分析出:

["#", "students"],
["#", "hobbies"],
["/", "hobbies"],
["/", "students"],

可知#是嵌套结构的开始,/是嵌套结构的结束,同时,先出现的students反而后结束,而后出现的hobbies反而先结束。对数据结构有一些研究的同学应该立即就能想到一种数据结构: ---- 一种先进后出的结构。而在JS中,可以用数组pushpop方法模拟栈结构。只要遇见#我们就压栈,记录当前是哪个层级,遇见/就出栈,退出当前层级,直到退到最外层。

于是,我们有了一个新的函数nestTokens:

function nestTokens(tokens) {
const nestedTokens = [];
const stack = [];
// 收集器默认为最外层
let collector = nestedTokens; for (let i = 0, len = tokens.length; i < len; i++) {
const token = tokens[i]; switch (token[0]) {
case '#':
// 收集当前token
collector.push(token);
// 压入栈中
stack.push(token);
// 由于进入了新的嵌套结构,新建一个数组保存嵌套结构
// 并修改collector的指向
collector = token[2] = [];
break;
case '/':
// 出栈
stack.pop();
// 将收集器指向上一层作用域中用于存放嵌套结构的数组
collector = stack.length > 0
? stack[stack.length - 1][2]
: nestedTokens;
break;
default:
collector.push(token);
}
} return nestedTokens;
}

于是我们的tokenizer函数就很好实现了,直接调用上面两个函数即可:

function tokenizer(tplt) {
const scanner = new Scanner(tplt.trim()); // 收集tokens,并将循环内容嵌套到tokens中,并返回
return nestTokens(collectTokens(scanner));
}

到这里,模板引擎已经完成了一大半,剩下的就是将数据注入和返回最终的字符串了。也就是tokens2dom函数。

不过在此之前,我们还要再解决一个问题,还记得我们在使用正则替换时是怎么注入数据的吗?

tplt.replace(/{{\s*(\w+)\s*}}/g, function(match, $1) {
return data[$1];
});

回顾一下,我们是通过data[$1]来获取对象数据的,可是,如果我的模板里写的是类似{{a.b.c}}这样的打点调用该怎么办?JS可不支持obj[a.b.c]这样的写法,而janl/mustache.js中是支持变量打点调用的。所以,在数据注入前,我们还需要一个函数来解决这个问题。

于是:

/**
* 在对象obj中用连续的打点字符串寻找到对象值
*
* @example lookup({a: {b: {c: 100}}}, 'a.b.c')
*
* @param {object} obj
* @param {string} key
* @return any
*/
function lookup(obj, key) {
const keys = key.split('.'); // 设置临时变量,一层一层查找
let val = obj;
for (const k of keys) {
if(val === undefined) {
console.warn(`Can't read ${k} of undefined`);
return '';
};
val = val[k];
}
return val;
}

解决了打点调用的问题,就可以开始数据注入了,我们要对不同类型的数据进行不同的操作,文本就直接拼接,变量就查找数据,循环的就遍历,嵌套的就递归。

于是:

function tokens2dom(tokens, data) {

  let html = '';
for (let i = 0, len = tokens.length; i < len; i++) {
const token = tokens[i]; // 按类型拼接字符串
switch (token[0]) {
case 'name':
if (token[1] === '.') {
html += data;
} else {
html += lookup(data, token[1]);
}
break;
case '#':
// 递归解决数组嵌套的情况
for (const item of data[token[1]]) {
html += tokens2dom(token[2], item);
}
break;
default:
html += token[1];
}
} return html;
}

到这里我们的模板引擎就全部结束啦,再向全局暴露一个对象方便调用:

// 暴露全局变量
window.TemplateEngine = {
render(tplt, data) {
// 转换为tokens
const tokens = tokenizer(tplt);
// 注入数据,让tokens转换为DOM字符串
const html = tokens2dom(tokens, data); return html;
}
}

这里只是对mustache的一个简单实现,还有类似于条件渲染等功能没有实现,同学们有兴趣的可以看看源码

janl/mustache.js

当然可以看本文实现的代码mini-tplt

前端数据渲染及mustache模板引擎的简单实现的更多相关文章

  1. JS模板引擎-Mustache模板引擎使用实例1-表格树

    1 使用实例代码 1.jsp代码 <!DOCTYPE html> <html lang="zh-CN"> <head> <title> ...

  2. 前端学PHP之自定义模板引擎

    前面的话 在大多数的项目组中,开发一个Web程序都会出现这样的流程:计划文档提交之后,前端工程师制作了网站的外观模型,然后把它交给后端工程师,它们使用后端代码实现程序逻辑,同时使用外观模型做成基本架构 ...

  3. 前端学PHP之Smarty模板引擎

    前面的话 对PHP来说,有很多模板引擎可供选择,但Smarty是一个使用PHP编写出来的,是业界最著名.功能最强大的一种PHP模板引擎.Smarty像PHP一样拥有丰富的函数库,从统计字数到自动缩进. ...

  4. Mustache模板引擎

    Mustache是一个Logic-Less模板引擎,即:零逻辑引擎,原因在于它只有标签,没有流程控制语句,这是它与其它模板引擎不同的地方. Mustache小巧玲珑,几乎用各种语言都实现了一遍. Mu ...

  5. nodejs+Express中使用mustache模板引擎

    由于公司一个seo项目,需要我协助.此项目他人已经开发大半,由于seo需要,使用了服务器端模板引擎.我项目的后端同事说项目是go语音写的,跑项目麻烦,只给了我template和css等静态文件. 为了 ...

  6. 前端知识点回顾——koa和模板引擎

    koa 基于Node.js的web框架,koa1只兼容ES5,koa2兼容ES6及以后. const Koa = requier("koa"); const koa = new K ...

  7. 【原创】javascript模板引擎的简单实现

    本来想把之前对artTemplate源码解析的注释放上来分享下,不过隔了一年,找不到了,只好把当时分析模板引擎原理后,自己尝试 写下的模板引擎与大家分享下,留个纪念,记得当时还对比了好几个模板引擎来着 ...

  8. 2019-07-24 Smarty模板引擎的简单应用

    smarty是什么? Smarty是一个使用PHP写出来的模板引擎,是业界最著名的PHP模板引擎之一.Smarty分离了逻辑代码和外在的内容,提供一种易于管理和使用的方法,用来将原本与HTML代码混杂 ...

  9. SpringBoot静态资源访问+拦截器+Thymeleaf模板引擎实现简单登陆

    在此记录一下这十几天的学习情况,卡在模板引擎这里已经是四天了. 对Springboot的配置有一个比较深刻的认识,在此和大家分享一下初学者入门Spring Boot的注意事项,如果是初学SpringB ...

随机推荐

  1. 元类、orm

    目录 一.内置函数exec 二.元类 1. 什么是元类 2. 元类的作用 3. 创建类的两种方法 4. 怎么自定义创建元类 三.ORM 1. ORM中可能会遇到的问题 2. ORM中元类需要解决的问题 ...

  2. linux之安装nginx

    nginx官网:http://nginx.org/en/download.html 1.安装nginx所需环境 a)  PCRE pcre-devel 安装 # yum install -y pcre ...

  3. 使用docker-compose配置mysql数据库并且初始化用户

    使用docker-compose配置mysql数据库并且初始化用户 docker-compose  测试创建一个docker-compose.yml测试 以下配置了外部数据卷.外部配置文件.外部初始化 ...

  4. redis.conf 配置说明

    redis.conf 配置项说明如下: 1. Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程 daemonize no 2. 当Redis以守护进程方式运行时,R ...

  5. MySQL注入与informantion_schema库

    目录 只可读 自动开启 和MySQL注入有关的3个表 手动注入的使用案例 表介绍 查询一个表中全部字段的过程 MySQL V5.0安装完成会默认会生成一个库(informantion_schema), ...

  6. jQuery实现QQ简易聊天框

    实现效果: html代码: <section id="chat"> <div class="chatBody"></div> ...

  7. 剑指 Offer 20. 表示数值的字符串 + 有限状态自动机

    剑指 Offer 20. 表示数值的字符串 Offer 20 常规解法: 题目解题思路:需要注意几种情况: 输入的字符串前后可能有任意多个空格,这是合法的. 正负号: (1)正负号只能出现一次. (2 ...

  8. 记离线部署docker,以及docker下部署zabbix

    一.离线安装docker 下载地址:https://download.docker.com/linux/static/stable/x86_64/ 上传软件并解压 [root@localhost op ...

  9. 谈谈注册中心 zookeeper 和 eureka中的CP和 AP

    谈谈注册中心 zookeeper 和 eureka中的CP和 AP 前言 在分布式架构中往往伴随CAP的理论.因为分布式的架构,不再使用传统的单机架构,多机为了提供可靠服务所以需要冗余数据因而会存在分 ...

  10. WPF 基础 - Trigger

    1. Trigger 1.1 由属性值触发的 Trigger 最基本的触发器,Property 是关注的属性名称,value 是触发条件,一旦触发条件满足,就会应用 Trigger 的 Setters ...