簡單模闆模式
簡單模闆模式是通過格式化字元串拼接出視圖避免建立視圖時大量的節點操作,簡單模闆模式不屬于一般定義的
23
種設計模式的範疇,而通常将其看作廣義上的技巧型設計模式。
描述
對比于模闆方法模式,其定義了如何執行某些算法的架構,通過父類公開的接口或方法子類去實作或者是調用,而簡單模闆模式是用來解決為了建立視圖的大量節點操作,并在此基礎上解決資料與結構的強耦合性。
節點操作
如果我們要生成一個清單,直接通過各類節點操作是相對比較麻煩的。
<!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) => 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