不用正则表达式,用javascript从零写一个模板引擎(一)

前言

模板引擎的作用就是将模板渲染成html,html = render(template,data),常见的js模板引擎有Pug,Nunjucks,Mustache等。网上一些制作模板引擎的文章大部分是用正则表达式做一些hack工作,看完能收获的东西很少。本文将使用编译原理那套理论来打造自己的模板引擎。之前玩过一年Django,还是偏爱那套模板引擎,这次就打算自己用js写一个,就叫jstemp

预览功能

写一个库,不可能一次性把所有功能全部实现,所以我们第一版就挑一些比较核心的功能

var jstemp = require('jstemp');
// 渲染变量
jstemp.render('{{value}}', {value: 'hello world'});// hello world// 渲染if/elseif/else表达式 
jstemp.render('{% if value1 %}hello{% elseif value %}world{% else %}byebye{% endif %}', {value: 'hello world'});// world// 渲染列表
jstemp.render('{%for item : list %}{{item}}{%endfor%}', {list:[1, 2, 3]});// 123

词法分析

词法分析就是将字符串分割成一个一个有意义的token,每个token都有它要表达的意义,供语法分析器去建AST。
jstemp的token类型如下

{EOF: 0, // 文件结束Character: 1, // 字符串Variable: 2, // 变量开始{{VariableName: 3, // 变量名IfStatement: 4,// if 语句IfCondition: 5,// if 条件ElseIfStatement: 6,// else if 语句ElseStatement: 7,// else 语句EndTag: 8,// }},%}这种闭合标签EndIfStatement: 9,// endif标签ForStatement: 10,// for 语句ForItemName: 11,// for item 的变量名ForListName: 12,// for list 的变量名EndForStatement: 13// endfor 标签
};

一般来说,词法分析有几种方法(欢迎补充)

  • 使用正则表达式

  • 使用开源库解析,如ohm,yacc,lex

  • 自己写有穷状态自动机进行解析

作者本着自虐的心理,采取了第三种方法。

举例说明有穷状态自动机,解析<p>{{value}}</p>的过程
输入图片说明

  1. Init 状态

  2. 遇到<,转Char状态

  3. 直到遇到{转化为LeftBrace,返回一个token

  4. 再遇{转Variable状态,返回一个token

  5. 解析value,直到}},再返回一个token

  6. }}后再转状态,再返回token,转init状态

结果是{type:Character,value:'<p>'},{type:Variable},{type:VariableName, valueName: 'value'},{type:EndTag},{type:Character,value:'</p>'}这五个token。(当然如果你喜欢,可以把{{value}}当作一个token,但是我这里分成了五个)。最后因为考虑到空格和if/elseif/else,for等情况,状态机又复杂了许多。

代码的话就是一个循环加一堆switch 转化状态(特别很累,也很容易出错),有一些情况我也没考虑全。截一部分代码下来看

