javascript
不用正则表达式,用javascript从零写一个模板引擎(一)
前言
模板引擎的作用就是將模板渲染成html,html = render(template,data),常見的js模板引擎有Pug,Nunjucks,Mustache等。網(wǎng)上一些制作模板引擎的文章大部分是用正則表達式做一些hack工作,看完能收獲的東西很少。本文將使用編譯原理那套理論來打造自己的模板引擎。之前玩過一年Django,還是偏愛那套模板引擎,這次就打算自己用js寫一個,就叫jstemp
預覽功能
寫一個庫,不可能一次性把所有功能全部實現(xiàn),所以我們第一版就挑一些比較核心的功能
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類型如下
一般來說,詞法分析有幾種方法(歡迎補充)
使用正則表達式
使用開源庫解析,如ohm,yacc,lex
自己寫有窮狀態(tài)自動機進行解析
作者本著自虐的心理,采取了第三種方法。
舉例說明有窮狀態(tài)自動機,解析<p>{{value}}</p>的過程
Init 狀態(tài)
遇到<,轉Char狀態(tài)
直到遇到{轉化為LeftBrace,返回一個token
再遇{轉Variable狀態(tài),返回一個token
解析value,直到}},再返回一個token
}}后再轉狀態(tài),再返回token,轉init狀態(tài)
結果是{type:Character,value:'<p>'},{type:Variable},{type:VariableName, valueName: 'value'},{type:EndTag},{type:Character,value:'</p>'}這五個token。(當然如果你喜歡,可以把{{value}}當作一個token,但是我這里分成了五個)。最后因為考慮到空格和if/elseif/else,for等情況,狀態(tài)機又復雜了許多。
代碼的話就是一個循環(huán)加一堆switch 轉化狀態(tài)(特別很累,也很容易出錯),有一些情況我也沒考慮全。截一部分代碼下來看
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;// ...此處省去無數(shù)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樹。樹的根節(jié)點rootNode為一個childNodes數(shù)組用來連接子節(jié)點
let rootNode = {childNodes:[]}字符串節(jié)點
{type:'character',value:'123' }變量節(jié)點
{type:'variable',valueName: 'name' }if 表達式的節(jié)點和for表達式節(jié)點可以嵌套其他語句,所以要多一個childNodes數(shù)組來裝語句內的表達式,childNodes 可以裝任意的node,然后我們解析的時候遞歸向下解析。elseifNodes 裝elseif/else 節(jié)點,解析的時候,當if的conditon為false的時候,按順序取elseifNodes數(shù)組里的節(jié)點,誰的condition為true,就執(zhí)行誰的childNodes,然后返回結果。
// if node {type:'if',condition: '',elseifNodes: [],childNodes:[], } // elseif node {type: 'elseif',// 其實這個屬性沒用condition: '',childNodes:[] } // else node {type: 'elseif',// 其實這個屬性沒用condition: true,childNodes:[] }for節(jié)點
{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節(jié)點開始解析
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為數(shù)據(jù)變量如{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) {// 遞歸向下解析子節(jié)點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) {// 遞歸向下解析子節(jié)點result += calStatement(env, childNode);}return result;}}return result; }解析for節(jié)點
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) {// 遞歸向下解析子節(jié)點result += calStatement(statementEnv, childNode);}}return result; }結束語
目前的實現(xiàn)的jstemp功能還比較單薄,存在以下不足:
不支持模板繼承
不支持過濾器
condition表達式支持有限
錯誤提示不夠完善
單元測試,持續(xù)集成沒有完善
...
未來將一步步完善,另外無恥求個star
github地址
總結
以上是生活随笔為你收集整理的不用正则表达式,用javascript从零写一个模板引擎(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c++-add two numbers
- 下一篇: sass、gulp应用