ejs是一種歷史悠久的模版,具有簡單、效能好、使用廣泛的特點。雖然沒有vue
、react
這些專案流行,但還是有使用的場合和學習的價值。這裡會介紹ejs專案的原始碼。使用方法詳見專案的readme,或者這裡。
哲學
ejs是字串模版引擎,生成的是字串,其實可以被用到非常多的地方,只要是動態生成字串,就都可以用到。它的思想是模版 + 資料 => 最終的字串
。模版
是字串的格式,包含可變部分的和固定的部分,可變的部分通過資料
來控制。通過使用include
方法引用其他模版。這個模型就比較符合前端開發的需要了。
一些概念
-
template
:即模版,比如這個栗子?。
<% if (user) { %>
<h2><%= user.name %></h2>
<% } %>
-
data
: 模版對應的資料,具有模版中使用的所有變數,比如上面這個栗子中必須具有user.name
這個資料。 -
option
:模版配置項。有這些?:-
cache
Compiled functions are cached, requiresfilename
-
filename
The name of the file being rendered. Not required if you
are usingrenderFile()
. Used bycache
to key caches, and for includes. -
root
Set project root for includes with an absolute path (/file.ejs). -
context
Function execution context -
compileDebug
Whenfalse
no debug instrumentation is compiled -
client
Whentrue
, 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/close -
debug
Output generated function body -
strict
When set totrue
, generated function is in strict mode -
_with
Whether or not to usewith() {}
constructs. Iffalse
then the locals will be stored in thelocals
object. Set tofalse
in strict mode. -
localsName
Name to use for the object storing local variables when not usingwith
Defaults tolocals
-
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
Whentrue
, 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這個類來生成函式。執行流程分別為:
- 根據正規表示式切割模版,比如
{ key1 = <%= key1 %>, 2key1 = <%= key1+key1 %> }
會被切割成[ `{ key1 = `, `<%=`, ` key1 `, `%>`, `, 2key1 = `, `<%=`, ` key1+key1 `, `%>`, ` }` ]
- 根據切割後的資料,生成渲染函式中的執行步驟。上面這個栗子中,執行步驟為
` ; __append("{ key1 = ")
; __append(escapeFn( key1 ))
; __append(", 2key1 = ")
; __append(escapeFn( key1+key1 ))
; __append(" }")
`
- 組裝函式,通過prepend+執行步驟+append的模式生成函式體的字串,最後生成這樣的函式頭、函式體,以及渲染函式。
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方法呼叫子模版的渲染函式。
一些細節
渲染函式的4個引數:data
、escapeFn
、include
、rethrow
。
-
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會根據不同的標籤生成不同的執行步驟。執行過程中會遍歷整個陣列。由於標籤不能巢狀,而且成對出現,正好可以利用全域性的的變數,儲存當前標籤的型別,執行到夾在一對標籤中的內容時,可以獲取到外層標籤資訊。當執行到閉合標籤時,重置標籤資訊。
關於路徑
先了解一下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
變數,檔案分佈在不同的目錄下。這個就比較坑了,使用起來很不方便。當巢狀層次比較高時,怎麼複用模版?貌似只能通過絕對路徑的方式了。