nextToken() {Tokenizer.currentToken = '';while (this.baseoffset < this.template.length) {switch (this.state) {case Tokenizer.InitState:if (this.template[this.baseoffset] === '{') {this.state = Tokenizer.LeftBraceState;this.baseoffset++;}else if (this.template[this.baseoffset] === '\\') {this.state = Tokenizer.EscapeState;this.baseoffset++;}else {this.state = Tokenizer.CharState;Tokenizer.currentToken += this.template[this.baseoffset++];}break;case Tokenizer.CharState:if (this.template[this.baseoffset] === '{') {this.state = Tokenizer.LeftBraceState;this.baseoffset++;return TokenType.Character;}else if (this.template[this.baseoffset] === '\\') {this.state = Tokenizer.EscapeState;this.baseoffset++;}else {Tokenizer.currentToken += this.template[this.baseoffset++];}break;case Tokenizer.LeftBraceState:if (this.template[this.baseoffset] === '{') {this.baseoffset++;this.state = Tokenizer.BeforeVariableState;return TokenType.Variable;}else if (this.template[this.baseoffset] === '%') {this.baseoffset++;this.state = Tokenizer.BeforeStatementState;}else {this.state = Tokenizer.CharState;Tokenizer.currentToken += '{' + this.template[this.baseoffset++];}break;// ...此处省去无数casedefault:console.log(this.state, this.template[this.baseoffset]);throw Error('错误的语法');}}if (this.state === Tokenizer.InitState) {return TokenType.EOF;}else if (this.state === Tokenizer.CharState) {this.state = Tokenizer.InitState;return TokenType.Character;}else {throw Error('错误的语法');}}

具体代码看这里

语法分析

当我们将字符串序列化成一个个token后,就需要建AST树。树的根节点rootNode为一个childNodes数组用来连接子节点

let rootNode = {childNodes:[]}

字符串节点

{type:'character',value:'123'
}

变量节点

{type:'variable',valueName: 'name'
}

if 表达式的节点和for表达式节点可以嵌套其他语句,所以要多一个childNodes数组来装语句内的表达式,childNodes 可以装任意的node,然后我们解析的时候递归向下解析。elseifNodes 装elseif/else 节点,解析的时候,当if的conditon为false的时候,按顺序取elseifNodes数组里的节点,谁的condition为true,就执行谁的childNodes,然后返回结果。

// if node
{type:'if',condition: '',elseifNodes: [],childNodes:[],
}
// elseif node
{type: 'elseif',// 其实这个属性没用condition: '',childNodes:[]
}
// else node
{type: 'elseif',// 其实这个属性没用condition: true,childNodes:[]
}

for节点

{type:'for',itemName: '',listName: '',childNodes: []
}

举例:

let template = `
<p>how to</p>
{%for num : list %}let say{{num.num}}
{%endfor%}
{%if obj%}{{obj.test}}
{%else%}hello world
{%endif%}
`;// AST树为
let rootNode = {childNode:[{type:'char',value: '<p>how to</p>'},{type:'for',itemName: 'num',listName: 'list',childNodes:[{type:'char',value:'let say',},{type: 'variable',valueName: 'num.num'}]},{type:'if',condition: 'obj',childNodes: [{type: 'variable',valueName: 'obj.test'}],elseifNodes: [{type: 'elseif',condition:true,childNodes:[{type: 'char',value: 'hello world'}]}]}]
}

具体建树逻辑可以看代码

解析AST树

rootNode节点开始解析

let html = '';
for (let node of rootNode.childNodes) {html += calStatement(env, node);
}

calStatement为所有语句的解析入口

function calStatement(env, node) {let html = '';switch (node.type) {case NodeType.Character:html += node.value;break;case NodeType.Variable:html += calVariable(env, node.valueName);break;case NodeType.IfStatement:html += calIfStatement(env, node);break;case NodeType.ForStatement:html += calForStatement(env, node);break;default:throw Error('未知node type');}return html;
}

解析变量

// env为数据变量如{value:'hello world'},valueName为变量名
function calVariable(env, valueName) {if (!valueName) {return '';}let result = env;for (let name of valueName.split('.')) {result  = result[name];}return result;
}

解析if 语句及condition 条件

// 目前只支持变量值判断,不支持||,&&,<=之类的表达式
function calConditionStatement(env, condition) {if (typeof condition === 'string') {return calVariable(env, condition) ? true : false;}return condition ? true : false;
}function calIfStatement(env, node) {let status = calConditionStatement(env, node.condition);let result = '';if (status) {for (let childNode of node.childNodes) {// 递归向下解析子节点result += calStatement(env, childNode);}return result;}for (let elseifNode of node.elseifNodes) {let elseIfStatus = calConditionStatement(env, elseifNode.condition);if (elseIfStatus) {for (let childNode of elseifNode.childNodes) {// 递归向下解析子节点result += calStatement(env, childNode);}return result;}}return result;
}

解析for节点

function calForStatement(env, node) {let result = '';let obj = {};let name = node.itemName.split('.')[0];for (let item of env[node.listName]) {obj[name] = item;let statementEnv = Object.assign(env, obj);for (let childNode of node.childNodes) {// 递归向下解析子节点result += calStatement(statementEnv, childNode);}}return result;
}

结束语

目前的实现的jstemp功能还比较单薄,存在以下不足:

  1. 不支持模板继承

  2. 不支持过滤器

  3. condition表达式支持有限

  4. 错误提示不够完善

  5. 单元测试,持续集成没有完善

...
未来将一步步完善,另外无耻求个star
github地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/540293.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[转载] Java静态绑定与动态绑定

参考链接&#xff1a; Java中的静态绑定与动态绑定 程序绑定的概念&#xff1a; 绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来。对java来说&#xff0c;绑定分为静态绑定和动态绑定&#xff1b;或者叫做前期绑定和后期绑定. 静态绑定&#xff1a; 在程序执行前方…

关于批量插入数据之我见(100万级别的数据,mysql)

2019独角兽企业重金招聘Python工程师标准>>> 因前段时间去面试&#xff0c;问到如何高效向数据库插入10万条记录&#xff0c;之前没处理过类似问题&#xff0c;也没看过相关资料&#xff0c;结果没答上来&#xff0c;今天就查了些资料&#xff0c;总结出三种方法&am…

各个OS关于查看磁盘和wwn号的方法

1,HP-UX几个命令1&#xff09;查看型号&#xff0c;和uname -a差不多model2&#xff09;查看光纤卡信息ioscan -funC fc3) 查看扫描出的磁盘信息ioscan -fucC disk4&#xff09;查看磁盘及其对应的路径ioscan -m dsf5) 查看划分过来的lunioscan -m lun6) 查看磁盘大小diskinfo …

[转载] Java是不是面向对象的程序

参考链接&#xff1a; 为什么Java不是纯粹的面向对象语言 转载自&#xff1a;https://blog.csdn.net/a21700790yan/article/details/80129053 Java——是否确实的 “纯面向对象”&#xff1f;让我们深入到Java的世界&#xff0c;试图来证实它。 在我刚开始学习 Java 的前面几…

极速理解设计模式系列:6.适配器模式(Adapter Pattern)

四个角色&#xff1a;目标抽象类角色(Target)、目标对象角色(Adapter)、源对象角色(Adaptee)、客户端角色(Client) 目标抽象类角色&#xff08;Target)&#xff1a;定义需要实现的目标接口 目标对象角色&#xff08;Adapter)&#xff1a;调用另外一个源对象&#xff0c;并且转换…

[转载] Java之继承

参考链接&#xff1a; Java多重继承 Java之继承 继承是面向对象程序的一个基本特征&#xff0c;通过继承可以实现父子关系&#xff0c;以及代码的复用。通过继承实现的类称为子类&#xff0c;被继承的类称为父类&#xff0c;所有直接或间接被继承的类都称为父类。 Java类体…

Spark(二): 内存管理

2019独角兽企业重金招聘Python工程师标准>>> Spark 作为一个以擅长内存计算为优势的计算引擎&#xff0c;内存管理方案是其非常重要的模块&#xff1b; Spark的内存可以大体归为两类&#xff1a;execution和storage&#xff0c;前者包括shuffles、joins、sorts和agg…

[转载] 手把手教你整合最优雅SSM框架:SpringMVC + Spring + MyBatis

参考链接&#xff1a; Java继承类的对象创建 本文发表于2016年6月&#xff0c;写于作者学生时期。文中使用到的技术和框架可能不是当下最佳实践&#xff0c;甚至很不“优雅”。但对于刚接触JavaEE和Spring的同学来说&#xff0c;还是能有很多收获的&#xff0c;大牛轻拍 我们…

多播、组播、广播优缺点分析

2019独角兽企业重金招聘Python工程师标准>>> 单播、多播和广播单播”&#xff08;Unicast&#xff09;、“多播”&#xff08;Multicast&#xff09;和“广播”&#xff08;Broadcast&#xff09;这三个术语都是用来描述网络节点之间通讯方式的术语。那么这些术语究…

[转载] Java重载、覆盖与构造函数

参考链接&#xff1a; Java中的继承和构造函数 /** * 拷贝构造函数---Copyf t2 new Copyf(t1);就不会在调用默认构造函数了。 * 复制clone和引用 * 重载是在同一个类&#xff08;范围&#xff09;中&#xff0c;覆盖是子类对父类而言。 重载不关心返回值类型。 静态方法不能被…

LOFTERD18B542F16FF685FD684F427B4…

2019独角兽企业重金招聘Python工程师标准>>> 验证 转载于:https://my.oschina.net/jinhengyu/blog/1572124

[转载] Java获取一个类继承的父类或者实现的接口的泛型参数

参考链接&#xff1a; Java中的接口和继承 泛型的作用就不多介绍了&#xff0c;如果你想具备架构设计能力&#xff0c;那么熟练使用泛型是必不可少的。 不多说了&#xff0c;先定义泛型父类和泛型接口&#xff1a; package cn.zhh; public class Parent<T1, T2> { …

PHP系列(一)PHP流程控制结构

while(){} do{ }while(); for( 表达式1; 表达式2;表达式3 ){ 语句或语句序列; } if(){} if(){ }elseif{} <?php $i0; while(true) { if($i>100) break; echo ".$i.<br>"; $i; } ?> <?php echo "<table border1800>"; echo &quo…

[转载] Scala继承与Java的区别

参考链接&#xff1a; Java中将final与继承一起使用 在之前的笔记Java静态属性和方法的继承问题中&#xff0c;通过具体的实验证明&#xff0c;在子类中重写父类的字段时并没有覆盖父类的字段&#xff0c;只是隐藏了父类的字段。而在scala中则不同&#xff0c;scala子类的同名…

Source Map调试压缩后代码

在前端开发过程中&#xff0c;无论是样式还是脚本&#xff0c;运行时的文件可能是压缩后的&#xff0c;那这个时候调试起来就很麻烦。 这个时候&#xff0c;可以使用Source Map文件来优化调试&#xff0c;Source Map是一个信息文件&#xff0c;里面储存着原代码位置信息&#x…

[转载] Python3十大经典错误及解决办法

参考链接&#xff1a; Python中的关键字2 ◆ ◆ ◆ ◆ ◆ 接触了很多Python爱好者&#xff0c;有初学者&#xff0c;亦有转行人。不论大家学习Python的目的是什么&#xff0c;总之&#xff0c;学习Python前期写出来的代码不报错就是极好的。下面&#xff0c;严小样儿为大家罗…

两台电脑间大量数据拷贝的快捷方法

可能大家会遇到需要将一台电脑里的数据拷贝到另外一台电脑&#xff0c;最常用的方法是用u盘或移动硬盘等存储设备来拷贝&#xff0c;这样速度慢&#xff0c;而且可能拷贝多次才能将数据拷贝完。现提供一种方法&#xff0c;就是通过windows 的文件共享来实现。通过千兆网线直接连…

[转载] 使用 Web 标准生成 ASP.NET 2.0 Web 站点

参考链接&#xff1a; 使用super访问Java祖父母的成员 Stephen WaltherSuperExpert.com 适用于&#xff1a; Microsoft ASP.NET 2.0 (Beta 2) Microsoft Visual Studio .NET 2005 Microsoft Visual Web Developer 摘要&#xff1a; Microsoft ASP.NET 2.0 具有很多有用的功能…

Office快捷键大全之三(Access快捷键下篇)

向下键 向某帮助主题的末尾滚动 Page Up 以较大增量向某帮助主题的开头滚动 Page Down 以较大增量向某帮助主题的末尾滚动 Home 移到某帮助主题的开头 End 移到某帮助主题的末尾 CtrlP 打印当前帮助主题 CtrlA 选定整个帮助主题 CtrlC 将选定项复制到"剪贴…

[转载] 如何在Android设备之间共享Google Play应用,音乐等

参考链接&#xff1a; 使用super访问Java祖父母的成员 We recently showed you how to configure your iOS devices for app and media sharing; more than a few people wrote in asking how to do the same thing with Google Play purchases. Read on as we dig into how t…