前端模板是什麼?前端模板該如何實作?很多朋友可能對這個不太了解,那麼,下面這篇文章將給大家介紹一下關於前端模板的原理以及簡單的實作程式碼。
範本可以說是前端開發最常接觸的工具之一。將頁面固定不變的內容抽出成模板,服務端傳回的動態資料填入模板中預留的坑位,最後組裝成完整的頁面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)
然後來實作範本:
模板引擎取得到模板字串之後,通常要使用正規切割字串,區分出那些是靜態的字串,那些是需要編譯的程式碼區塊,生成抽象語法樹(AST)。
// 将未处理过的字符串进行分词,形成字符组tokens Panda.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); };
這一步驟分割字串通常使用正規則來完成的,後面檢索字串會大量用到正規方法。
在這一步驟通常可以檢查出模板標籤閉合異常,並報錯。
生成AST之後,普通字串不需要再管了,最後會直接輸出,專注於模板語法的分類。
// 专门处理模板中的js Panda.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完成了。
现在开始根据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编译器,模板引擎在查找变量的时候找不到对应变量即终止查找,返回空并不会报错。
这里开始讲模板语法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; }
页面字符串已经解析完成,可以直接输出:
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这种可以实现双向绑定的模板可以抽空多看一看。
相关推荐:
javascript - 大家前端js模板是用underscore还是handlebars呢?
学习前端模板引擎 jade (一)_html/css_WEB-ITnose
以上是前端模板是什麼?前端模板的原理介紹以及實例的詳細內容。更多資訊請關注PHP中文網其他相關文章!