摘要:前言模板引擎的作用就是將模板渲染成,,常見的模板引擎有等。網上一些制作模板引擎的文章大部分是用正則表達式做一些工作,看完能收獲的東西很少。本文將使用編譯原理那套理論來打造自己的模板引擎。最后因為考慮到空格和等情況,狀態機又復雜了許多。
前言
模板引擎的作用就是將模板渲染成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
自己寫有窮狀態自動機進行解析
作者本著自虐的心理,采取了第三種方法。
舉例說明有窮狀態自動機,解析
{{value}}
的過程Init 狀態
遇到<,轉Char狀態
直到遇到{轉化為LeftBrace,返回一個token
再遇{轉Variable狀態,返回一個token
解析value,直到}},再返回一個token
}}后再轉狀態,再返回token,轉init狀態
結果是{type:Character,value:"
"},{type:Variable},{type:VariableName, valueName: "value"},{type:EndTag},{type:Character,value:"
"}這五個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 = `how to
{%for num : list %} let say{{num.num}} {%endfor%} {%if obj%} {{obj.test}} {%else%} hello world {%endif%} `; // AST樹為 let rootNode = { childNode:[ { type:"char", value: "how to
" }, { 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地址
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/89674.html
摘要:轉換成為模板函數聯系上一篇文章,其實模板函數的構造都大同小異,基本是都是通過拼接函數字符串,然后通過對象轉換成一個函數,變成一個函數之后,只要傳入對應的數據,函數就會返回一個模板數據渲染好的字符串。 教程目錄1.手把手教你從零寫一個簡單的 VUE2.手把手教你從零寫一個簡單的 VUE--模板篇 Hello,我又回來了,上一次的文章教會了大家如何書寫一個簡單 VUE,里面實現了VUE 的...
摘要:本系列是一個教程,下面貼下目錄手把手教你從零寫一個簡單的手把手教你從零寫一個簡單的模板篇今天給大家帶來的是實現一個簡單的類似一樣的前端框架,框架現在應該算是非常主流的前端數據驅動框架,今天我們來從零開始寫一個非常簡單的框架,主要是讓大家 本系列是一個教程,下面貼下目錄~1.手把手教你從零寫一個簡單的 VUE2.手把手教你從零寫一個簡單的 VUE--模板篇 今天給大家帶來的是實現一個簡單...
摘要:文章目錄前言爬取分析視頻教學成果展示福利入門到就業學習路線規劃小白快速入門爬蟲路線前言皮皮蝦一個沙雕而又有趣的憨憨少年,和大多數小伙伴們一樣喜歡聽歌游戲,當然除此之外還有寫作的興趣,,日子還很長,讓我們一起加油努力叭話 ...
閱讀 2976·2023-04-26 02:25
閱讀 2249·2023-04-25 18:05
閱讀 647·2021-09-30 09:57
閱讀 2943·2021-09-27 14:10
閱讀 1652·2019-08-30 15:44
閱讀 1004·2019-08-29 15:28
閱讀 2524·2019-08-29 14:10
閱讀 2263·2019-08-29 13:30