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
变量,文件分布在不一样的目录下。这个就比较坑了,使用起来很不方便。当嵌套层次比较高时,怎么复用模版?貌似只能经过绝对路径的方式了。