前面
js 模板引擎有很多很多,我以前經常用 art-template ,有時候也會拿 vue 來當模板引擎用。
直到......
年初的時候,我還在上個項目組,那時候代碼規范是未經允許不能使用 【外部代碼】,囧 。
有了需求,那么就去寫吧,但是后來因為一些原因沒用上。后來分了產線,自己搭了一套構建,用了幾個月感覺挺爽,把這小段代碼按照比較大眾的規范重寫,跟大家分享下。
https://github.com/shalldie/mini-tpl
語法
首先是選擇模板語法,ejs語法是首選,因為大眾,更無需去學習指令型模板引擎的那些東西。
如果寫過 jsp 或者 asp/asp.net 的可以直接上手。
怎么用它?
我要這么用
<body> <div id="root"></div> <script id="tplContent" type="text/html"> <ul> <% for(var i=0; i<data.length; i++){ var item = data[i]; if(item.age < 30){%> <li>我的名字是<%=item.name%>,我的年齡是<%=item.age%></li> <%}else{%> <li>my name is <%=item.name%>,my age is a sercet.</li> <%}%> <% } %> </ul> </script> <script src="../build/mini-tpl.min.js"></script> <script> var data = [{ name: 'tom', age: 12 }, { name: 'lily', age: 24 }, { name: 'lucy', age: 55 }]; var content = document.getElementById('tplContent').innerHTML; var result = miniTpl(content, data); document.getElementById('root').innerHTML = result; </script> </body>
想要這么用,那么就分析一下怎么才能實現。
new Function
1 const content = 'console.log("hello world");'; 2 3 let func = new Function(content); 4 5 func(); // hello world
new Function ([arg1[, arg2[, ...argN]],] functionBody)
functionBody 一個含有包括函數定義的JavaScript語句的字符串。
使用Function構造器生成的函數,並不會在創建它們的上下文中創建閉包;它們一般在全局作用域中被創建。
當運行這些函數的時候,它們只能訪問自己的本地變量和全局變量,不能訪問Function構造器被調用生成的上下文的作用域。(MDN)
也就是說:
- 可以用 new Function 來動態的創建一個函數,去執行某動態生成的函數定義js語句。
- 通過 new Function 生成的函數,作用域在全局。
- 那么傳參有3種:
把變量放到全局(扯淡)、函數傳參、用call/apply把值傳給函數的this。
最初我用的是 call 來傳值,如今想了想不太優雅,換成了用參數傳遞。也就是這樣:
const content = 'console.log(data);'; let func = new Function('data', content); func('hello world'); // hello world
到此為止,雛形有了。下面來拆分。
模板拆分
先看模板:
<% for(var i=0; i<data.length; i++){ var item = data[i]; if(item.age < 30){%> <li>我的名字是<%=item.name%>,我的年齡是<%=item.age%></li> <%}else{%> <li>my name is <%=item.name%>,my age is a sercet.</li> <%}%> <% } %>
js 邏輯部分,由 <%%> 包裹, js 變量的占位,由 <%= %> 包裹,剩下的是普通的要拼接的html字符串部分。
也就是說,需要用正則找出的部分有3種:
<%%>邏輯部分的js內容<%=%>占位部分的js內容- 其它的
純文本內容
其中第2項,js占位的部分,也屬於拼接文本。所以可以放在一起,就是 js部分 ,拼接部分。
正則提取
當然是選擇正則表達式啊!
這里先跟大家擴展一下關於偽數組方面的內容,以及瀏覽器的控制台如何看待偽數組:

不扯遠,直接說結論:
只要有 int類型的 length屬性,有 function類型 的 splice屬性。 那么瀏覽器就會認為他是一個數組。
如果里面的其它屬性按照索引來排序,甚至還可以像數組里面的項那樣在控制台展示出來。
這種判斷方式叫 duck typing ,如果一個東西長得像鴨子,而且叫起來像鴨子,,,那么它就是鴨子 0_o
回到正文,這個需要多次從模板中,把 js邏輯部分 和 文本 依次提取出來。
對於每一次提取,都要獲取提取出的內容,本次匹配最后的索引項(用於提起文本內容)。所以我選擇了 RegExp.prototype.exec 。

舉個例子,RegExp.prototype.exec 返回的是一個集合(偽數組),它的類型是這樣的:
| 屬性/索引 | 描述 |
|---|---|
[0] |
匹配的全部字符串 |
[1],...[n] |
括號中的分組捕獲 |
index |
匹配到的字符位於原始字符串的基於0的索引值 |
input |
原始字符串 |
通過這樣,就可以拿到匹配到的 js 邏輯部分,並通過 index 和本次匹配到的內容,來獲取每個js邏輯部分之間的文本內容項。
要注意,在全局匹配模式下,正則表達式會接着上次匹配的結果繼續匹配新的字符串。
/**
* 從原始模板中提取 文本/js 部分
*
* @param {string} content
* @returns {Array<{type:number,txt:string}>}
*/
function transform(content) {
var arr = []; //返回的數組,用於保存匹配結果
var reg = /<%(?!=)([\s\S]*?)%>/g; //用於匹配js代碼的正則
var match; //當前匹配到的match
var nowIndex = 0; //當前匹配到的索引
while (match = reg.exec(content)) {
// 保存當前匹配項之前的普通文本/占位
appendTxt(arr, content.substring(nowIndex, match.index));
//保存當前匹配項
arr.push({
type: 1, //js代碼
txt: match[1] //匹配到的內容
});
//更新當前匹配索引
nowIndex = match.index + match[0].length;
}
//保存文本尾部
appendTxt(arr, content.substr(nowIndex));
return arr;
}
/**
* 普通文本添加到數組,對換行部分進行轉義
*
* @param {Array<{type:number,txt:string}>} list
* @param {string} content
*/
function appendTxt(list, content) {
content = content.replace(/\r?\n/g, "\\n");
list.push({ txt: content });
}
...
得到了js邏輯項 和 文本內容 ,就可以把他們拼在一起,來動態生成一個function。要注意的是,文本內容中,包含 js占位項,這個地方要轉換一下。
/**
* 模板 + 數據 =》 渲染后的字符串
*
* @param {string} content 模板
* @param {any} data 數據
* @returns 渲染后的字符串
*/
function render(content, data) {
data = data || {};
var list = ['var tpl = "";'];
var codeArr = transform(content); // 代碼分割項數組
for (var i = 0, len = codeArr.length; i < len; i++) {
var item = codeArr[i]; // 當前分割項
// 如果是文本類型,或者js占位項
if (!item.type) {
var txt = 'tpl+="' +
item.txt.replace(/<%=(.*?)%>/g, function (g0, g1) {
return '"+' + g1 + '+"';
}) + '"';
list.push(txt);
}
else { // 如果是js代碼
list.push(item.txt);
}
}
list.push('return tpl;');
return new Function('data', list.join('\n'))(data);
}
這樣就完成了簡易的模板引擎,不要覺得拼字符串慢。
在現代瀏覽器(IE8開始)中,特地對字符串的操作做了大量的優化,用 += 拼字符串,要比用數組 push 再 join 的方式快很多很多,即使放到IE7(IE6不清楚)中,我這里測試也是拼字符串快。。。
最后
模板引擎這東西我搜了一下園子里面有不少,我這是炒炒冷飯。
造輪子這事偶爾為之會提高成就感,但是干什么都要自己造就很caodan了。
附上 github 地址:https://github.com/shalldie/mini-tpl
希望大家錢途無量,少加班,能寫喜歡的代碼 :D
.
