ejs是一種歷史悠久的模版,具備簡單、性能好、使用普遍的特色。雖然沒有vue
、react
這些項目流行,但仍是有使用的場合和學習的價值。這裏會介紹ejs項目的源碼。使用方法詳見項目的readme,或者這裏。html
哲學 ejs是字符串模版引擎,生成的是字符串,其實能夠被用到很是多的地方,只要是動態生成字符串,就均可以用到。它的思想是模版 + 數據 => 最終的字符串
。模版
是字符串的格式,包含可變部分的和固定的部分,可變的部分經過數據
來控制。經過使用include
方法引用其餘模版。這個模型就比較符合前端開發的須要了。前端
一些概念vue
template
:即模版,好比這個栗子👇。<% if (user) { %>
<h2><%= user.name %></h2>
<% } %>
複製代碼
data
: 模版對應的數據,具備模版中使用的全部變量,好比上面這個栗子中必須具備user.name
這個數據。option
:模版配置項。有這些👇:
cache
Compiled functions are cached, requires filename
filename
The name of the file being rendered. Not required if you are using renderFile()
. Used by cache
to key caches, and for includes.root
Set project root for includes with an absolute path (/file.ejs).context
Function execution contextcompileDebug
When false
no debug instrumentation is compiledclient
When true
, compiles a function that can be rendered in the browser without needing to load the EJS Runtime (ejs.min.js).delimiter
Character to use with angle brackets for open/closedebug
Output generated function bodystrict
When set to true
, generated function is in strict mode_with
Whether or not to use with() {}
constructs. If false
then the locals will be stored in the locals
object. Set to false
in strict mode.localsName
Name to use for the object storing local variables when not using with
Defaults to locals
rmWhitespace
Remove all safe-to-remove whitespace, including leading and trailing whitespace. It also enables a safer version of -%>
line slurping for all scriptlet tags (it does not strip new lines of tags in the middle of a line).escape
The escaping function used with <%=
construct. It is used in rendering and is .toString()
ed in the generation of client functions. (By default escapes XML).outputFunctionName
Set to a string (e.g., 'echo' or 'print') for a function to print output inside scriptlet tags.async
When true
, EJS will use an async function for rendering. (Depends on async/await support in the JS runtime.compile
:編譯函數,把template和option轉化爲一個函數,往這個函數中注入數據,生成最終的字符串,不必定是html哦,還能夠是各類形式的字符串。render
:渲染函數,直接把template、data和option轉化爲最終的字符串。主流程 ejs引擎的實現思路是把配置的模版轉化爲渲染的函數,再經過的數據生成字符串。把模版轉化爲渲染函數的這個過程就是compile
。它的主要工做就是通生成函數輸入和函數體的字符串,再經過Function這個類來生成函數。執行流程分別爲:react
{ key1 = <%= key1 %>, 2key1 = <%= key1+key1 %> }
會被切割成[ '{ key1 = ', '<%=', ' key1 ', '%>', ', 2key1 = ', '<%=', ' key1+key1 ', '%>', ' }' ]
' ; __append("{ key1 = ")\n ; __append(escapeFn( key1 ))\n ; __append(", 2key1 = ")\n ; __append(escapeFn( key1+key1 ))\n ; __append(" }")\n'
複製代碼
opts.localsName + ', escapeFn, include, rethrow'
複製代碼
var __output = [], __append = __output.push.bind(__output);
with (locals || {}) {
; __append("{ key1 = ")
; __append(escapeFn( key1 ))
; __append(", 2key1 = ")
; __append(escapeFn( key1+key1 ))
; __append(" }")
}
return __output.join("");
複製代碼
function (data) {
var include = function (path, includeData) {
var d = utils.shallowCopy({}, data);
if (includeData) {
d = utils.shallowCopy(d, includeData);
}
return includeFile(path, opts)(d);
};
return fn.apply(opts.context, [data || {}, escapeFn, include, rethrow]);
}
複製代碼
經過渲染函數和數據生成結果的方式和react比較像哈。但ejs在不直接支持嵌套,而是經過include方法調用子模版的渲染函數。git
一些細節 渲染函數的4個參數:data
、escapeFn
、include
、rethrow
。github
data
: 傳入的數據。escapeFn
: 轉義函數。include
: 引入子模版函數。主要的邏輯是根據路徑獲取模版,而且編譯生成渲染函數進行緩存,最後進行渲染。var include = function (path, includeData) {
var d = utils.shallowCopy({}, data);
if (includeData) {
d = utils.shallowCopy(d, includeData);
}
return includeFile(path, opts)(d);
};
複製代碼
rethrow
: 拋出異常函數。生成渲染函數的執行步驟。 這一步是在模版被切割以後進行的,首先模版遇到ejs的標籤時就會被切割,切割後的字符串中標籤是成對出現的,引用一下上面的栗子。正則表達式
[ '{ key1 = ', '<%=', ' key1 ', '%>', ', 2key1 = ', '<%=', ' key1+key1 ', '%>', ' }' ]
複製代碼
ejs會根據不一樣的標籤生成不一樣的執行步驟。執行過程當中會遍歷整個數組。因爲標籤不能嵌套,並且成對出現,正好能夠利用全局的的變量,保存當前標籤的類型,執行到夾在一對標籤中的內容時,能夠獲取到外層標籤信息。當執行到閉合標籤時,重置標籤信息。api
關於路徑 先了解一下include
方法,ejs的語法不支持嵌套,只能經過這個方法來複用模版。下面是一個使用的栗子。數組
<ul>
<% users.forEach(function(user){ %>
<%- include('user/show', {user: user}) %>
<% }); %>
</ul>
複製代碼
在使用include
方法時,須要傳入複用template
的路徑和data
。路徑的邏輯先會看是不是絕對路徑,而後會拼接傳入的路徑參數和options.filename
,若是不存在這個文件最後看views
的目錄下是否存在這個文件,代碼請看👇緩存
function getIncludePath(path, options) {
var includePath;
var filePath;
var views = options.views;
// Abs path
if (path.charAt(0) == '/') {
includePath = exports.resolveInclude(path.replace(/^\/*/,''), options.root || '/', true);
}
// Relative paths
else {
// Look relative to a passed filename first
if (options.filename) {
filePath = exports.resolveInclude(path, options.filename);
if (fs.existsSync(filePath)) {
includePath = filePath;
}
}
// Then look in any views directories
if (!includePath) {
if (Array.isArray(views) && views.some(function (v) {
filePath = exports.resolveInclude(path, v, true);
return fs.existsSync(filePath);
})) {
includePath = filePath;
}
}
if (!includePath) {
throw new Error('Could not find the include file "' +
options.escapeFunction(path) + '"');
}
}
return includePath;
}
複製代碼
這就意味着在使用include
的時候,子template
文件只能在views
目錄下,後綴爲ejs
的文件。或者設置options.filename
變量,文件分佈在不一樣的目錄下。這個就比較坑了,使用起來很不方便。當嵌套層次比較高時,怎麼複用模版?貌似只能經過絕對路徑的方式了。