這篇文章給大家分享的是有關(guān)怎么用JavaScript實(shí)現(xiàn)一個(gè)模板引擎的內(nèi)容。小編覺(jué)得挺實(shí)用的,因此分享給大家做個(gè)參考,一起跟隨小編過(guò)來(lái)看看吧。
靖安網(wǎng)站建設(shè)公司創(chuàng)新互聯(lián),靖安網(wǎng)站設(shè)計(jì)制作,有大型網(wǎng)站制作公司豐富經(jīng)驗(yàn)。已為靖安上千家提供企業(yè)網(wǎng)站建設(shè)服務(wù)。企業(yè)網(wǎng)站搭建\外貿(mào)網(wǎng)站制作要多少錢(qián),請(qǐng)找那個(gè)售后服務(wù)好的靖安做網(wǎng)站的公司定做!
一個(gè)模板引擎,在我看來(lái),就是由兩塊核心功能組成,一個(gè)是用來(lái)將模板語(yǔ)言解析為 ast(抽象語(yǔ)法樹(shù))。還有一個(gè)就是將 ast 再編譯成 html。
先說(shuō)明一下 ast 是什么,已知的可以忽略。
抽象語(yǔ)法樹(shù)(abstract syntax tree或者縮寫(xiě)為AST),或者語(yǔ)法樹(shù)(syntax tree),是源代碼的抽象語(yǔ)法結(jié)構(gòu)的樹(shù)狀表現(xiàn)形式,這里特指編程語(yǔ)言的源代碼。樹(shù)上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。之所以說(shuō)語(yǔ)法是“抽象”的,是因?yàn)檫@里的語(yǔ)法并不會(huì)表示出真實(shí)語(yǔ)法中出現(xiàn)的每個(gè)細(xì)節(jié)。比如,嵌套括號(hào)被隱含在樹(shù)的結(jié)構(gòu)中,并沒(méi)有以節(jié)點(diǎn)的形式呈現(xiàn);而類(lèi)似于if-condition-then這樣的條件跳轉(zhuǎn)語(yǔ)句,可以使用帶有兩個(gè)分支的節(jié)點(diǎn)來(lái)表示。
在實(shí)現(xiàn)具體邏輯之前,先決定要實(shí)現(xiàn)哪幾種 tag 的功能,在我看來(lái),for
,if else
,set
,raw
還有就是基本的變量輸出,有了這幾種,模板引擎基本上也就夠用了。除了 tag,還有就是 filter 功能也是必須的。
我們需要把模板語(yǔ)言解析成一個(gè)又一個(gè)的語(yǔ)法節(jié)點(diǎn),比如下面這段模板語(yǔ)言:
{% if test > 1 %} {{ test }} {% endif %}
很明顯,div 將會(huì)被解析為一個(gè)文本節(jié)點(diǎn),然后接著是一個(gè)塊級(jí)節(jié)點(diǎn) if ,然后 if 節(jié)點(diǎn)下又有一個(gè)變量子節(jié)點(diǎn),再之后有是一個(gè) 的文本節(jié)點(diǎn),用 json 來(lái)表示這個(gè)模板解析成的 ast 就可以表示為:
[ {type: 1, text: ''}, {type: 2, tag: 'if', item: 'test > 1', children: [{ type: 3, item: 'test'}] }, {type: 1, text: ''} ]
基本上就分成三種類(lèi)型了,一種是普通文本節(jié)點(diǎn),一種是塊級(jí)節(jié)點(diǎn),一種是變量節(jié)點(diǎn)。那么實(shí)現(xiàn)的話(huà),就只需要找到各個(gè)節(jié)點(diǎn)的文本,并且抽象成對(duì)象即可。一般來(lái)說(shuō)找節(jié)點(diǎn)都是根據(jù)模板語(yǔ)法來(lái)找,比如上面的塊級(jí)節(jié)點(diǎn)以及變量節(jié)點(diǎn)的開(kāi)始肯定是{%
或者{{
,那么就可以從這兩個(gè)關(guān)鍵字符下手:
...const matches = str.match(/{{|{%/);const isBlock = matches[0] === '{%';const endIndex = matches.index; ...
通過(guò)上面一段代碼,就可以獲取到處于文本最前面的{{
或者{%
位置了。
既然獲取到了***個(gè)非文本類(lèi)節(jié)點(diǎn)的位置,那么該節(jié)點(diǎn)位置以前的,就都是文本節(jié)點(diǎn)了,因此就已經(jīng)可以得到***個(gè)節(jié)點(diǎn),也就是上面的 獲取到 div 文本節(jié)點(diǎn)后,我們也可以知道獲取到的***個(gè)關(guān)鍵字符是 而此時(shí)我們就可以知道匹配到的當(dāng)前關(guān)鍵字符是 獲取到 因?yàn)?if 是個(gè)塊級(jí)節(jié)點(diǎn),那么繼續(xù)往下匹配的時(shí)候,在遇到 緊接著再重復(fù)上面的操作,獲取下一個(gè) 創(chuàng)建完變量節(jié)點(diǎn)后繼續(xù)重復(fù)上述操作,就能夠獲取到 相對(duì)比較完整的實(shí)現(xiàn)如下: 當(dāng)然,具體實(shí)現(xiàn)起來(lái)還是有其他東西要考慮的,比如一個(gè)文本是 創(chuàng)建好 ast 后,要渲染 html 的時(shí)候,就只需要遍歷語(yǔ)法樹(shù),根據(jù)節(jié)點(diǎn)類(lèi)型做出不同的處理即可。 比如,如果是文本節(jié)點(diǎn),就直接 封裝后具體實(shí)現(xiàn)如下: 使用 with ,可以讓在 function 中執(zhí)行的語(yǔ)句關(guān)聯(lián)對(duì)象,比如 雖然 with 不推薦在編寫(xiě)代碼的時(shí)候使用,因?yàn)闀?huì)讓 js 引擎無(wú)法對(duì)代碼進(jìn)行優(yōu)化,但是卻很適合用來(lái)做這種模板編譯,會(huì)方便很多。包括 vue 中的 render function 也是用 with 包裹起來(lái)的。不過(guò) nunjucks 是沒(méi)有用 with 的,它是自己來(lái)解析表達(dá)式的,因此在 nunjucks 的模板語(yǔ)法中,需要遵循它的規(guī)范,比如最簡(jiǎn)單的條件表達(dá)式,如果用 with 的話(huà),直接寫(xiě) anyway,各有各的好吧。 在將 ast 轉(zhuǎn)換成 html 的時(shí)候,有一個(gè)很常見(jiàn)的場(chǎng)景就是多級(jí)作用域,比如在一個(gè) for 循環(huán)中再嵌套一個(gè) for 循環(huán)。而如何在做這個(gè)作用域分割,其實(shí)也是很簡(jiǎn)單,就是通過(guò)遞歸。 比如我的對(duì)一個(gè) ast 樹(shù)的處理方法命名為: 那么 processAst 就可以這么實(shí)現(xiàn): 就簡(jiǎn)單通過(guò)一個(gè)遞歸,就可以把作用域一直傳遞下去了。 實(shí)現(xiàn)上面功能后,組件就已經(jīng)具備基本的模板渲染能力,不過(guò)在用模板引擎的時(shí)候,還有一個(gè)很常用的功能就是 filter 。一般來(lái)說(shuō) filter 的使用方式都是這這樣 還是舉個(gè)例子: 在構(gòu)建 AST 的時(shí)候,就可以獲取到其中的 不過(guò)后來(lái)又覺(jué)得為了性能考慮,能夠在 AST 階段就能做完的工作就不要放到渲染階段了。因此就改成 vue 的方法組合方式。也就是把上面字符串變成: 預(yù)先用個(gè)方法包裹起來(lái),在渲染的時(shí)候,就不需要再通過(guò)循環(huán)去獲取 filter 并且執(zhí)行了。具體實(shí)現(xiàn)如下: 上面還有一個(gè)就是對(duì) safe 的處理,如果有 safe 這個(gè) filter ,就不做 escape 了。完成這個(gè)之后,有 filter 的 variable 都會(huì)變成 其實(shí)也是很簡(jiǎn)單,就是在 new Function 的時(shí)候,多傳入一個(gè)獲取 filter 的方法即可,然后有 filter 的 variable 就能被正常識(shí)別解析了。 感謝各位的閱讀!關(guān)于“怎么用JavaScript實(shí)現(xiàn)一個(gè)模板引擎”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,讓大家可以學(xué)到更多知識(shí),如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到吧!{%
,也就是上面的endIndex
是我們要的索引,記得要更新剩余的字符,直接通過(guò) slice 更新即可:// 2 是 {% 的長(zhǎng)度str = str.slice(endIndex + 2);
{%
,那么他的閉合處就肯定是%}
,因此就可以再通過(guò)const expression = str.slice(0, str.indexOf('%}'))
if test > 1
這個(gè)字符串了。然后我們?cè)偻ㄟ^(guò)正則/^if\s+([\s\S]+)$/
匹配,就可以知道這個(gè)字符串是 if 的標(biāo)簽,同時(shí)可以獲得test > 1
這一個(gè)捕獲組,然后就可以創(chuàng)建我們的第二個(gè)節(jié)點(diǎn),if 的塊級(jí)節(jié)點(diǎn)了。{% endif %}
之前的所有節(jié)點(diǎn),都是屬于 if 節(jié)點(diǎn)的子節(jié)點(diǎn),所以我們?cè)趧?chuàng)建節(jié)點(diǎn)時(shí)要給它一個(gè)children
數(shù)組屬性,用來(lái)保存子節(jié)點(diǎn)。{%
以及{{
的位置,跟上面的邏輯差不多,獲取到{{
的位置后再判斷}}
的位置,就可以創(chuàng)建第三個(gè)節(jié)點(diǎn),test 的變量節(jié)點(diǎn),并且 push 到 if 節(jié)點(diǎn)的子節(jié)點(diǎn)列表中。{% endif %}
這個(gè)閉合節(jié)點(diǎn),當(dāng)遇到該節(jié)點(diǎn)之后的節(jié)點(diǎn),就不能保存到 if 節(jié)點(diǎn)的子節(jié)點(diǎn)列表中了。緊接著就又是一個(gè)文本節(jié)點(diǎn)。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é)點(diǎn)
}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)建塊級(jí)節(jié)點(diǎn) elparent = el;
} else {
...創(chuàng)建變量節(jié)點(diǎn) el
}
(parent ? parent.children : root).push(el);
parse(str.slice(rightEndIndex + 2));
}
{% {{ test }}
,就要考慮到{%的干擾等。還有比如 else 還有 elseif 節(jié)點(diǎn)的處理,這兩個(gè)是需要關(guān)聯(lián)到 if 標(biāo)簽上的,這個(gè)也是需要特殊處理的。不過(guò)大概邏輯基本上就是以上。組合 html
html += el.text
即可。如果是if
節(jié)點(diǎn),則判斷表達(dá)式,比如上面的test > 1
,有兩種辦法可以實(shí)現(xiàn)表達(dá)式的計(jì)算,一種就是eval
,還有一種就是new Function
了,eval 會(huì)有安全性問(wèn)題,因此就不考慮了,而是使用new Function
的方式來(lái)實(shí)現(xiàn)。變量節(jié)點(diǎn)的計(jì)算也一樣,用new Function
來(lái)實(shí)現(xiàn)。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 中卻要寫(xiě)成?{{ 'good' if test else 'bad' }}
。實(shí)現(xiàn)多級(jí)作用域
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 ,比如上面的 listlist.forEach(item => {
processAst(ast.children, Object.assign({}, scope, {
[ast.key]: item, // ast.key 則是 for key in list 中的 key}))
})
}
...
}
Filter 功能實(shí)現(xiàn)
{{ test | filter1 | filter2 }}
,這個(gè)的實(shí)現(xiàn)也說(shuō)一下,這一塊的實(shí)現(xiàn)我參考了 vue 的解析的方式,還是蠻有意思的。{{ test | filter1 | filter2 }}
test | filter1 | filter2
,然后我們可以很簡(jiǎn)單的就獲取到 filter1 和 filter2 這兩個(gè)字符串。起初我的實(shí)現(xiàn)方式,是把這些 filter 字符串扔進(jìn) ast 節(jié)點(diǎn)的 filters 數(shù)組中,在渲染的時(shí)候再一個(gè)一個(gè)拿出來(lái)處理。_$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 wrappingutils.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 errorif (e.message.indexOf('is not defined') >= 0) { return '';
} else { throw e;
}
}
}
網(wǎng)站標(biāo)題:怎么用JavaScript實(shí)現(xiàn)一個(gè)模板引擎
轉(zhuǎn)載來(lái)源:http://weahome.cn/article/ihppep.html