新聞中心
前言
不知不覺就很長時間沒造過什么輪子了,以前一直想自己實現一個模板引擎,只是沒付諸于行動,最近終于在業(yè)余時間里抽了點時間寫了一下。因為我們的項目大部分用的是 swig 或者 nunjucks ,于是就想實現一個類似的模板引擎。

創(chuàng)新互聯專注于企業(yè)營銷型網站建設、網站重做改版、興海網站定制設計、自適應品牌網站建設、H5建站、電子商務商城網站建設、集團公司官網建設、外貿網站建設、高端網站制作、響應式網頁設計等建站業(yè)務,價格優(yōu)惠性價比高,為興海等各大城市提供網站開發(fā)制作服務。
至于為什么要做這么一個東西?基本上每一個做前端的人都會有自己的一個框架夢,而一個成熟的前端框架,模板編譯能力就是其中很重要的一環(huán),雖然目前市面上的大部分框架 vue、angular 這些都是屬于 dom base 的,而 swig nunjucks ejs這些都是屬于 string base 的,但是其實實現起來都是差不多的。不外乎都是 Template =parse=> Ast =render=>String。
再者,做一個模板引擎,個人感覺還是對自身的編碼能力的提升還是很有幫助的,在性能優(yōu)化、正則、字符解析上尤為明顯。在日后的業(yè)務需求中,如果有一些需要解析字符串相關的需求,也會更得心應手。
功能分析
一個模板引擎,在我看來,就是由兩塊核心功能組成,一個是用來將模板語言解析為 ast(抽象語法樹)。還有一個就是將 ast 再編譯成 html。
先說明一下 ast 是什么,已知的可以忽略。
抽象語法樹(abstract syntax tree或者縮寫為AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式,這里特指編程語言的源代碼。樹上的每個節(jié)點都表示源代碼中的一種結構。之所以說語法是“抽象”的,是因為這里的語法并不會表示出真實語法中出現的每個細節(jié)。比如,嵌套括號被隱含在樹的結構中,并沒有以節(jié)點的形式呈現;而類似于if-condition-then這樣的條件跳轉語句,可以使用帶有兩個分支的節(jié)點來表示。
在實現具體邏輯之前,先決定要實現哪幾種 tag 的功能,在我看來,for,if else,set,raw還有就是基本的變量輸出,有了這幾種,模板引擎基本上也就夠用了。除了 tag,還有就是 filter 功能也是必須的。
構建 AST
我們需要把模板語言解析成一個又一個的語法節(jié)點,比如下面這段模板語言:
{% if test > 1 %} {{ test }} {% endif %}
很明顯,div 將會被解析為一個文本節(jié)點,然后接著是一個塊級節(jié)點 if ,然后 if 節(jié)點下又有一個變量子節(jié)點,再之后有是一個 的文本節(jié)點,用 json 來表示這個模板解析成的 ast 就可以表示為:
[
{
type: 1,
text: ''
},
{
type: 2,
tag: 'if',
item: 'test > 1',
children: [{
type: 3,
item: 'test'
}]
},
{
type: 1,
text: ''
}
]基本上就分成三種類型了,一種是普通文本節(jié)點,一種是塊級節(jié)點,一種是變量節(jié)點。那么實現的話,就只需要找到各個節(jié)點的文本,并且抽象成對象即可。一般來說找節(jié)點都是根據模板語法來找,比如上面的塊級節(jié)點以及變量節(jié)點的開始肯定是{%或者{{,那么就可以從這兩個關鍵字符下手:
...
const matches = str.match(/{{|{%/);
const isBlock = matches[0] === '{%';
const endIndex = matches.index;
...通過上面一段代碼,就可以獲取到處于文本最前面的{{或者{%位置了。
既然獲取到了***個非文本類節(jié)點的位置,那么該節(jié)點位置以前的,就都是文本節(jié)點了,因此就已經可以得到***個節(jié)點,也就是上面的 獲取到 div 文本節(jié)點后,我們也可以知道獲取到的***個關鍵字符是 而此時我們就可以知道匹配到的當前關鍵字符是 獲取到 因為 if 是個塊級節(jié)點,那么繼續(xù)往下匹配的時候,在遇到 緊接著再重復上面的操作,獲取下一個 創(chuàng)建完變量節(jié)點后繼續(xù)重復上述操作,就能夠獲取到 相對比較完整的實現如下: 當然,具體實現起來還是有其他東西要考慮的,比如一個文本是 創(chuàng)建好 ast 后,要渲染 html 的時候,就只需要遍歷語法樹,根據節(jié)點類型做出不同的處理即可。 比如,如果是文本節(jié)點,就直接 封裝后具體實現如下: 使用 with ,可以讓在 function 中執(zhí)行的語句關聯對象,比如 雖然 with 不推薦在編寫代碼的時候使用,因為會讓 js 引擎無法對代碼進行優(yōu)化,但是卻很適合用來做這種模板編譯,會方便很多。包括 vue 中的 render function 也是用 with 包裹起來的。不過 nunjucks 是沒有用 with 的,它是自己來解析表達式的,因此在 nunjucks 的模板語法中,需要遵循它的規(guī)范,比如最簡單的條件表達式,如果用 with 的話,直接寫 anyway,各有各的好吧。 在將 ast 轉換成 html 的時候,有一個很常見的場景就是多級作用域,比如在一個 for 循環(huán)中再嵌套一個 for 循環(huán)。而如何在做這個作用域分割,其實也是很簡單,就是通過遞歸。 比如我的對一個 ast 樹的處理方法命名為: 那么 processAst 就可以這么實現: 就簡單通過一個遞歸,就可以把作用域一直傳遞下去了。 實現上面功能后,組件就已經具備基本的模板渲染能力,不過在用模板引擎的時候,還有一個很常用的功能就是 filter 。一般來說 filter 的使用方式都是這這樣 還是舉個例子: 在構建 AST 的時候,就可以獲取到其中的 不過后來又覺得為了性能考慮,能夠在 AST 階段就能做完的工作就不要放到渲染階段了。因此就改成 vue 的方法組合方式。也就是把上面字符串變成: 預先用個方法包裹起來,在渲染的時候,就不需要再通過循環(huán)去獲取 filter 并且執(zhí)行了。具體實現如下: 上面還有一個就是對 safe 的處理,如果有 safe 這個 filter ,就不做 escape 了。完成這個之后,有 filter 的 variable 都會變成 其實也是很簡單,就是在 new Function 的時候,多傳入一個獲取 filter 的方法即可,然后有 filter 的 variable 就能被正常識別解析了。 至此,AST 構建、AST 到 html 的轉換、多級作用域以及 Filter 的實現,都已經基本講解完成。 貼一下自己實現的一個模板引擎輪子:https://github.com/whxaxes/mus 算是實現了大部分模板引擎該有的功能,歡迎各路豪杰 star 。{%,也就是上面的endIndex是我們要的索引,記得要更新剩余的字符,直接通過 slice 更新即可:// 2 是 {% 的長度
str = str.slice(endIndex + 2);{%,那么他的閉合處就肯定是%},因此就可以再通過const expression = str.slice(0, str.indexOf('%}'))if test > 1 這個字符串了。然后我們再通過正則/^if\s+([\s\S]+)$/匹配,就可以知道這個字符串是 if 的標簽,同時可以獲得test > 1這一個捕獲組,然后就可以創(chuàng)建我們的第二個節(jié)點,if 的塊級節(jié)點了。{% endif %} 之前的所有節(jié)點,都是屬于 if 節(jié)點的子節(jié)點,所以我們在創(chuàng)建節(jié)點時要給它一個children數組屬性,用來保存子節(jié)點。{%以及{{的位置,跟上面的邏輯差不多,獲取到{{的位置后再判斷}}的位置,就可以創(chuàng)建第三個節(jié)點,test 的變量節(jié)點,并且 push 到 if 節(jié)點的子節(jié)點列表中。{% endif %}這個閉合節(jié)點,當遇到該節(jié)點之后的節(jié)點,就不能保存到 if 節(jié)點的子節(jié)點列表中了。緊接著就又是一個文本節(jié)點。const root = [];
let parent;
function parse(str){
const matches = str.match(/{{|{%/);
const isBlock = matches[0] === '{%';
const endIndex = matches.index;
const chars = str.slice(0, matches ? endIndex : str.length);
if(chars.length) {
...創(chuàng)建文本節(jié)點
}
if(!matches) return;
str = str.slice(endIndex + 2);
const leftStart = matches[0];
const rightEnd = isBlock ? '%}' : '}}';
const rightEndIndex = str.indexOf(rightEnd);
const expression = str.slice(0, rightEndIndex)
if(isBlock) {
...創(chuàng)建塊級節(jié)點 el
parent = el;
} else {
...創(chuàng)建變量節(jié)點 el
}
(parent ? parent.children : root).push(el);
parse(str.slice(rightEndIndex + 2));
}{% {{ test }},就要考慮到{%的干擾等。還有比如 else 還有 elseif 節(jié)點的處理,這兩個是需要關聯到 if 標簽上的,這個也是需要特殊處理的。不過大概邏輯基本上就是以上。組合 html
html += el.text即可。如果是if節(jié)點,則判斷表達式,比如上面的test > 1,有兩種辦法可以實現表達式的計算,一種就是eval,還有一種就是new Function了,eval 會有安全性問題,因此就不考慮了,而是使用new Function的方式來實現。變量節(jié)點的計算也一樣,用new Function來實現。function computedExpression(obj, expression) {
const methodBody = `return (${expression})`;
const funcString = obj ? `with(__obj__){ ${methodBody} }` : methodBody;
const func = new Function('__obj__', funcString);
try {
let result = func(obj);
return (result === undefined || result === null) ? '' : result;
} catch (e) {
return '';
}
}with({ a: '123' }) {
console.log(a); // 123
}{{ test ? 'good' : 'bad' }},但是在 nunjucks 中卻要寫成?{{ 'good' if test else 'bad' }}。實現多級作用域
processAst(ast, scope),再比如最初的 scope 是
{
list: [
{ subs: [1, 2, 3] },
{ subs: [4, 5, 6] }
]
}function processAst(ast, scope) {
...
if(ast.for) {
const list = scope[ast.item]; // ast.item 自然就是列表的 key ,比如上面的 list
list.forEach(item => {
processAst(ast.children, Object.assign({}, scope, {
[ast.key]: item, // ast.key 則是 for key in list 中的 key
}))
})
}
...
}Filter 功能實現
{{ test | filter1 | filter2 }},這個的實現也說一下,這一塊的實現我參考了 vue 的解析的方式,還是蠻有意思的。
{{ test | filter1 | filter2 }}test | filter1 | filter2,然后我們可以很簡單的就獲取到 filter1 和 filter2 這兩個字符串。起初我的實現方式,是把這些 filter 字符串扔進 ast 節(jié)點的 filters 數組中,在渲染的時候再一個一個拿出來處理。
_$f('filter2', _$f('filter1', test))const filterRE = /(?:\|\s*\w+\s*)+$/;
const filterSplitRE = /\s*\|\s*/;
function processFilter(expr, escape) {
let result = expr;
const matches = expr.match(filterRE);
if (matches) {
const arr = matches[0].trim().split(filterSplitRE);
result = expr.slice(0, matches.index);
// add filter method wrapping
utils.forEach(arr, name => {
if (!name) {
return;
}
// do not escape if has safe filter
if (name === 'safe') {
escape = false;
return;
}
result = `_$f('${name}', ${result})`;
});
}
return escape ? `_$f('escape', ${result})` : result;
}_$f('filter2', _$f('filter1', test))這種形式了。因此,此前的 computedExpression 方法也要做一些改造了。function processFilter(filterName, str) {
const filter = filters[filterName] || globalFilters[filterName];
if (!filter) {
throw new Error(`unknown filter ${filterName}`);
}
return filter(str);
}
function computedExpression(obj, expression) {
const methodBody = `return (${expression})`;
const funcString = obj ? `with(_$o){ ${methodBody} }` : methodBody;
const func = new Function('_$o', '_$f', funcString);
try {
const result = func(obj, processFilter);
return (result === undefined || result === null) ? '' : result;
} catch (e) {
// only catch the not defined error
if (e.message.indexOf('is not defined') >= 0) {
return '';
} else {
throw e;
}
}
}
名稱欄目:說說如何用JavaScript實現一個模板引擎
文章網址:http://www.dlmjj.cn/article/cdsgpgc.html


咨詢
建站咨詢
