前端模板是什嗎?前端模板該如何??很多朋友可能對這個不太瞭解,那麼,下面這篇文章將給大家介紹一下關於前端模板的原理以及簡單的實現代碼。
前端模板的發展
模板可以說是前端開發最常接觸的工具之一。將頁面固定不變的內容抽出成模板,服務端返回的動態資料裝填到模板中預留的坑位,最後裝配成完整的頁面html字串交給瀏覽器去解析。
模板可以大大提升開發效率,如果沒有模板開發人員怕是要手動拼字字串。
var tpl = '<p>' + user.name + '</p>';$('body').append(tpl);
在近些年前端發展過程中,模板也跟著變化:
1. php模板 JSP模板
早期還沒有前後端分離時代,前端只是後端項目中的一個檔案夾,這時期的php和java都提供了各自的模板引擎。以JSP為例:java web應用的頁面通常是一個個.jsp的檔案,這個檔案內容是大部分的html以及一些模板內建文法,本質上是純文字,但是既不是html也不是java。
JSP文法:index.jsp
<html><head><title>Hello World</title></head><body>Hello World!<br/><%out.println("Your IP address is " + request.getRemoteAddr());%></body></html>
這個時期的模板引擎,往往是服務端來編譯模板字串,產生html字串給用戶端。
2. handlebar mustache通用模板
09年node發布,JavaScript也可以來實現服務端的功能,這也大大的方便了開發人員。mustache和handlebar模板的誕生方便了前端開發人員,這兩個模板均使用JavaScript來實現,從此前端模板既可以在服務端運行,也可以在用戶端運行,但是大多數使用情境都是js根據服務端非同步擷取的資料套入模板,產生新的dom插入頁碼。 對前端後端開發都非常有利。
mustache文法:index.mustache
<p>Username: {{user.name}}</p>{{#if (user.gender === 2)}} <p>女</p>{{/if}}
3. vue中的模板 React中的JSX
接下來到了新生代,vue中的模板寫法跟之前的模板有所不同,而且功能更加強大。既可以在用戶端使用也可以在服務端使用,但是使用情境上差距非常大:頁面往往根據資料變化,模板產生的dom發生變化,這對於模板的效能要求很高。
vue文法:index.vue
<p>Username: {{user.name}}</p><template v-if="user.gender === 2"> <p>女</p></div>
模板實現的功能
無論是從JSP到vue的模板,模板在文法上越來越簡便,功能越來越豐富,但是準系統是不能少的:
變數輸出(轉義/不轉義):出於安全考慮,模板基本預設都會將變數的字串轉義輸出,當然也實現了不轉義輸出的功能,謹慎使用。
條件判斷(if else):開發中經常需要的功能。
迴圈變數:迴圈數組,產生很多重複的程式碼片段。
模板嵌套:有了模板嵌套,可以減少很多重複代碼,並且嵌套樣板集成範圍。
以上功能基本涵蓋了大多數模板的基礎功能,針對這些基礎功能就可以探究模板如何?的。
模板實現原理
正如標題所說的,模板本質上都是純文字的字串,字串是如何操作js程式的呢?
模板用法上:
var domString = template(templateString, data);
模板引擎獲得到模板字串和模板的範圍,經過編譯之後產生完整的DOM字串。
大多數模板實現原理基本一致:
模板字串首先通過各種手段剝離出一般字元串和模板文法字串產生抽象文法樹AST;然後針對模板文法片段進行編譯,期間模板變數均去引擎輸入的變數中尋找;模板文法片段產生出普通html片段,與原始一般字元串進行拼接輸出。
其實模板編譯邏輯並沒有特別複雜,至於vue這種動態綁定資料的模板有時間可以參考文末連結。
快速實現簡單的模板
現在以mustache模板為例,手動實現一個實現準系統的模板。
模板字串模板:index.txt
<!DOCTYPE html><html><head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Page Title</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" type="text/css" media="screen" href="main.css" /> <script src="main.js"></script></head><body> <h1>Panda模板編譯</h1> <h2>普通變數輸出</h2> <p>username: {{common.username}}</p> <p>escape:{{common.escape}}</p> <h2>不轉義輸出</h2> <p>unescape:{{&common.escape}}</p> <h2>列表輸出:</h2> <ul> {{#each list}} <li class="{{value}}">{{key}}</li> {{/each}} </ul> <h2>條件輸出:</h2> {{#if shouldEscape}} <p>escape{{common.escape}}</p> {{else}} <p>unescape:{{&common.escape}}</p> {{/if}}</body></html>
模板對應資料:
module.exports = { common: { username: 'Aus', escape: '<p>Aus</p>' }, shouldEscape: false, list: [ {key: 'a', value: 1}, {key: 'b', value: 2}, {key: 'c', value: 3}, {key: 'd', value: 4} ]};
模板的使用方法:
var fs = require("fs");var tpl = fs.readFileSync('./index.txt', 'utf8');var state = require('./test');var Panda = require('./panda');Panda.render(tpl, state)
然後來實現模板:
1. 正則切割字串
模板引擎擷取到模板字串之後,通常要使用正則切割字串,區分出那些是靜態字串,那些是需要編譯的代碼塊,產生抽象文法樹(AST)。
// 將未處理過的字串進行分詞,形成字元組tokensPanda.prototype.parse = function (tpl) { var tokens = []; var tplStart = 0; var tagStart = 0; var tagEnd = 0; while (tagStart >= 0) { tagStart = tpl.indexOf(openTag, tplStart); if (tagStart < 0) break; // 純文字 tokens.push(new Token('text', tpl.slice(tplStart, tagStart))); tagEnd = tpl.indexOf(closeTag, tagStart) + 2; if (tagEnd < 0) throw new Error('{{}}標籤未閉合'); // 細分js var tplValue = tpl.slice(tagStart + 2, tagEnd - 2); var token = this.classifyJs(tplValue); tokens.push(token); tplStart = tagEnd; } // 最後一段 tokens.push(new Token('text', tpl.slice(tagEnd, tpl.length))); return this.parseJs(tokens);};
這一步分割字串通常使用正則來完成的,後面檢索字串會大量用到正則方法。
在這一步通常可以檢查出模板標籤閉合異常,並報錯。
2. 模板文法的分類
產生AST之後,一般字元串不需要再管了,最後會直接輸出,專註於模板文法的分類。
// 專門處理模板中的jsPanda.prototype.parseJs = function (tokens) { var sections = []; var nestedTokens = []; var conditionsArray = []; var collector = nestedTokens; var section; var currentCondition; for (var i = 0; i < tokens.length; i++) { var token = tokens[i]; var value = token.value; var symbol = token.type; switch (symbol) { case '#': { collector.push(token); sections.push(token); if(token.action === 'each'){ collector = token.children = []; } else if (token.action === 'if') { currentCondition = value; var conditionArray; collector = conditionArray = []; token.conditions = token.conditions || conditionsArray; conditionsArray.push({ condition: currentCondition, collector: collector }); } break; } case 'else': { if(sections.length === 0 || sections[sections.length - 1].action !== 'if') { throw new Error('else 使用錯誤'); } currentCondition = value; collector = []; conditionsArray.push({ condition: currentCondition, collector: collector }); break; } case '/': { section = sections.pop(); if (section && section.action !== token.value) { throw new Error('指令標籤未閉合'); } if(sections.length > 0){ var lastSection = sections[sections.length - 1]; if(lastSection.action === 'each'){ collector = lastSection.chidlren; } else if (lastSection.action = 'if') { conditionsArray = []; collector = nestedTokens; } } else { collector = nestedTokens; } break; } default: { collector.push(token); break; } } } return nestedTokens;}
上一步我們產生了AST,這個AST在這裡就是一個分詞token數組:
[ Token {}, Token {}, Token {},]
這個token就是每一段字串,分別記錄了token的類型,動作,子token,條件token等資訊。
/** * token類表示每個分詞的標準資料結構 */function Token (type, value, action, children, conditions) { this.type = type; this.value = value; this.action = action; this.children = children; this.conditions = conditions;}
在這一步要將迴圈方法中的子token嵌套到對應的token中,以及條件渲染子token嵌套到對應token中。
這步完成之後,一個標準的帶有嵌套關係的AST完成了。
3. 變數尋找與賦值
現在開始根據token中的變數尋找到對應的值,根據相應功能產生值得字串。
/** * 解析資料結構的類 */function Context (data, parentContext) { this.data = data; this.cache = { '.': this.data }; this.parent = parentContext;}Context.prototype.push = function (data) { return new Context(data, this);}// 根據字串name找到真實的變數值Context.prototype.lookup = function lookup (name) { name = trim(name); var cache = this.cache; var value; // 查詢過緩衝 if (cache.hasOwnProperty(name)) { value = cache[name]; } else { var context = this, names, index, lookupHit = false; while (context) { // user.username if (name.indexOf('.') > 0) { value = context.data; names = name.split('.'); index = 0; while (value != null && index < names.length) { if (index === names.length - 1) { lookupHit = hasProperty(value, names[index]); } value = value[names[index++]]; } } else { value = context.data[name]; lookupHit = hasProperty(context.data, name); } if (lookupHit) { break; } context = context.parent; } cache[name] = value; } return value;}
為了提高尋找效率,採用緩衝代理,每次尋找到的變數儲存路徑方便下次快速尋找。
不同於JavaScript編譯器,模板引擎在尋找變數的時候找不到對應變數即終止尋找,返回空並不會報錯。
4. 節點的條件渲染與嵌套
這裡開始講模板文法token和一般字元串token開始統一編譯產生字串,並拼接成完整的字串。
// 根據tokens和context混合拼接字串輸出結果Panda.prototype.renderTokens = function (tokens, context) { var result = ''; var token, symbol, value; for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { value = undefined; token = tokens[i]; symbol = token.type; if (symbol === '#') value = this.renderSection(token, context); else if (symbol === '&') value = this.unescapedValue(token, context); else if (symbol === '=') value = this.escapedValue(token, context); else if (symbol === 'text') value = this.rawValue(token); if (value !== undefined) result += value; } return result;}
5. 繪製頁面
頁面字串已經解析完成,可以直接輸出:
Panda.prototype.render = function (tpl, state) { if (typeof tpl !== 'string') { return new Error('請輸入字串!'); } // 解析字串 var tokens = this.cache[tpl] ? tokens : this.parse(tpl); // 解析資料結構 var context = state instanceof Context ? state : new Context(state); // 渲染模板 return this.renderTokens(tokens, context);};
輸出頁面字串被瀏覽器解析,就出現了頁面。
以上只是簡單的模板實現,並沒有經過系統測試,僅供學習使用,源碼傳送門。成熟的模板引擎是有完整的異常處理,變數尋找解析,範圍替換,最佳化渲染,斷點調試等功能的。
總結
前端模板這塊能做的東西還很多,很多架構都是整合模板的功能,配合css,js等混合編譯產生解析好樣式和綁定成功事件的dom。
另外實現模板的方式也有很多,本文的實現方式參考了mustache源碼,模板標籤內的代碼被解析,但是是通過程式碼片段分類,變數尋找的方式來執行的,將純字串的代碼變成了被解譯器執行的代碼。
另外向vue這種可以實現雙向繫結的模板可以抽空多看一看。