EJS[1]-源碼解析

  • 2019 年 12 月 5 日
  • 筆記

官方文檔中有提到兩個,最基本的使用也確實只有那兩個,但是實際上可以調用的函數有五個。 本篇會介紹下這五個API的作用&本人對於該API實現的一些想法。

EJSv1.x,程式碼篇幅上可以稱得上短小精悍,算上注釋不過400行。

parse

我們會從最裡邊的parse函數說起。parse函數是根據EJS模版來生成一段可執行的腳本字元串。

parsecompilerender三個函數的參數是屬於透傳的,第一個參數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函數在後邊還會處理一個EJSv1.x版本有的Filters特性,因為不常用,而且v2.x版本已經移除了,所以就不再贅述。

compile

compile函數中會調用parse函數,獲取腳本字元串。 並將字元串作為一個函數的主體來創建新的函數。 如果開啟了debugcompile會添加一些額外的資訊在腳本中。一些類似於堆棧監聽之類的。

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的,但是為了避免renderFilepath和快取的key重複,所以renderFile中有這麼一個小操作。

var key = path + ':string';

小記

EJSv1.x源碼非常清晰易懂,很適合作為研究模版引擎類的入門。 v2.x使用了一些面向對象的程式設計。。篇幅更是達到了接近900行(費解-.-不知道意義何在)。。有機會嘗試著會去讀一些v2.x版本的程式碼。