不用正則表達式,用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.pswp.cn/news/540293.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/540293.shtml
英文地址,請注明出處:http://en.pswp.cn/news/540293.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

[轉載] Java靜態綁定與動態綁定

參考鏈接&#xff1a; Java中的靜態綁定與動態綁定 程序綁定的概念&#xff1a; 綁定指的是一個方法的調用與方法所在的類(方法主體)關聯起來。對java來說&#xff0c;綁定分為靜態綁定和動態綁定&#xff1b;或者叫做前期綁定和后期綁定. 靜態綁定&#xff1a; 在程序執行前方…

關于批量插入數據之我見(100萬級別的數據,mysql)

2019獨角獸企業重金招聘Python工程師標準>>> 因前段時間去面試&#xff0c;問到如何高效向數據庫插入10萬條記錄&#xff0c;之前沒處理過類似問題&#xff0c;也沒看過相關資料&#xff0c;結果沒答上來&#xff0c;今天就查了些資料&#xff0c;總結出三種方法&am…

各個OS關于查看磁盤和wwn號的方法

1,HP-UX幾個命令1&#xff09;查看型號&#xff0c;和uname -a差不多model2&#xff09;查看光纖卡信息ioscan -funC fc3) 查看掃描出的磁盤信息ioscan -fucC disk4&#xff09;查看磁盤及其對應的路徑ioscan -m dsf5) 查看劃分過來的lunioscan -m lun6) 查看磁盤大小diskinfo …

[轉載] Java是不是面向對象的程序

參考鏈接&#xff1a; 為什么Java不是純粹的面向對象語言 轉載自&#xff1a;https://blog.csdn.net/a21700790yan/article/details/80129053 Java——是否確實的 “純面向對象”&#xff1f;讓我們深入到Java的世界&#xff0c;試圖來證實它。 在我剛開始學習 Java 的前面幾…

極速理解設計模式系列:6.適配器模式(Adapter Pattern)

四個角色&#xff1a;目標抽象類角色(Target)、目標對象角色(Adapter)、源對象角色(Adaptee)、客戶端角色(Client) 目標抽象類角色&#xff08;Target)&#xff1a;定義需要實現的目標接口 目標對象角色&#xff08;Adapter)&#xff1a;調用另外一個源對象&#xff0c;并且轉換…

[轉載] Java之繼承

參考鏈接&#xff1a; Java多重繼承 Java之繼承 繼承是面向對象程序的一個基本特征&#xff0c;通過繼承可以實現父子關系&#xff0c;以及代碼的復用。通過繼承實現的類稱為子類&#xff0c;被繼承的類稱為父類&#xff0c;所有直接或間接被繼承的類都稱為父類。 Java類體…

Spark(二): 內存管理

2019獨角獸企業重金招聘Python工程師標準>>> Spark 作為一個以擅長內存計算為優勢的計算引擎&#xff0c;內存管理方案是其非常重要的模塊&#xff1b; Spark的內存可以大體歸為兩類&#xff1a;execution和storage&#xff0c;前者包括shuffles、joins、sorts和agg…

[轉載] 手把手教你整合最優雅SSM框架:SpringMVC + Spring + MyBatis

參考鏈接&#xff1a; Java繼承類的對象創建 本文發表于2016年6月&#xff0c;寫于作者學生時期。文中使用到的技術和框架可能不是當下最佳實踐&#xff0c;甚至很不“優雅”。但對于剛接觸JavaEE和Spring的同學來說&#xff0c;還是能有很多收獲的&#xff0c;大牛輕拍 我們…

多播、組播、廣播優缺點分析

2019獨角獸企業重金招聘Python工程師標準>>> 單播、多播和廣播單播”&#xff08;Unicast&#xff09;、“多播”&#xff08;Multicast&#xff09;和“廣播”&#xff08;Broadcast&#xff09;這三個術語都是用來描述網絡節點之間通訊方式的術語。那么這些術語究…

[轉載] Java重載、覆蓋與構造函數

