Ejs 编译过程简单理解
// 编写一个简单的 ejs 编译, 使用 debug 输出编译完成的执行函数
const ejs = require('ejs');
const html = `<% for (let i = 0; i < n; i++) { %>
<div><%= i %></div>_%>
<% } %>`
const template = ejs.compile(html, { debug: true })
console.log(template({ n: 3 }))
执行 ejs.compile
后返回 template
函数
// template
var __line = 1
, __lines = "<% for (let i = 0; i < n; i++) { %>\n<div><%= i %></div>_%>\n<% } %>"
, __filename = undefined;
try {
var __output = "";
function __append(s) { if (s !== undefined && s !== null) __output += s }
with (locals || {}) {
; for (let i = 0; i < n; i++) {
; __append("\n<div>")
; __line = 2
; __append(escapeFn( i ))
; __append("</div>")
; __line = 3
; }
}
return __output;
} catch (e) {
rethrow(e, __lines, __filename, __line, escapeFn);
}
可以看到,编译后模板主要被转换成了
var __output = ""; // 用于输出
// __append 进行判空
function __append(s) { if (s !== undefined && s !== null) __output += s }
with (locals || {}) { // locals 即为,template 执行时传入的 data 参数,这里为 { n: 3 }
; for (let i = 0; i < n; i++) {
; __append("\n<div>")
; __line = 2 // 用于抛出异常,显示错误行数
; __append(escapeFn( i ))
; __append("</div>")
; __line = 3
; }
}
return __output;
// escapeFn 默认会调用 escapeXML 对内容进转义,例如:< 会被转为 <
var escapeFn = opts.escapeFunction;
options.escapeFunction = opts.escape || opts.escapeFunction || utils.escapeXML;
// utils.js
exports.escapeXML = function (markup) {
return markup == undefined
? ''
: String(markup)
.replace(_MATCH_HTML, encode_char); // _MATCH_HTML: /[&<>'"]/g
};
查看源码
exports.compile = function compile(template, opts) {
var templ;
// v1 compat
// 'scope' is 'context'
// FIXME: Remove this in a future version
if (opts && opts.scope) {
if (!scopeOptionWarned){
console.warn('`scope` option is deprecated and will be removed in EJS 3');
scopeOptionWarned = true;
}
if (!opts.context) {
opts.context = opts.scope;
}
delete opts.scope;
}
// 采用默认执行,会直接到这一步,创建一个 Template
templ = new Template(template, opts);
// 编译模板函数
return templ.compile();
};
// Template
function Template(text, opts) {
// 内部初始化了默认配置
...
// 创建匹配正则
this.regex = this.createRegex();
}
// createRegex
createRegex: function () {
var str = _REGEX_STRING; // (<%%|%%>|<%=|<%-|<%_|<%#|<%|%>|-%>|_%>)
// 可供自定义配置
var delim = utils.escapeRegExpChars(this.opts.delimiter);
var open = utils.escapeRegExpChars(this.opts.openDelimiter);
var close = utils.escapeRegExpChars(this.opts.closeDelimiter);
str = str.replace(/%/g, delim)
.replace(/</g, open)
.replace(/>/g, close);
return new RegExp(str);
},
// templ.compile();
compile: function () {
// 初始化配置
// 处理
if (!this.source) {
this.generateSource(); //
prepended +=
' var __output = "";\n' +
' function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
if (opts.outputFunctionName) {
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
}
if (opts.destructuredLocals && opts.destructuredLocals.length) {
var destructuring = ' var __locals = (' + opts.localsName + ' || {}),\n';
for (var i = 0; i < opts.destructuredLocals.length; i++) {
var name = opts.destructuredLocals[i];
if (i > 0) {
destructuring += ',\n ';
}
destructuring += name + ' = __locals.' + name;
}
prepended += destructuring + ';\n';
}
if (opts._with !== false) {
prepended += ' with (' + opts.localsName + ' || {}) {' + '\n';
appended += ' }' + '\n';
}
appended += ' return __output;' + '\n';
this.source = prepended + this.source + appended;
}
// this.source
// var __output = "";
// function __append(s) { if (s !== undefined && s !== null) __output += s }
// with (locals || {}) {
// ; for (let i = 0; i < n; i++) {
// ; __append("\<div>") ; __line = 2
// ; __append(escapeFn( i ))
// ; __append("</div>")
// ; __line = 3
// ; __append( include('./user.ejs', { user: '<test' }) )
// ; __line = 4 ; }
// }
// return __output;
// 是否启用 debug 模式
if (opts.compileDebug) {
// 添加异常捕获
src = 'var __line = 1' + '\n'
+ ' , __lines = ' + JSON.stringify(this.templateText) + '\n'
+ ' , __filename = ' + sanitizedFilename + ';' + '\n'
+ 'try {' + '\n'
+ this.source
+ '} catch (e) {' + '\n'
+ ' rethrow(e, __lines, __filename, __line, escapeFn);' + '\n'
+ '}' + '\n';
}
else {
src = this.source;
}
// 其它处理,模板检查
...
// 判断是否使用 async/await
try {
if (opts.async) {
// Have to use generated function for this, since in envs without support,
// it breaks in parsing
try {
// 通过 new Function 的方式创建一个 async 构造方法
// 等价于 (function () { return (async function () { }).constructor; })()
ctor = (new Function('return (async function(){}).constructor;'))();
}
catch(e) {
if (e instanceof SyntaxError) {
throw new Error('This environment does not support async/await');
}
else {
throw e;
}
}
}
else {
// Function 对象
ctor = Function;
}
// new Function('a,b','console.log(a + b)') => function(a, b) { console.log(a + b) }
// 创建 fn, 配置参数
fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src);
}
catch(e) {
// istanbul ignore else
if (e instanceof SyntaxError) {
if (opts.filename) {
e.message += ' in ' + opts.filename;
}
e.message += ' while compiling ejs\n\n';
e.message += 'If the above error is not helpful, you may want to try EJS-Lint:\n';
e.message += 'https://github.com/RyanZim/EJS-Lint';
if (!opts.async) {
e.message += '\n';
e.message += 'Or, if you meant to create an async function, pass `async: true` as an option.';
}
}
throw e;
}
// Return a callable function which will execute the function
// created by the source-code, with the passed data as locals
// Adds a local `include` function which allows full recursive include
// 判断是否为 client
var returnedFn = opts.client ? fn : function anonymous(data) {
// 处理 include 会重新执行一遍编译
var include = function (path, includeData) {
var d = utils.shallowCopy({}, data);
if (includeData) {
d = utils.shallowCopy(d, includeData);
}
return includeFile(path, opts)(d);
};
// 调用 fn,传入参数, 会在缓存中执行,输出结果
return fn.apply(opts.context, [data || {}, escapeFn, include, rethrow]);
// (function anonymous(locals, escapeFn, include, rethrow
// ) {
// var __line = 1
// , __lines = "<% for (let i = 0; i < n; i++) { %>\n<div><%= i %></div>_%>\n<%- include('./user.ejs', { user: '<test' }); _%>\n<% } %>"
// , __filename = undefined;
// try {
// var __output = "";
// function __append(s) { if (s !== undefined && s !== null) __output += s }
// with (locals || {}) {
// ; for (let i = 0; i < n; i++) {
// ; __append("\n<div>")
// ; __line = 2
// ; __append(escapeFn( i ))
// ; __append("</div>")
// ; __line = 3
// ; __append( include('./user.ejs', { user: '<test' }) )
// ; __line = 4
// ; }
// }
// return __output;
// } catch (e) {
// rethrow(e, __lines, __filename, __line, escapeFn);
// }
// })
};
if (opts.filename && typeof Object.defineProperty === 'function') {
var filename = opts.filename;
var basename = path.basename(filename, path.extname(filename));
try {
Object.defineProperty(returnedFn, 'name', {
value: basename,
writable: false,
enumerable: false,
configurable: true
});
} catch (e) {/* ignore */}
}
return returnedFn;
}
// generateSource
generateSource: function () {
var opts = this.opts;
// 判断是否去掉空格/空行
if (opts.rmWhitespace) {
// Have to use two separate replace here as `^` and `$` operators don't
// work well with `\r` and empty lines don't work well with the `m` flag.
this.templateText =
this.templateText.replace(/[\r\n]+/g, '\n').replace(/^\s+|\s+$/gm, '');
}
// 去掉前后空格
// Slurp spaces and tabs before <%_ and after _%>
this.templateText =
this.templateText.replace(/[ \t]*<%_/gm, '<%_').replace(/_%>[ \t]*/gm, '_%>');
var self = this;
var matches = this.parseTemplateText(); // 分割模板字符串
var d = this.opts.delimiter;
var o = this.opts.openDelimiter;
var c = this.opts.closeDelimiter;
// 遍历分割的数组,判断是否合法
if (matches && matches.length) {
matches.forEach(function (line, index) {
var closing;
// If this is an opening tag, check for closing tags
// FIXME: May end up with some false positives here
// Better to store modes as k/v with openDelimiter + delimiter as key
// Then this can simply check against the map
if ( line.indexOf(o + d) === 0 // If it is a tag
&& line.indexOf(o + d + d) !== 0) { // and is not escaped
closing = matches[index + 2];
if (!(closing == d + c || closing == '-' + d + c || closing == '_' + d + c)) {
throw new Error('Could not find matching close tag for "' + line + '".');
}
}
// 处理项
self.scanLine(line);
});
}
},
// parseTemplateText
parseTemplateText: function () {
var str = this.templateText; // 获取模板字符串
var pat = this.regex; // 获取正则匹配规则 (<%%|%%>|<%=|<%-|<%_|<%#|<%|%>|-%>|_%>)
var result = pat.exec(str); // 执行匹配,返回匹配结果 result
var arr = [];
var firstPos; // 下标
while (result) { // 有匹配结果
firstPos = result.index; // 获取匹配下标
if (firstPos !== 0) { // 下标是否不等于 0
arr.push(str.substring(0, firstPos)); // 截取前段,推入数组
str = str.slice(firstPos); // 去掉前段
}
arr.push(result[0]); // 将第一个匹配部分,推入数组
str = str.slice(result[0].length); // 去掉匹配部分
result = pat.exec(str); // 继续匹配,直到匹配不到
}
if (str) { // 判断字符串是否还有剩余
arr.push(str); // 有推入数组
}
return arr; // 返回数组
},
// scanLine 扫描每一行
scanLine: function (line) {
var self = this;
var d = this.opts.delimiter;
var o = this.opts.openDelimiter;
var c = this.opts.closeDelimiter;
var newLineCount = 0;
newLineCount = (line.split('\n').length - 1);
// 根据符号,设置模式
switch (line) {
case o + d:
case o + d + '_':
this.mode = Template.modes.EVAL;
break;
case o + d + '=':
this.mode = Template.modes.ESCAPED;
break;
case o + d + '-':
this.mode = Template.modes.RAW;
break;
case o + d + '#':
this.mode = Template.modes.COMMENT;
break;
case o + d + d:
this.mode = Template.modes.LITERAL;
this.source += ' ; __append("' + line.replace(o + d + d, o + d) + '")' + '\n';
break;
case d + d + c:
this.mode = Template.modes.LITERAL;
this.source += ' ; __append("' + line.replace(d + d + c, d + c) + '")' + '\n';
break;
case d + c:
case '-' + d + c:
case '_' + d + c:
if (this.mode == Template.modes.LITERAL) {
this._addOutput(line);
}
this.mode = null;
this.truncate = line.indexOf('-') === 0 || line.indexOf('_') === 0;
break;
default:
// In script mode, depends on type of tag
if (this.mode) {
// If '//' is found without a line break, add a line break.
switch (this.mode) {
case Template.modes.EVAL:
case Template.modes.ESCAPED:
case Template.modes.RAW:
if (line.lastIndexOf('//') > line.lastIndexOf('\n')) {
line += '\n';
}
}
switch (this.mode) {
// Just executing code
case Template.modes.EVAL:
this.source += ' ; ' + line + '\n';
break;
// Exec, esc, and output
case Template.modes.ESCAPED:
this.source += ' ; __append(escapeFn(' + stripSemi(line) + '))' + '\n';
break;
// Exec and output
case Template.modes.RAW:
this.source += ' ; __append(' + stripSemi(line) + ')' + '\n';
break;
case Template.modes.COMMENT:
// Do nothing
break;
// Literal <%% mode, append as raw output
case Template.modes.LITERAL:
this._addOutput(line);
break;
}
}
// In string mode, just add the output
else {
this._addOutput(line);
}
}
if (self.opts.compileDebug && newLineCount) {
this.currentLine += newLineCount;
this.source += ' ; __line = ' + this.currentLine + '\n';
}
}
补充
参数
cache
缓存编译后的函数,需要指定filename
filename
被cache
参数用做键值,同时也用于 include 语句context
函数执行时的上下文环境compileDebug
当值为false
时不编译调试语句client
返回独立的编译后的函数delimiter
放在角括号中的字符,用于标记标签的开与闭debug
将生成的函数体输出_with
是否使用with() {}
结构。如果值为false
,所有局部数据将存储在locals
对象上。localsName
如果不使用with
,localsName 将作为存储局部变量的对象的名称。默认名称是locals
rmWhitespace
删除所有可安全删除的空白字符,包括开始与结尾处的空格。对于所有标签来说,它提供了一个更安全版本的-%>
标签(在一行的中间并不会剔除标签后面的换行符)。escape
为<%=
结构设置对应的转义(escape)函数。它被用于输出结果以及在生成的客户端函数中通过.toString()
输出。(默认转义 XML)。outputFunctionName
设置为代表函数名的字符串(例如'echo'
或'print'
)时,将输出脚本标签之间应该输出的内容。async
当值为true
时,EJS 将使用异步函数进行渲染。(依赖于 JS 运行环境对 async/await 是否支持)
标签含义
<%
‘脚本’ 标签,用于流程控制,无输出。<%_
删除其前面的空格符<%=
输出数据到模板(输出是转义 HTML 标签)<%-
输出非转义的数据到模板<%#
注释标签,不执行、不输出内容<%%
输出字符串 ‘<%’%>
一般结束标签-%>
删除紧随其后的换行符_%>
将结束标签后面的空格符删除