模板的工作原理可以簡單地分成兩個步驟:模板解析(翻譯)和資料渲染。這兩個步驟可分別部署在前端或後端來執行。如果放在後端執行,則是像Smarty,FreeMarker這樣的後端模板引擎,而如果放在前端來執行,則是我們要探討的前端模板。
FreeMarker是一個模板引擎,一個基於模板生成文字輸出的通用工具,使用純Java編寫,模板用servlet提供的資料動態地生成 HTML,模板語言是強大的直觀的,編譯器速度快,輸出接近靜態HTML頁面的速度。這裡不再對後端模版進行描述。
前端模版提高了前端開發的可維護性(後期改起來方便)以及可擴充套件性(想要增加功能,增加需求方便);提高了開發效率提高(程式邏輯組織更好,除錯方便);最重要的一點就是:【檢視(包括展示渲染邏輯)與程式邏輯的分離】。好處是減輕伺服器負擔,壞處是可能不利於seo以及模版錯誤不好除錯。
當今前端模版主要有三類:
1 2 3 |
-String-based 模板技術 (基於字串的parse和compile過程) -Dom-based 模板技術 (基於Dom的link或compile過程) -雜交的Living templating 技術 (基於字串的parse 和 基於dom的compile過程)。 |
一.前端模版的演變
傳統的前端開發方式是通過通過ajax獲取資料進行繁瑣的資料渲染。隨著前端頁面的互動越來越繁雜,頁面無重新整理的傳輸與頁面的顯然也越發的頻繁,導致頁面效能低下。即當前端從後臺通過ajax等方式獲取到資料更新後,都需要將這個資料渲染到指定的dom元素中,需要重新進行各種字串拼接工作或者一系列建立元素的工作,這種方式是繁瑣且費時的。這種在可讀性和維護性上也存在問題。
基於字串的模板引擎最大的功勞就是把你從大量的夾帶邏輯的字串拼接中解放出來了,由於它的完全基於字串的特性,它擁有一些無可替代的優勢。如下的字串拼接:
Dom-based的模板技術中,如果你需要從一段字串建立出一個view,你必然通過innerHTML來獲得初始Dom結構. 然後引擎會利用Dom API(attributes, getAttribute, firstChild… etc)層級的從這個原始Dom的屬性中提取指令、事件等資訊,Dom-based的模板技術並沒有完整的parse的過程。繼而完成資料與View的繫結,使其”活動化”。所以Dom-based的模板技術更像是一個資料與dom之間的“連結”和*“改寫”*過程。 完成compile之後,data與View仍然保持聯絡,即你可以不依賴與手動操作Dom API來更新View。
String-based 和 Dom-based的模板技術都或多或少的依賴與innerHTML, 它們的區別是一個是主要是為了Rendering 一個是為了 Parsing 提取資訊。Living Template Engine模版引擎的解析過程類似於String-based 模板技術 和 compile過程類似於Dom-based模板技術。
二.String-based 模板技術 (基於字串的parse和compile過程)
抽象語法樹(Abstract Syntax Tree)也稱為AST語法樹,指的是原始碼語法所對應的樹狀結構。也就是說,對於一種具體程式語言下的原始碼,通過構建語法樹的形式將原始碼中的語句對映到樹中的每一個節點上。
實現一個簡單的字串迴圈模版:
上面的例子很好的說明了String-based 模板技術的原理。它產生html結構,直接通過innerHTML插入到DOM中。
1 2 |
優點:相對於字串拼接,實現了模版和程式碼邏輯的分離,不用大量的字串拼接 缺點:render之後資料即與view完全分離,innerHTML的效能問題,安全問題等 |
三.Dom-based 模板技術 (基於Dom的link或compile過程)
先通過innerHTML來獲得初始Dom結構,然後引擎會利用Dom API(attributes, getAttribute, firstChild… etc)層級的從這個原始Dom的屬性中提取指令、事件等資訊。繼而完成資料與View的繫結,使其”活動化”。
Node物件定義了一系列屬性和方法,來方便遍歷整個文件。用parentNode屬性和childNodes[]陣列可以在文件樹中上下移動;通過遍歷childNodes[]陣列或者使用firstChild和nextSibling屬性進行迴圈操作,也可以使用 lastChild和previousSibling進行逆向迴圈操作,也可以列舉指定節點的子節點。而呼叫appendChild()、insertBefore()、removeChild()、replaceChild()方法可以改變一個節點的子節點從而改變文件樹。需要指出的是,childNodes[]的值實際上是一個NodeList物件。因此,可以通過遍歷childNodes[]陣列的每個元素,來列舉一個給定節點的所有子節點。
1 |
在 JavaScript 中也有很多樹形結構。比如 DOM 樹,省市區地址聯動,檔案目錄等; JSON 本身就是樹形結構。通過遞迴,可以列舉樹中的所有節點。 |
Dom-based 模板技術的實現過程會包含在下面Living Template Engine的敘述中。
四.Living Template Engine
String-based 和 Dom-based的模板技術都或多或少的依賴與innerHTML, 它們的區別是一個是主要是為了Rendering 一個是為了 Parsing 提取資訊。所以為什麼不結合它們兩者來完全移除對innerHTML的依賴呢?parse和compile的過程分別類似於String-based 模板技術 和 Dom-based模板技術。
先呼叫Parser()模組對字串進行解析輸出AST,這個方法模板內部將包含對模板的詞法分析、語法分析、構造輸出AST。然後呼叫this.compile(AST)方法編譯,這個方法裡調用walkers進行遞歸遍歷這個AST,最後輸出並保存這個組件的Dom,當調用這個組件的compile(AST)方法編譯,這個方法裡呼叫walkers進行遞迴遍歷這個AST,最後輸出並儲存這個元件的Dom,當呼叫這個元件的inject()方法就可以把這個Dom插入到頁面中。
1 . Parsing
首先我們使用一個內建DSL來解析模板字串並輸出AST。
1)詞法分析器又稱為掃描器,詞法分析是指將文字程式碼流解析為一個個記號,分析得到的記號以供後續的詞法分析使用。 這個模組在Regular頂級模組執行過程中呼叫Parse模組進行語法分析前會呼叫Lexer詞法分析模組對字串模板進行詞法分析。詞法分析的主要流程如下圖所示:
詞法分析主要分為兩部分進行,分別是Tag型別元素字串,還有一類是JST字串。通過全域性中全域性中儲存一個state狀態,當前解析完成後,會判斷一個字串的開頭是否以“<”字元開始,如果是則進入Tag詞法解析流程,如果不是則進入JST模板詞法解析流程。 最後通過詞法分析,將得到一個很長的陣列,這個陣列中裝著一個個上面的詞法物件,這將為之後的語法分析做下鋪墊。
2)在詞法分析模組部分,將解析詞法分析出的詞塊,然後根據Regular模板語法,拼接零散的詞塊為具體含義的語法物件,然後輸出一棵抽象語法樹AST,這是進行下一步編譯this.$compile()的輸入。 首先要定義出這個抽象語法樹每個節點物件的型別以及它含有的屬性。
一一輸入詞法分析出來的詞法塊,根據這個詞法塊的type型別的不同來執行不同的邏輯,他們的本質都是根據當前type型別去判斷,取出之後的詞法塊的一定個數,然後通過建立出特定的語法節點物件。
最終就成功得到了一棵由7種語法節點物件組成的抽象語法樹AST。這將為之後的編譯做下鋪墊。
例如,在regularjs中,下面這段簡單的模板字串
會被解析為以下這段資料結構
2.Compiler
結合特定的資料模型(在regularjs中,是一個裸資料), 模板引擎層級遊歷AST並遞迴生成Dom節點(不會涉及到innerHTML). 與此同時,指令、事件和插值等binder也同時完成了繫結,使得最終產生的Dom是與Model相維繫的,即是活動的.
通過上一節已經得到了一棵AST,這課抽象語法樹的節點是7中節點的一種,這個時候只要通過先序遍歷[17]這個AST,然後根據語法塊的type型別執行不通過的建構函式建立出Dom物件即可。
以上面的模板程式碼的一個插值為例:”{{isLogin? ‘Login’: ‘Wellcome’}}”。一旦regularjs的引擎遇到這段模板與代表的語法元素節點,會進入如下函式處理
正如我們所見, 歸功於$watch函式,一旦表示式發生改變,文字節點也會隨之改變,這一切其實與angularjs並無兩樣(事實上regularjs同樣也是基於髒檢查)
與Dom-based 模板技術利用Dom節點承載資訊所不同的是,它的中間產物AST 承載了所有Compile過程中需要的資訊(語句, 指令, 屬性…等等). 這帶來幾個好處
1 2 3 4 |
輕量級, 在Dom中進行讀寫操作是低效的. 可重用的. 可序列化 , 你可以在本地或伺服器端預處理這個過程。 安全, 因為安全不需要innerHTML幫我們生成初始Dom。 |
小結:
後端模板可以承載頁面的固定資料,如登陸的webUser,它隨著頁面的產生而產生,隨著頁面的消失而消失;前端模板主要實現複雜的頁面互動伴隨的資料變化,進行頁面無重新整理的資料更新,實現頁面的多彩化。所以前端模板和後端模板要相互結合使用,才能更好的服務於web應用。