模板引擎的做用就是將模板渲染成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
使用正則表達式正則表達式
使用開源庫解析,如ohm,yacc,lex數組
本身寫有窮狀態自動機進行解析單元測試
做者本着自虐的心理,採起了第三種方法。測試
舉例說明有窮狀態自動機,解析<p>{{value}}</p>
的過程
ui
Init 狀態this
遇到<,轉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; // ...此處省去無數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數組用來鏈接子節點
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) ? 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地址