模板原理
模板的誕生是為了將顯示與數據分離,模板技術多種多樣,但其本質是將模板文件和數據通過模板引擎生成最終的HTML代碼。
模板技術並不是什么神秘技術,干的是拼接字符串的體力活。模板引擎就是利用正則表達式識別模板標識,並利用數據替換其中的標識符。比如:
Hello, <%= name%>
數據是{name: '木的樹'}
,那么通過模板引擎解析后,我們希望得到Hello, 木的樹
。模板的前半部分是普通字符串,后半部分是模板標識,我們需要將其中的標識符替換為表達式。模板的渲染過程如下:
//字符串替換的思想
function tmpl(str, obj) {
if (typeof str === 'string') {
return str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
var key = arguments[1];
return obj[key];
});
}
}
var str = "Hello, <%= name%>";
var obj = {name: "Lzz"};
模板引擎
引擎核心
上面我們演示是簡單的字符串替換,但對於模板引擎來說,要做的事情更復雜些。通常需要以下幾個步驟:
- 利用正則表達式分解出普通字符串和模板標識符,
<%=%>
的正則表達式為/<%=\s*([^%>]+)\s*%>/g
. - 將模板標識符轉換成普通的語言表達式
- 生成待執行語句
- 將數據填入執行,生成最終的字符串
Demo代碼如下:
//編譯的思想
function tmpl(str, obj) {
if (typeof str === 'string') {
var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
var key = arguments[1];
return "' + obj." + key; // 在函數字符串中利用'包裹正常字符串
});
tm = "return '" + tm; //"'Hello' + obj.name"
var compile = new Function('obj', tm);
return compile(obj);
}
}
var str = "Hello, <%= name%>";
var obj = {name: "Lzz"}; // Hello, Lzz
模板編譯
上述代碼中有如下部分:
tm = "return '" + tm; //"'Hello' + obj.name"
var compile = new Function('obj', tm);
為了能夠與數據一起執行生成字符串,我們需要將原始的模板字符串轉換成一個函數對象。這個過程稱為模板編譯
。模板編譯使用了new Function()
, 這里通過它創建了一個函數對象,語法如下:
new Function(arg1, arg2,..., functionbody)
Function()
構造函數接受多個參數,最后一個參數作為函數體的內容,其之前的參數全部作為生成的新函數的參數。需要注意的是Function的參數全部是字符串類型,函數體部分對於字符串跟函數表達式一定要區分清楚,初學者往往在對函數體字符串中的普通字符串和表達式的拼接上犯錯。一定要將函數體字符串和內部字符串正確拼接,如:
new Function('obj', "return 'Hello,' + obj.name")
或者對其中的字符換使用\"
new Function('obj', 'strip', "var tmp = \"\"; with(obj){ tmp = '';for(var i = 0; i < 3; i++){ tmp+='name is ' + strip(name) +' ';} tmp+=''; } return tmp;")
模板編譯過程中每次都要利用Function重新生成一個函數,浪費CPU。為此我們可以將函數緩存起來,代碼如下:
//模板預編譯
var tmpl = (function(){
var cache = {};
return function(str, obj){
if (!typeof str === 'string') {
return;
}
var compile = cache[str];
if (!cache[str]) {
var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
var key = arguments[1];
return "' + obj." + key;
});
tm = "return '" + tm; //"'Hello' + obj.name"
compile = new Function('obj', tm);
cache[str] = compile;
}
return compile(obj); //預編譯情況下應該返回compile函數
}
}());
var str = "Hello, <%= name%>";
var obj = {name: "Lzz"};
tmpl(str, obj);
利用with
利用with
我們可以不用把模板標識符轉換成obj.name
,只需要保持name
標識符即可。
// 利用with使得變量自己尋找對象, 找不到的視為普通字符串
// 貌似return后面不能直接跟with
//模板預編譯
var tmpl = (function(){
var cache = {};
return function(str, obj){
if (!typeof str === 'string') {
return;
}
var compile = cache[str];
if (!cache[str]) {
var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
var key = arguments[1];
return "' + " + key;
});
tm = "var tmp = \"\"; with(obj){ tmp = '" + tm + "; } return tmp;"; //"'Hello' + obj.name"
compile = new Function('obj', tm);
cache[str] = compile;
}
return compile(obj); //預編譯情況下應該返回compile函數
}
}());
var str = "Hello, <%= name%>";
var obj = {name: "LZZ"};
tmpl(str, obj);
XSS漏洞
如果上面的obj變成var obj = {name: "<script>alert(\"XSS\")</script>"};
,那么最終生成的結果就會變成:
"Hello, <script>alert("XSS")</script>"
為此我們需要堵上這個漏洞,基本就是要將形成HTML標簽的字符轉換成安全的字符,這些字符通常是&
, <
, >
, "
, '
。轉換函數如下:
var strip = function(html) {
return String(html)
.replace(/&/g, '&')//&
.replace(/</g, '<')//左尖號
.replace(/>/g, '>')//右尖號
.replace(/"/g, '"')//雙引號"
.replace(/'/g, ''');//IE下不支持''
}
這樣下來,模板引擎應該變成這樣:
var tmpl = (function(){
var cache = {};
var strip = function(html) {
return String(html)
.replace(/&/g, '&')//&
.replace(/</g, '<')//左尖號
.replace(/>/g, '>')//右尖號
.replace(/"/g, '"')//雙引號"
.replace(/'/g, ''');//IE下不支持''
}
return function(str, obj){
if (!typeof str === 'string') {
return;
}
var compile = cache[str];
if (!cache[str]) {
//var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
// var key = arguments[1];
// return "' + strip(" + key + ")";
//});
var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
var code = arguments[1];
return "' + strip(" + code + ")"; //利用escape包裹code
}).replace(/<%=\s*([^%>]+)\s*%>/g, function() {
var key = arguments[1];
return "' + " + key;
});
tm = "var tmp = \"\"; with(obj){ tmp = '" + tm + "; } return tmp;"; //"'Hello' + obj.name"
compile = new Function('obj', 'strip', tm);
cache[str] = compile;
}
return compile(obj, strip); //預編譯情況下應該返回compile函數
}
}());
var str = "<%= name%>";
var obj = {name: "<script>alert(\"XSS\")</script>"};
tmpl(str, obj);
這時候我們得到如下結果:
"<script>alert("XSS")</script>"
模板邏輯
功能稍微強大的模板引擎,都允許在模板中添加一部分邏輯來控制頁面的最終渲染。如:
var str = "<%for(var i = 0; i < 3; i++){%>name is <%= name%> <%}%>";
這里我們用<%%>
代表邏輯代碼<%=%>
代表模板中需要替換的標識符。我們的模板代碼變成了如下所示:
//模板邏輯
var tmpl = (function(){
var cache = {};
var strip = function(html) {
return String(html)
.replace(/&/g, '&')//&
.replace(/</g, '<')//左尖號
.replace(/>/g, '>')//右尖號
.replace(/"/g, '"')//雙引號"
.replace(/'/g, ''');//IE下不支持''
}
return function(str, obj){debugger;
if (!typeof str === 'string') {
return;
}
var compile = cache[str];
if (!cache[str]) {
//var tm = str.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
// var key = arguments[1];
// return "' + strip(" + key + ")";
//});
var tm = str.replace(/<%\s*([^=][^%>]*)\s*%>/g, function() {
var key = arguments[1];
return "';" + key + " tmp+='"; // 邏輯代碼需要一塊塊的拼接起來,為的是拼接成一段合理的函數字符串傳遞給new Function
}).replace(/<%=\s*([^%>]+)\s*%>/g, function() {
var code = arguments[1];
return "' + strip(" + code + ") +'"; //利用escape包裹code ,加入模板邏輯時要注意,保證拼接成正確的函數字符串
}).replace(/<%=\s*([^%>]+)\s*%>/g, function() {
var key = arguments[1];
return "' + " + key + "+ '";//加入模板邏輯時要注意,保證拼接成正確的函數字符串
});debugger;
tm = "var tmp = \"\"; with(obj){ tmp = '" + tm + "'; } return tmp;"; //"'Hello' + obj.name"
compile = new Function('obj', 'strip', tm);
cache[str] = compile;
}
return compile(obj, strip); //預編譯情況下應該返回compile函數
}
}());
var str = "<%for(var i = 0; i < 3; i++){%>name is <%= name%> <%}%>";
var obj = {name: "<script>alert(\"XSS\")</script>"};
tmpl(str, obj);
第一步,我們將模板中的邏輯表達式找出來,用的正則表達式是/<%\s*([^=][^%>]*)\s*%>/g
str.replace(/<%\s*([^=][^%>]*)\s*%>/g, function() {
var key = arguments[1];
return "';" + key + " tmp+='"; // 邏輯代碼需要一塊塊的拼接起來,為的是拼接成一段合理的函數字符串傳遞給new Function
})
注意在拼接時,為了防止函數字符串中的字符串沒有閉合對表達式造成影響,我們在key
前后都加了'
來保證其中的字符串閉合
。
第二步, 對可能存在的HTML標簽進行轉義
.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
var code = arguments[1];
return "' + strip(" + code + ") +'"; //利用escape包裹code ,加入模板邏輯時要注意,保證拼接成正確的函數字符串
})
同樣需要注意前后的字符串閉合
第三步,像先前一樣處理模板標識符
.replace(/<%=\s*([^%>]+)\s*%>/g, function() {
var key = arguments[1];
return "' + " + key + "+ '";//加入模板邏輯時要注意,保證拼接成正確的函數字符串
})
仍然要注意其中的字符串閉合問題
。
模板引擎是一個系統的問題,復雜模板還支持模板嵌套
,這里就不介紹了,希望此文能夠拋磚引玉,讓大火帶來更好的干貨!