前言
當下前端充斥著各種各樣的開發框架:React,Vue 等等。然而大多數這些框架的設計模式是採用了以資料為核心的 MVVM 模式。MVC 的開發模式已經離我們漸行漸遠。
對於 MVVM 模式來說,最核心的部件就是一個圍繞資料的模板引擎。
模板引擎分為前端和後端
- 前端常用的模板引擎如:mustache.js,渲染是在客戶端完成的;
- 後端的模引擎,渲染就是在伺服器完成的;
今天我們就來深入研究一下客戶端模板引擎的實現!
需求分析
模板引擎分為兩個部分:
- 模板結構,用於規定渲染頁面的結構
- 資料來源,用於填充模板內的資料
由此我們就可以寫下我們的第一行程式碼:
/**
* @param {String} template
* @param {Object} data
* @returns {String}
* @description render the template with the data source
*/
function TemplateEngine(template, data) {
return;
}
複製程式碼
假如我這裡有如下模板需要渲染:
var template =
"<div>" +
"<p>Name<span><% this.name %></span></p>" +
"<p>Gender<span>" +
"<% if(this.gender === 'male') { %>" +
"Male" +
"<% } else { %> " +
"Female" +
"<% } %>" +
"</span></p>" +
"</div>";
複製程式碼
資料來源:
var data = {
name: "AJie",
gender: "male"
};
複製程式碼
需要的渲染結果:
<div>
<p>Name<span>AJie</span></p>
<p>Gender<span>Male</span></p>
</div>
複製程式碼
實現思路
我們先將資料來源和渲染結果放在一邊,先來看看我們的模板。
假若你是瀏覽器,你會將模板最終渲染成為什麼樣子呢?當你去除掉字串的引號以及<% %>之後,答案漸漸的就浮出水面了。
<div>
<p>Name<span>this.name</span></p>
<p>Gender<span>
if(this.gender === 'male') {
Male
} else {
Female
}
</span></p>
</div>"
複製程式碼
我們現在來分析一下上面這段程式碼,不難發現,只要是之前沒有包含在<% %>之中的模板字串,都進行原樣輸出了;而那些再<% %>中的模板字串則變成了可執行的 JavaScript 邏輯程式碼。
因此我們可以先定義一個空陣列,用於儲存 JavaScript 邏輯程式碼:
var r = [];
r.push("<div><p>Name<span>");
r.push(this.name);
r.push("</span></p><p>Gender<span>");
if (this.gender === "male") {
r.push(" Male ");
} else {
r.push(" Female ");
}
r.push("</span></p></div>");
return r.join("");
複製程式碼
對於原樣輸出的字串,我們先將它們封裝在 push() 的程式碼之中,變為 JavaScript 程式碼,而對於之前的邏輯程式碼則予以保留。
這麼一來,上面的程式碼就更像是一個函式的函式體了,而呼叫者正是我們的 data 資料來源。其返回值則是我們最終需要的結果——渲染好的字串!
由此我們可以逐步完善之前的程式碼:
function TemplateEngine(template, data) {
var code = [];
...
return new Function(code).apply(data);
}
複製程式碼
值得注意:
-
new Function ([arg1[, arg2[, ...argN]],] functionBody)
函式的引數首先出現,而函式體在最後。所有引數都寫成字串形式。
-
apply(thisobj, args)
如果不瞭解 apply(),還可以檢視我之前寫的文章:《Function.call()的需求分析》
到了這一步的時候,思路漸漸的明瞭了起來,我們只需要在函式內部寫出一個生成可執行 JavaScript 程式碼的字串陣列即可。
這時候我們就應該想怎麼去處理函式內的 template 形參。
有了之前的那些思路鋪墊之後,我們不難發現這是一個字串檢索匹配的過程:
- 初始化 code = "var r=[];\n"
- 我們先檢索模板字串,依據<% %>將模板字串劃分為幾段
- 將每段按先後順序 push() 進 code 陣列之中
- 對於沒有被<% %>包裹的模板字串,我們直接將其放在'r.push("' + ... + '");\n'之中,再放進 code 陣列。
- 對於被<% %>包裹的模板字串,需要判斷書語句還是屬性:
- 如果是屬性,就放在"r.push(" + 變數 + ")"之中,再放進 code 陣列。
- 如果是語句,就直接放入 code 陣列之中。
程式碼實現
結果前面的分析,對於該模板引擎的設計大致已經 OKay 了!
藉助於正則做模式字串匹配進行功能實現:
/**
* @param {String} template
* @param {Object} data
* @returns {String}
* @description render the template with the data source
*/
function TemplateEngine(template, data) {
var re = /<%([^%>]+)?%>/g,
reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g, //用於匹配語句標識
code = "var r=[];\n",
cursor = 0,
match;
var add = function(line, js) {
js
? (code += line.match(reExp) ? line + "\n" : "r.push(" + line + ");\n")
: (code +=
line != "" ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : "");
return add;
};
while ((match = re.exec(template))) {
add(template.slice(cursor, match.index))(match[1], true);
cursor = match.index + match[0].length;
}
add(template.substr(cursor, template.length - cursor));
code += 'return r.join("");';
return new Function(code.replace(/[\r\t\n]/g, "")).apply(data);
}
複製程式碼
第一個正則會匹配<%%>
然後把<%
和%>
之間的內容儲存下來,第二個正則正好會處理第一個正則儲存下來的內容。在<%
的後面和%>
的前面可以沒有空格,也可以有一個空格,比如<%name%>
和<% name %>
應該被認為是一樣的,所以為了滿足這個需求,前面需要新增一個( )?
,( )
表示匹配一個空格,?
表示前面的重複 0 到 1 次,所以( )?
的意思就是說可以有一個空格,也可以沒有。
在while
迴圈中,首先用第一個re
去匹配(match = re.exec(tpl)
),然後<%
和%>
之間的內容被儲存在match[1]
中,然後用re2
去匹配(re2.test(match[1])
)。
注意,re
把<%
和%>
之間的內容全部放在match[1]
中了,所以如果是<%name%>
那麼match[1]
中的就是"name"
,但是如果是<% name %>
那麼match[1]
中的就是" name "
,所以需要使用( )?
來處理一下空格。
測試
測試程式碼如下:
console.log(TemplateEngine(template, data));
複製程式碼
控制檯列印輸出:
<div>
<p>Name<span>AJie</span></p>
<p>Gender<span>Male</span></p>
</div>
複製程式碼
總結
我們始終要記住 MVVM 模式是以資料為驅動的。
所謂模板引擎,簡單地說,就是依據頁面結構模板和資料來源,渲染出真正的頁面結構的功能函式。
-EFO-
筆者專門在 github 上建立了一個倉庫,用於記錄平時學習全棧開發中的技巧、難點、易錯點,歡迎大家點選下方連結瀏覽。如果覺得還不錯,就請給個小星星吧!?
2019/04/25