端模板是什麼?前端模板該如何實現?很多朋友可能對這個不太了解,那麼,下面這篇文章將給大家介紹一下關於前端模板的原理以及簡單的實現代碼。
前端模板的發展
模板可以說是前端開發最常接觸的工具之一。將頁面固定不變的內容抽出成模板,服務端返回的動態數據裝填到模板中預留的坑位,最後組裝成完整的頁面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)。
// 將未處理過的字符串進行分詞,形成字符組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);
};
這一步分割字符串通常使用正則來完成的,後面檢索字符串會大量用到正則方法。
在這一步通常可以檢查出模板標籤閉合異常,並報錯。
2. 模板語法的分類
生成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完成了。
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這種可以實現雙向綁定的模板可以抽空多看一看。
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/273338.html