模板引擎的做用就是将模板渲染成html,html = render(template,data)
,常见的js模板引擎有Pug,Nunjucks,Mustache等。网上一些制做模板引擎的文章大部分是用正则表达式作一些hack工做,看完能收获的东西不多。本文将使用编译原理那套理论来打造本身的模板引擎。以前玩过一年Django,仍是偏心那套模板引擎,此次就打算本身用js写一个,就叫jstemphtml
写一个库,不可能一次性把全部功能所有实现,因此咱们初版就挑一些比较核心的功能node
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类型以下git
{
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 标签
};复制代码
通常来讲,词法分析有几种方法(欢迎补充)github
做者本着自虐的心理,采起了第三种方法。正则表达式
举例说明有穷状态自动机,解析<p>{{value}}</p>
的过程
数组
结果是{type:Character,value:'<p>'}
,{type:Variable}
,{type:VariableName, valueName: 'value'}
,{type:EndTag}
,{type:Character,value:'</p>'}
这五个token。(固然若是你喜欢,能够把{{value}}
看成一个token,可是我这里分红了五个)。最后由于考虑到空格和if/elseif/else,for等状况,状态机又复杂了许多。bash
代码的话就是一个循环加一堆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;
// ...此处省去无数case
default:
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数组用来链接子节点ui
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'
}
]
}
]
}
]
}复制代码
具体建树逻辑能够看代码
解析变量节点
从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);
}
return !!condition;
}
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功能还比较单薄,存在如下不足:
...
将来将一步步完善,另外无耻求个star
github地址