EJS[1]-源碼解析
- 2019 年 12 月 5 日
- 筆記
官方文檔中有提到兩個,最基本的使用也確實只有那兩個,但是實際上可以調用的函數有五個。 本篇會介紹下這五個API的作用&本人對於該API實現的一些想法。
EJS
v1.x,程式碼篇幅上可以稱得上短小精悍,算上注釋不過400行。
parse
我們會從最裡邊的parse
函數說起。parse
函數是根據EJS
模版來生成一段可執行的腳本字元串。
parse
、compile
、render
三個函數的參數是屬於透傳的,第一個參數str
為模版源字元串,第二個參數options
是可選的配置參數。
parse
函數在拿到str
以後,會將字元串拆成一個個的字元來匹配。
拋開匹配到界定符的邏輯外,其餘的一些匹配都是自增+1形式的,比如n
、\
、'
或任意的普通文本。 也就是說,如果一個EJS
模版文件沒有用到太多的動態腳本,強烈建議開啟cache
。 就如同下圖的程式碼,EJS
會循環字元串的所有字元,執行一遍拼接,這個工作後續是有大量的重複的,如果開啟了cache
後,就可以避免這個問題,這也是可以提升性能的。
ejs.render('<h1>Title</h1>')
其次就是判斷字元命中為界定符: 會進一步的去查找結束的界定符,如果沒有找到則會拋出異常。
var open = options.open || exports.open || '<%' var close = options.close || exports.close || '%>' for (var i = 0, len = str.length; i < len; ++i) { var stri = str[i]; // 判斷是否匹配為開始界定符 if (str.slice(i, open.length + i) == open) { // ... some code var end = str.indexOf(close, i); // 如果沒有找到結束的界定符,拋出異常 if (end < 0){ throw new Error('Could not find matching close tag "' + close + '".'); } } }
在得到了JavaScript
腳本的範圍(在字元串中的下標)後,我們就可以開始著手拼接腳本的工作了。 首先我們需要判斷這一段腳本的類型,因為我們知道EJS
提供了有三種腳本標籤<% code %>
、<%- code %>
、<%= code %>
三種處理方式也是不一樣的,第一個會直接執行腳本,其餘兩個會輸出腳本執行的返回值。 所以三種標籤的差異就體現在這裡: 這裡是將要包裹腳本的前綴後綴給創建了出來。 最終的返回結果會是 prefix + js + postfix
。 我們會發現prefix
裡邊有一個line
變數,這裡用到了逗號運算符/逗號操作符,很巧妙。 作為一個行號的輸出,既不會影響程式的執行,又可以在出錯的時候幫助我們快速定位問題所在。
switch (str[i]) { case '=': // 序列化返回值 prefix = "', escape((" + line + ', '; postfix = ")), '"; ++i; break; case '-': // 直接返回 prefix = "', (" + line + ', '; postfix = "), '"; ++i; break; default: // 僅僅是執行 prefix = "');" + line + ';'; postfix = "; buf.push('"; }
三種標籤拼接後的示例:
// var buf = [] ejs.render('<h1><%= "Title" %></h1>') // buf.push('<h1>', escape((1, 'Title')), '</h1>') ejs.render('<h1><%- "Title" %></h1>') // buf.push('<h1>', (1, 'Title'), '</h1>') ejs.render('<h1><% "Title" %></h1>') // buf.push('<h1>'); 1; 'Title'; buf.push('</h1>') // return buf.join('')
P.S. parse
函數在後邊還會處理一個EJS
v1.x版本有的Filters
特性,因為不常用,而且v2.x版本已經移除了,所以就不再贅述。
compile
compile
函數中會調用parse
函數,獲取腳本字元串。 並將字元串作為一個函數的主體來創建新的函數。 如果開啟了debug
,compile
會添加一些額外的資訊在腳本中。一些類似於堆棧監聽之類的。
str = exports.parse(str, options) // 獲取腳本字元串 var fn = new Function('locals, filters, escape, rethrow', str) // 創建函數 return function (locals) { fn.call(this, locals, filters, escape, rethrow); }
render
render
函數會調用compile
函數,並執行它得到模版處理後的結果。 cache
的判斷也是在render
函數這裡做的。 我們存在記憶體中用來快取的模版並不是執行後的結果,而是創建好的那個函數,也就是compile
的返回值,也就是說,我們快取的其實是構建函數的那一個步驟,我們可以傳入不同的變數來實現動態的渲染,並且不必多次重複構建模版函數。
renderFile
renderFile
函數只能夠在node環境下使用。。因為有涉及到了io
的操作,需要取讀取文件內容,然後調用render
函數。 同時renderFile
也是可以使用cache
的,但是為了避免renderFile
的path
和快取的key
重複,所以renderFile
中有這麼一個小操作。
var key = path + ':string';
小記
EJS
v1.x源碼非常清晰易懂,很適合作為研究模版引擎類的入門。 v2.x使用了一些面向對象的程式設計。。篇幅更是達到了接近900行(費解-.-不知道意義何在)。。有機會嘗試著會去讀一些v2.x版本的程式碼。