前言
模板引擎的作用就是将模板渲染成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>
的过程
Init 状态
遇到<,转Char状态
直到遇到{转化为LeftBrace,返回一个token
再遇{转Variable状态,返回一个token
解析value,直到}},再返回一个token
}}后再转状态,再返回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功能还比较单薄,存在以下不足:
不支持模板继承
不支持过滤器
condition表达式支持有限
错误提示不够完善
单元测试,持续集成没有完善
...
未来将一步步完善,另外无耻求个star
github地址