參考鏈接&#xff1a; Java中的繼承和構造函數 /** * 拷貝構造函數---Copyf t2 new Copyf(t1);就不會在調用默認構造函數了。 * 復制clone和引用 * 重載是在同一個類&#xff08;范圍&#xff09;中&#xff0c;覆蓋是子類對父類而言。 重載不關心返回值類型。 靜態方法不能被…

LOFTERD18B542F16FF685FD684F427B4…

2019獨角獸企業重金招聘Python工程師標準>>> 驗證 轉載于:https://my.oschina.net/jinhengyu/blog/1572124

[轉載] Java獲取一個類繼承的父類或者實現的接口的泛型參數

參考鏈接&#xff1a; Java中的接口和繼承 泛型的作用就不多介紹了&#xff0c;如果你想具備架構設計能力&#xff0c;那么熟練使用泛型是必不可少的。 不多說了&#xff0c;先定義泛型父類和泛型接口&#xff1a; package cn.zhh; public class Parent<T1, T2> { …

PHP系列(一)PHP流程控制結構

while(){} do{ }while(); for( 表達式1; 表達式2;表達式3 ){ 語句或語句序列; } if(){} if(){ }elseif{} <?php $i0; while(true) { if($i>100) break; echo ".$i.<br>"; $i; } ?> <?php echo "<table border1800>"; echo &quo…

[轉載] Scala繼承與Java的區別

參考鏈接&#xff1a; Java中將final與繼承一起使用 在之前的筆記Java靜態屬性和方法的繼承問題中&#xff0c;通過具體的實驗證明&#xff0c;在子類中重寫父類的字段時并沒有覆蓋父類的字段&#xff0c;只是隱藏了父類的字段。而在scala中則不同&#xff0c;scala子類的同名…

Source Map調試壓縮后代碼

在前端開發過程中&#xff0c;無論是樣式還是腳本&#xff0c;運行時的文件可能是壓縮后的&#xff0c;那這個時候調試起來就很麻煩。 這個時候&#xff0c;可以使用Source Map文件來優化調試&#xff0c;Source Map是一個信息文件&#xff0c;里面儲存著原代碼位置信息&#x…

[轉載] Python3十大經典錯誤及解決辦法

參考鏈接&#xff1a; Python中的關鍵字2 ◆ ◆ ◆ ◆ ◆ 接觸了很多Python愛好者&#xff0c;有初學者&#xff0c;亦有轉行人。不論大家學習Python的目的是什么&#xff0c;總之&#xff0c;學習Python前期寫出來的代碼不報錯就是極好的。下面&#xff0c;嚴小樣兒為大家羅…

兩臺電腦間大量數據拷貝的快捷方法

可能大家會遇到需要將一臺電腦里的數據拷貝到另外一臺電腦&#xff0c;最常用的方法是用u盤或移動硬盤等存儲設備來拷貝&#xff0c;這樣速度慢&#xff0c;而且可能拷貝多次才能將數據拷貝完。現提供一種方法&#xff0c;就是通過windows 的文件共享來實現。通過千兆網線直接連…

[轉載] 使用 Web 標準生成 ASP.NET 2.0 Web 站點

參考鏈接&#xff1a; 使用super訪問Java祖父母的成員 Stephen WaltherSuperExpert.com 適用于&#xff1a; Microsoft ASP.NET 2.0 (Beta 2) Microsoft Visual Studio .NET 2005 Microsoft Visual Web Developer 摘要&#xff1a; Microsoft ASP.NET 2.0 具有很多有用的功能…

Office快捷鍵大全之三(Access快捷鍵下篇)

向下鍵 向某幫助主題的末尾滾動 Page Up 以較大增量向某幫助主題的開頭滾動 Page Down 以較大增量向某幫助主題的末尾滾動 Home 移到某幫助主題的開頭 End 移到某幫助主題的末尾 CtrlP 打印當前幫助主題 CtrlA 選定整個幫助主題 CtrlC 將選定項復制到"剪貼…

[轉載] 如何在Android設備之間共享Google Play應用,音樂等

參考鏈接&#xff1a; 使用super訪問Java祖父母的成員 We recently showed you how to configure your iOS devices for app and media sharing; more than a few people wrote in asking how to do the same thing with Google Play purchases. Read on as we dig into how t…