The simple template pattern is to avoid a large number of node operations when creating views by splicing the formatted string. The simple template pattern does not belong to the category of the 23 commonly defined design patterns, but it is usually regarded as a broad-spectrum technical design pattern.
In contrast to the template method pattern, which defines the framework for how to execute certain algorithms and allows subclasses to implement or call it through the interfaces or methods exposed by the parent class, the simple template pattern is used to solve the problem of a large number of node operations required to create views, and to solve the strong coupling between data and structure based on this.
If we want to create a list, it's relatively troublesome to do it directly through various node operations.
<!DOCTYPE html>
<html>
<head>
<title>Node Operation</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>
Although using string splicing can reduce the apparent complexity, the actual maintainability is usually poor due to the strong coupling of data and structure, which means that changes to the data or structure will require code modification. In addition, the dynamic creation of a ul
list using the template string syntax of ES6
seems simple, but if you use string splicing directly, it will be much more cumbersome.
<!DOCTYPE html>
<html>
<head>
<title>String Concatenation</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>
By creating a template, we can use data to format strings to render views and insert them into containers, making the solution much more readable.
<!DOCTYPE html>
<html>
<head>
<title>Template Rendering</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>
A simple implementation is made for the mustcache
style {{}}
, only the data display aspect is implemented, and the directives such as loops are not. By processing the string, converting it into a function, and executing it with parameters, the data display can be achieved. By handling the strings and using Function
to implement the template syntax, it is entirely possible to generate a more complete template syntax filter using regular expressions, including Js expressions and built-in directives such as the laytpl
module in mustcache.js
and layui.js
.
<!DOCTYPE html>
<html>
<head>
<title>Template Engine</title>
</head>
<body>
<div id="root">
<div>{{show}}</div>
<div>{{description}}</div>
</div>
</body>
<script type="text/javascript">
var data = {
show: 1,
description: "A simple template engine"
};
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>
The template syntax based on AST
requires parsing the HTML
into an AST
, then converting the AST
into a string. This string is executed as a function, and this process still requires the use of Function
. The example below only utilizes Js
to obtain the DOM
structure-generated AST
and does not parse HTML
independently. Although it seems that using Function
to handle strings is necessary in the end, and AST
still needs to parse HTML
and then splice the string, increasing the computation time, if the template syntax is implemented solely based on processing strings, rendering is required when the data changes, and each render
needs to re-render the entire DOM
. Although the above simple implementation also re-renders the entire template through AST
, mainstream Js frameworks such as Vue
are now based on AST
. First, the template
is parsed into AST
, and then static node markings are applied to the AST
to mark the static nodes for reuse and skip comparison, thus optimizing rendering. Then a virtual DOM
is generated, and when the data changes, the virtual DOM
performs a diff
algorithm comparison to find the nodes with data changes, and then minimizes rendering. This way, the entire template does not need to be re-rendered when the data changes, thereby increasing rendering efficiency.
<!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: "A simple template syntax"
};
```javascript
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
[Link to the juejin post](https://juejin.cn/post/6844903633000087560)
[Link to the cnblogs page](https://www.cnblogs.com/libin-1/p/6544519.html)
[Link to the laytpl.js file on Github](https://github.com/sentsin/layui/blob/master/src/lay/modules/laytpl.js)