不用正则表达式,用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,一经查实,立即删除!

相关文章

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

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

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

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

Spark(二): 内存管理

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

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

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

LOFTERD18B542F16FF685FD684F427B4…

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

Source Map调试压缩后代码

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

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

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

工作所用的模块回滚脚本

俗话说的好&#xff1a;“真男人从来不回滚”&#xff0c;但是家中常备一个回滚脚本也是很有必要的&#xff0c;我所在公司的服务器模块名都是在初始化的时候写进/etc/role_install这个文件里&#xff0c;如下图的这个服务器就是fss服务器&#xff1a; 再比如下面这个服务器&am…

Quartus II 8.1 详解--有图---图片详解 【1讲】

Quartus II 8.1 详解--有图---图片详解 看图片比较清楚&#xff0c;比文章清楚的多 本文转自 zhangguangyi 51CTO博客&#xff0c;原文链接http://blog.51cto.com/bosszhang/808019:

shell下的进度条和最大最小平均值

进度条 C语言下的进度条参考我原来的一篇blog->进度条 1234567891011121314151617181920212223function proc() {i0 str arr(| / - \\) index0 while [ $i -le 100 ] do printf "[%-101s][%d%%][%c]\r" "$str" "$i" "${arr[$index]}&qu…

Linux系统管理初步(七)系统服务管理、chkconfig与systemd 编辑中

Linux系统本身包含了很多服务&#xff0c;CentOS6之前系统的服务用SysV控制&#xff0c;CentOS7改为systemd控制 一、chkconfig服务管理机制 简而言之&#xff0c;chkconfig就是CentOS6以前用来控制系统服务的工具&#xff0c; 常用方法举例 chkconfig --list #列出所有的系统服…

窗体间传递数据

前言 做项目的时候&#xff0c;winfrom因为没有B/S的缓存机制&#xff0c;窗体间传递数据没有B/S页面传递数据那么方便&#xff0c;今天我们就说下winfrom中窗体传值的几种方式。 共有字段传递 共有字段传递实现起来很方便&#xff0c;就是在窗体类中加个共有字段属性&#xff…

微信抢房软件开发

2019独角兽企业重金招聘Python工程师标准>>> 这两年楼市真可谓是一个"火“字难以形容 经历了长沙两次开盘&#xff0c;都没有抢到&#xff0c;目前还没有买到房子&#xff0c;说说我的悲剧吧&#xff0c;让大伙都开心开心 第一次抢房是今年4月份长沙万科金域国…

11.13 模10计数器设计

.新建一个工程 Family&#xff1a;FLEX10K Available device&#xff1a;EPF10K20TC144-3 2.设置lpm_counter宏单元参数并连接引脚 连接引脚的时候要注意的是&#xff0c;向量线的连接。 3.时序仿真 检查无误后进行下一步 4.载入7448并进行引脚连接 5.分配管脚 再次编译&#x…

使用valueOf前必须进行校验

每个枚举都是java.lang.Enum的子类,都可以访问Enum类提供的方法,比如hashCode(),name(),valueOf()等..... 其中valueOf()方法会把一个String类型的名称转变为枚举项,也就是枚举项中查找出字面值与该参数相等的枚举项,虽然这个方法很简单,但是JDK却做了一个对于开发人员来说并不…

多IDC GSLB的部署

之前已经介绍过GSLB的实现原理&#xff0c;这里再向大家讲述一下GSLB经常遇到的部署方式&#xff0c;多IDC的部署。很多大型的企业或业务容灾要求非常高的客户都会部署有多个异地的数据中心&#xff0c;以保证其业务的“全天候”不间断的正常运行&#xff0c;而要整合多个IDC的…

信息系统开发平台OpenExpressApp:【OpenTest】 之 语法及其使用介绍

在OpenTest 之 运行环境准备中介绍了运行自动化测试需要做的一些准备工作&#xff0c;本篇将继续给大家介绍OpenTest的脚本语法以及使用方法&#xff0c;通过学习后读者应该能够开始动手编写UI自动化测试脚本了。 关键字驱动测试 在学习语法之前&#xff0c;需要了解一下关键字…

Oracle 10g 高级安装图文教程(二)

第八步&#xff1a;为了简便起见&#xff08;工作中肯定不安全&#xff09;&#xff0c;选择“所有的帐户都是用同一个口令”&#xff0c;并输入口令&#xff0c;点击“下一步”&#xff1a;本文转自 victoryan 51CTO博客&#xff0c;原文链接:http://blog.51cto.com/victoryan…

Gram matrix 格拉姆矩阵

2019独角兽企业重金招聘Python工程师标准>>> Gram matrix 度量各个维度自己的特性以及各个维度之间的关系。 来自&#xff1a;https://www.zhihu.com/question/49805962?fromprofile_question_card 由感知机&#xff08;对偶感知机中需要计算样本点两两之间的内积和…

MySQL日常应用操作记录

1.知道一个字段名&#xff0c;怎样查到它在数据库里的哪张表里&#xff1f; USE Information_schema;SELECT TABLE_NAME FROM COLUMNS WHERE COLUMN_NAME字段名称; MySQL中查看库表字段信息都在information_schemal中&#xff0c;获取数据字典等信息都要通过这个视图。 如&…