簡單模板模式是通過格式化字符串拼接出視圖避免創建視圖時大量的節點操作,簡單模板模式不屬於通常定義的設計模式範疇。
描述對比於模板方法模式,其定義了如何執行某些算法的框架,通過父類公開的接口或方法子類去實現或者是調用,而簡單模板模式是用來解決為了創建視圖的大量節點操作,並在此基礎上解決數據與結構的強耦合性。
節點操作如果我們要生成一個列表,直接通過各類節點操作是相對比較麻煩的。
<!DOCTYPE html>
<html>
<head>
<title>節點操作</title>
</head>
<body>
<div id="root"></div>
</body>
<script type="text/javascript">
(function(){
const container = document.getElementById("root");
const ul = document.createElement("ul");
const list = [{
"name": "google",
"url": "https://www.google.com"
}, {
"name": "baidu",
"url": "https://www.baidu.com"
}, {
"name": "bing",
"url": "https://cn.bing.com"
}];
list.forEach(v => {
let li = document.createElement("li");
let a = document.createElement("a");
a.href = v.url;
a.target = "_blank";
a.innerText = v.name;
li.appendChild(a);
ul.appendChild(li);
});
container.appendChild(ul);
})();
</script>
</html>
基於字符串拼接如果我們使用字符串拼接,雖然能夠減少看上去的複雜程度,但是實際由於數據和結構強耦合導致可維護性通常比較差,這導致的問題是如果數據或者結構發生變化時,都需要改變代碼。此外此處使用了ES6的模板字符串語法動態生成了一個ul列表,看上去貌似不會複雜,如果直接使用字符串拼接,會繁瑣很多。
<!DOCTYPE html>
<html>
<head>
<title>字符串拼接</title>
</head>
<body>
<div id="root"></div>
</body>
<script type="text/javascript">
(function(){
const container = document.getElementById("root");
const list = [{
"name": "google",
"url": "https://www.google.com"
}, {
"name": "baidu",
"url": "https://www.baidu.com"
}, {
"name": "bing",
"url": "https://cn.bing.com"
}];
let template = `<ul>`;
list.forEach(v => {
template += `<li>
<a href="${v.url}" target="_blank" >${v.name}</a>
</li>`;
});
template += "</ul>";
container.innerHTML = template.replace(/[\s]+/g, " ");
})();
</script>
</html>
模板渲染通過創建模板,我們可以使用數據去格式化字符串來渲染視圖並插入到容器中,這樣實現的方案可讀性會高很多。
<!DOCTYPE html>
<html>
<head>
<title>模板渲染</title>
</head>
<body>
<div id="root"></div>
</body>
<script type="text/javascript">
(function(){
const container = document.getElementById("root");
const formatString = function(str, data){
return str.replace(/\{\{(\w+)\}\}/g,
(match, key) => typeof(data[key]) === void 0 ? "" : data[key]);
}
const list = [{
"name": "google",
"url": "https://www.google.com"
}, {
"name": "baidu",
"url": "https://www.baidu.com"
}, {
"name": "bing",
"url": "https://cn.bing.com"
}];
let template = ["<ul>"];
list.forEach(v => {
template.push("<li>");
template.push(formatString('<a href="{{url}}" target="_blank" >{{name}}</a>', v));
template.push("</li>");
});
template.push("</ul>");
console.log(template)
container.innerHTML = template.join("");
})();
</script>
</html>
模板引擎的簡單實現對mustcache風格的{{}}進行簡單的實現,僅對於其數據的展示方面有實現,對於其指令例如循環等並未實現,通過處理字符串,將其轉換為一個函數並傳參執行,即可實現數據的展示。通過對於字符串的處理並使用Function實現模板語法,如果使用正則表達式進行較為完整的過濾,是完全可以生成較為完善的模板語法的處理的,包括Js的表達式以及自帶指令等,如mustcache.js、layui.js的laytpl模塊。
<!DOCTYPE html>
<html>
<head>
<title>模板引擎</title>
</head>
<body>
<div id="root">
<div>{{show}}</div>
<div>{{description}}</div>
</div>
</body>
<script type="text/javascript">
var data = {
show: 1,
description: "一個簡單的模板引擎"
};
function render(element, data) {
var originString = element.innerHTML;
var html = String(originString||'').replace(/"/g,'\\"').replace(/\s+|\r|\t|\n/g, ' ')
.replace(/\{\{(.)*?\}\}/g, function(value){
return value.replace("{{",'"+(').replace("}}",')+"');
})
html = `var targetHTML = "${html}";return targetHTML;`;
var parsedHTML = new Function(...Object.keys(data), html)(...Object.values(data));
element.innerHTML = parsedHTML;
}
render(document.getElementById("root"), data);
</script>
</html>
AST基於AST的模板語法需要解析HTML成為AST,然後將AST轉化為字符串,將字符串作為函數執行,這個過程依舊需要用到Function,下邊的例子只是藉助了Js取得DOM結構生成的AST,沒有自行解析HTML。雖然看起來最後都需要使用Function去處理字符串,而AST還需要解析HTML然後再拼接字符串,增加了計算的時間,但是如果僅僅是完全基於處理字符串的方式實現的模板語法,在數據進行變更時都需要進行render,每次render的時候都需要重新渲染整個DOM,雖然在上邊的簡單實現中AST也是重新渲染了整個模版,但是現在主流的Js框架例如Vue就是基於AST的方式,首先解析template為AST,然後對於AST進行靜態節點標記,用以標記靜態的節點進行重用跳過比對,從而進行渲染優化,然後生成虛擬DOM,當數據進行變更時虛擬DOM會進行diff算法的比對,找到數據有變更的節點,然後進行最小化渲染,這樣就不需要在數據變更時將整個模板進行渲染,從而增加了渲染的效率。
<!DOCTYPE html>
<html>
<head>
<title>AST</title>
</head>
<body>
<div id="root" class="root-node">
<div>{{show}}</div>
<div>{{description}}</div>
</div>
</body>
<script type="text/javascript">
var data = {
show: 1,
description: "一個簡單的模板語法"
};
function parseAST(root){
var node = {};
node.parent = null;
if(root.nodeName === "#text"){
node.type = "text";
node.tagName = "text";
node.content = root.textContent.replace(/\s+|\r|\t|\n/g, ' ').replace(/"/g,'\\"');
}else{
node.type = "tag";
node.tagName = root.localName;
node.children = [];
node.attr = {};
Array.prototype.forEach.call(root.attributes, item => node.attr[item.nodeName] = item.nodeValue );
}
Array.prototype.forEach.call(root.childNodes, element => {
var parsedNode = parseAST(element);
parsedNode.parent = root;
node.children.push(parsedNode);
});
return node;
}
function render(element, template, data) {
html = `var targetHTML = "${template}";return targetHTML;`;
var parsedHTML = new Function(...Object.keys(data), html)(...Object.values(data));
element.innerHTML = parsedHTML;
}
function generateHTMLTemplate(AST){
var template = "";
AST.forEach( node => {
if(node.type === "tag"){
template += `<${node.tagName}>`;
template += generateHTMLTemplate(node.children);
template += `</${node.tagName}>`;
}else{
if(node.content.match(/\{\{(.)*?\}\}/)){
var expression = node.content.replace(/\{\{(.)*?\}\}/g, function(value){
return value.replace("{{",'"+(').replace("}}",')+"');
})
template += expression;
}else{
template += node.content;
}
}
})
return template;
}
var root = document.getElementById("root");
var AST = parseAST(root);
var template = generateHTMLTemplate([AST]);
render(root, template, data);
</script>
</html>
每日一題https://github.com/WindrunnerMax/EveryDay
參考https://juejin.cn/post/6844903633000087560
https://www.cnblogs.com/libin-1/p/6544519.html
https://github.com/sentsin/layui/blob/master/src/lay/modules/laytpl.js