我将响应字符串以 EJS 形式存储在数据库中,并在 Node.js 中填写数据。我想要做的是能够使用我想要的任何属性,无论它来自什么模型,然后在 Node 中,一旦我有了模板,就根据所需的属性异步/等待这些模型。
所以如果我有一个像这样的模板:
"Hello <%=user.firstName%>."
我希望能够查看该模板并提取如下内容:
ejsProperties = ["user", "user.firstName"]
或类似的东西。
最佳答案
如果你只是想提取一些简单的东西,比如 user.firstName
然后在 EJS 文件上运行 RegExp 可能是一种很好的方法。您可能正在寻找一组特定且已知的对象和属性,因此您可以专门针对它们,而不是尝试提取所有可能的对象/属性。
在更一般的情况下,事情会很快变得困难。这样的事情处理起来非常棘手:
<% var u = user; %><%= u.firstName %>
这是一个愚蠢的例子,但这只是冰山一 Angular 。鉴于
user
正在从 locals
读取并且是感兴趣的对象,u
几乎可以是任何东西,我们不能轻易画出连接 firstName
的线。和 user
通过 u
.类似的类似 forEach
在数组或 for/in
在一个对象上将很快导致无法将属性链接到适当的 locals
入口。但是,我们可以做的是识别
locals
中的条目。 ,或者至少是非常接近的东西。以
<%= user.firstName %>
为例标识符 user
可以指三件事之一。首先,它可能是 locals
中的一个条目。 .其次,它可能是全局对象的属性。第三,它可以是在模板范围内创建的变量(如前面示例中的 u
)。我们无法真正区分前两种情况,但您可以很容易地分离出全局变量。像
console
这样的事情和 Math
可以识别和丢弃。第三种情况比较棘手,说明
locals
中的条目之间的区别。和模板中的一个变量,就像在这个例子中一样:<% users.forEach(function(user) { %>
<%= user.firstName %>
<% }); %>
在这种情况下
users
直接来自locals
但是 user
不是。要解决这个问题,我们需要类似于 IDE 中的变量范围分析。所以这就是我尝试过的:
locals
中的一个条目。目的。 EJS 使用 with (locals) {...}
内部所以真的没有办法知道它是哪一个。 我富有想象力地将结果称为
ejsprima
.我没有尝试支持 EJS 支持的所有选项,所以如果您使用自定义分隔符或严格模式,它将无法工作。 (如果您使用的是严格模式,则无论如何都必须在模板中显式写入
locals.user.firstName
,而这需要通过 RegExp 来完成)。它不会尝试关注任何 include
来电。如果某个地方没有潜伏错误,我会感到非常惊讶,即使有一些基本的 JS 语法,但我已经测试了我能想到的所有令人讨厌的情况。包括测试用例。
主演示中使用的 EJS 可以在 HTML 的顶部找到。我提供了一个“全局写入”的无偿示例,只是为了展示它们的外观,但我想它们不是你通常想要的东西。有趣的是
reads
部分。我针对 esprima 4 开发了这个,但我能找到的最好的 CDN 版本是 2.7.3。测试都仍然通过,所以这似乎并不重要。
我在片段的 JS 部分中包含的唯一代码是针对“ejsprima”本身的。要在 Node 中运行它,您只需要复制它并调整顶部和底部以更正导出和需要的东西。
// Begin 'ejsprima'
(function(exports) {
//var esprima = require('esprima');
// Simple EJS compiler that throws away the HTML sections and just retains the JavaScript code
exports.compile = function(tpl) {
// Extract the tags
var tags = tpl.match(/(<%(?!%)[\s\S]*?[^%]%>)/g);
return tags.map(function(tag) {
var parse = tag.match(/^(<%[=\-_#]?)([\s\S]*?)([-_]?%>)$/);
switch (parse[1]) {
case '<%=':
case '<%-':
return ';(' + parse[2] + ');';
case '<%#':
return '';
case '<%':
case '<%_':
return parse[2];
}
throw new Error('Assertion failure');
}).join('\n');
};
// Pull out the identifiers for all 'global' reads and writes
exports.extractGlobals = function(tpl) {
var ast = tpl;
if (typeof tpl === 'string') {
// Note: This should be parseScript in esprima 4
ast = esprima.parse(tpl);
}
// Uncomment this line to dump out the AST
//console.log(JSON.stringify(ast, null, 2));
var refs = this.processAst(ast);
var reads = {};
var writes = {};
refs.forEach(function(ref) {
ref.globalReads.forEach(function(key) {
reads[key] = true;
});
});
refs.forEach(function(ref) {
ref.globalWrites.forEach(function(key) {
writes[key] = true;
})
});
return {
reads: Object.keys(reads),
writes: Object.keys(writes)
};
};
exports.processAst = function(obj) {
var baseScope = {
lets: Object.create(null),
reads: Object.create(null),
writes: Object.create(null),
vars: Object.assign(Object.create(null), {
// These are all local to the rendering function
arguments: true,
escapeFn: true,
include: true,
rethrow: true
})
};
var scopes = [baseScope];
processNode(obj, baseScope);
scopes.forEach(function(scope) {
scope.globalReads = Object.keys(scope.reads).filter(function(key) {
return !scope.vars[key] && !scope.lets[key];
});
scope.globalWrites = Object.keys(scope.writes).filter(function(key) {
return !scope.vars[key] && !scope.lets[key];
});
// Flatten out the prototype chain - none of this is actually used by extractGlobals so we could just skip it
var allVars = Object.keys(scope.vars).concat(Object.keys(scope.lets)),
vars = {},
lets = {};
// An identifier can either be a var or a let not both... need to ensure inheritance sees the right one by
// setting the alternative to false, blocking any inherited value
for (var key in scope.lets) {
if (hasOwn(scope.lets)) {
scope.vars[key] = false;
}
}
for (key in scope.vars) {
if (hasOwn(scope.vars)) {
scope.lets[key] = false;
}
}
for (key in scope.lets) {
if (scope.lets[key]) {
lets[key] = true;
}
}
for (key in scope.vars) {
if (scope.vars[key]) {
vars[key] = true;
}
}
scope.lets = Object.keys(lets);
scope.vars = Object.keys(vars);
scope.reads = Object.keys(scope.reads);
function hasOwn(obj) {
return obj[key] && (Object.prototype.hasOwnProperty.call(obj, key));
}
});
return scopes;
function processNode(obj, scope) {
if (!obj) {
return;
}
if (Array.isArray(obj)) {
obj.forEach(function(o) {
processNode(o, scope);
});
return;
}
switch(obj.type) {
case 'Identifier':
scope.reads[obj.name] = true;
return;
case 'VariableDeclaration':
obj.declarations.forEach(function(declaration) {
// Separate scopes for var and let/const
processLValue(declaration.id, scope, obj.kind === 'var' ? scope.vars : scope.lets);
processNode(declaration.init, scope);
});
return;
case 'AssignmentExpression':
processLValue(obj.left, scope, scope.writes);
if (obj.operator !== '=') {
processLValue(obj.left, scope, scope.reads);
}
processNode(obj.right, scope);
return;
case 'UpdateExpression':
processLValue(obj.argument, scope, scope.reads);
processLValue(obj.argument, scope, scope.writes);
return;
case 'FunctionDeclaration':
case 'FunctionExpression':
case 'ArrowFunctionExpression':
var newScope = {
lets: Object.create(scope.lets),
reads: Object.create(null),
vars: Object.create(scope.vars),
writes: Object.create(null)
};
scopes.push(newScope);
obj.params.forEach(function(param) {
processLValue(param, newScope, newScope.vars);
});
if (obj.id) {
// For a Declaration the name is accessible outside, for an Expression it is only available inside
if (obj.type === 'FunctionDeclaration') {
scope.vars[obj.id.name] = true;
}
else {
newScope.vars[obj.id.name] = true;
}
}
processNode(obj.body, newScope);
return;
case 'BlockStatement':
case 'CatchClause':
case 'ForInStatement':
case 'ForOfStatement':
case 'ForStatement':
// Create a new block scope
scope = {
lets: Object.create(scope.lets),
reads: Object.create(null),
vars: scope.vars,
writes: Object.create(null)
};
scopes.push(scope);
if (obj.type === 'CatchClause') {
processLValue(obj.param, scope, scope.lets);
processNode(obj.body, scope);
return;
}
break; // Don't return
}
Object.keys(obj).forEach(function(key) {
var value = obj[key];
// Labels for break/continue
if (key === 'label') {
return;
}
if (key === 'left') {
if (obj.type === 'ForInStatement' || obj.type === 'ForOfStatement') {
if (obj.left.type !== 'VariableDeclaration') {
processLValue(obj.left, scope, scope.writes);
return;
}
}
}
if (obj.computed === false) {
// MemberExpression, ClassExpression & Property
if (key === 'property' || key === 'key') {
return;
}
}
if (value && typeof value === 'object') {
processNode(value, scope);
}
});
}
// An l-value is something that can appear on the left of an = operator. It could be a simple identifier, as in
// `var a = 7;`, or something more complicated, like a destructuring. There's a big difference between how we handle
// `var a = 7;` and `a = 7;` and the 'target' is used to control which of these two scenarios we are in.
function processLValue(obj, scope, target) {
nextLValueNode(obj);
function nextLValueNode(obj) {
switch (obj.type) {
case 'Identifier':
target[obj.name] = true;
break;
case 'ObjectPattern':
obj.properties.forEach(function(property) {
if (property.computed) {
processNode(property.key, scope);
}
nextLValueNode(property.value);
});
break;
case 'ArrayPattern':
obj.elements.forEach(function(element) {
nextLValueNode(element);
});
break;
case 'RestElement':
nextLValueNode(obj.argument);
break;
case 'AssignmentPattern':
nextLValueNode(obj.left);
processNode(obj.right, scope);
break;
case 'MemberExpression':
processNode(obj, scope);
break;
default: throw new Error('Unknown type: ' + obj.type);
}
}
}
};
})(window.ejsprima = {});
<body>
<script type="text/ejs" id="demo-ejs">
<body>
<h1>Welcome <%= user.name %></h1>
<% if (admin) { %>
<a href="/admin">Admin</a>
<% } %>
<ul>
<% friends.forEach(function(friend, index) { %>
<li class="<%= index === 0 ? "first" : "" %> <%= friend.name === selected ? "selected" : "" %>"><%= friend.name %></li>
<% }); %>
</ul>
<%
console.log(user);
exampleWrite = 'some value';
%>
</body>
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/esprima/2.7.3/esprima.min.js"></script>
<script>
function runTests() {
var assertValues = function(tpl, reads, writes) {
var program = ejsprima.compile(tpl);
var values = ejsprima.extractGlobals(program);
reads = reads || [];
writes = writes || [];
reads.sort();
writes.sort();
if (!equal(reads, values.reads)) {
console.log('Mismatched reads', reads, values.reads, tpl);
}
if (!equal(writes, values.writes)) {
console.log('Mismatched writes', writes, values.writes, tpl);
}
function equal(arr1, arr2) {
return JSON.stringify(arr1.slice().sort()) === JSON.stringify(arr2.slice().sort());
}
};
assertValues('<% console.log("hello") %>', ['console']);
assertValues('<% a = 7; %>', [], ['a']);
assertValues('<% var a = 7; %>');
assertValues('<% let a = 7; %>');
assertValues('<% const a = 7; %>');
assertValues('<% a = 7; var a; %>');
assertValues('<% var a = 7, b = a + 1, c = d; %>', ['d']);
assertValues('<% try{}catch(a){a.log()} %>');
assertValues('<% try{}catch(a){a = 9;} %>');
assertValues('<% try{}catch(a){b.log()} %>', ['b']);
assertValues('<% try{}catch(a){}a; %>', ['a']);
assertValues('<% try{}catch(a){let b;}b; %>', ['b']);
assertValues('<% try{}finally{let a;}a; %>', ['a']);
assertValues('<% (function(a){a();b();}) %>', ['b']);
assertValues('<% (function(a){a();b = 8;}) %>', [], ['b']);
assertValues('<% (function(a){a();a = 8;}) %>');
assertValues('<% (function name(a){}) %>');
assertValues('<% (function name(a){});name(); %>', ['name']);
assertValues('<% function name(a){} %>');
assertValues('<% function name(a){}name(); %>');
assertValues('<% a.map(b => b + c); %>', ['a', 'c']);
assertValues('<% a.map(b => b + c); b += 6; %>', ['a', 'b', 'c'], ['b']);
assertValues('<% var {a} = {b: c}; %>', ['c']);
assertValues('<% var {a} = {b: c}; a(); %>', ['c']);
assertValues('<% var {[d]: a} = {b: c}; a(); %>', ['c', 'd']);
assertValues('<% var {[d]: a} = {b: c}; a(); %>', ['c', 'd']);
assertValues('<% var {[d + e]: a} = {b: c}; a(); %>', ['c', 'd', 'e']);
assertValues('<% var {[d + e[f = g]]: a} = {b: c}; a(); %>', ['c', 'd', 'e', 'g'], ['f']);
assertValues('<% ({a} = {b: c}); %>', ['c'], ['a']);
assertValues('<% ({a: d.e} = {b: c}); %>', ['c', 'd']);
assertValues('<% ({[a]: d.e} = {b: c}); %>', ['a', 'c', 'd']);
assertValues('<% var {a = 7} = {}; %>', []);
assertValues('<% var {a = b} = {}; %>', ['b']);
assertValues('<% var {[a]: b = (c + d)} = {}; %>', ['a', 'c', 'd']);
assertValues('<% var [a] = [b]; a(); %>', ['b']);
assertValues('<% var [{a}] = [b]; a(); %>', ['b']);
assertValues('<% [{a}] = [b]; %>', ['b'], ['a']);
assertValues('<% [...a] = [b]; %>', ['b'], ['a']);
assertValues('<% let [...a] = [b]; %>', ['b']);
assertValues('<% var [a = b] = [c]; %>', ['b', 'c']);
assertValues('<% var [a = b] = [c], b; %>', ['c']);
assertValues('<% ++a %>', ['a'], ['a']);
assertValues('<% ++a.b %>', ['a']);
assertValues('<% var a; ++a %>');
assertValues('<% a += 1 %>', ['a'], ['a']);
assertValues('<% var a; a += 1 %>');
assertValues('<% a.b = 7 %>', ['a']);
assertValues('<% a["b"] = 7 %>', ['a']);
assertValues('<% a[b] = 7 %>', ['a', 'b']);
assertValues('<% a[b + c] = 7 %>', ['a', 'b', 'c']);
assertValues('<% var b; a[b + c] = 7 %>', ['a', 'c']);
assertValues('<% a in b; %>', ['a', 'b']);
assertValues('<% "a" in b; %>', ['b']);
assertValues('<% "a" in b.c; %>', ['b']);
assertValues('<% if (a === b) {c();} %>', ['a', 'b', 'c']);
assertValues('<% if (a = b) {c();} else {d = e} %>', ['b', 'c', 'e'], ['a', 'd']);
assertValues('<% a ? b : c %>', ['a', 'b', 'c']);
assertValues('<% var a = b ? c : d %>', ['b', 'c', 'd']);
assertValues('<% for (a in b) {} %>', ['b'], ['a']);
assertValues('<% for (var a in b.c) {} %>', ['b']);
assertValues('<% for (let {a} in b) {} %>', ['b']);
assertValues('<% for ({a} in b) {} %>', ['b'], ['a']);
assertValues('<% for (var {[a + b]: c} in d) {} %>', ['a', 'b', 'd']);
assertValues('<% for ({[a + b]: c} in d) {} %>', ['a', 'b', 'd'], ['c']);
assertValues('<% for (var a in b) {a = a + c;} %>', ['b', 'c']);
assertValues('<% for (const a in b) console.log(a); %>', ['b', 'console']);
assertValues('<% for (let a in b) console.log(a); %>', ['b', 'console']);
assertValues('<% for (let a in b) {let b = 5;} %>', ['b']);
assertValues('<% for (let a in b) {let b = 5;} console.log(a); %>', ['console', 'a', 'b']);
assertValues('<% for (const a in b) {let b = 5;} console.log(a); %>', ['console', 'a', 'b']);
assertValues('<% for (var a in b) {let b = 5;} console.log(a); %>', ['console', 'b']);
assertValues('<% for (a of b) {} %>', ['b'], ['a']);
assertValues('<% for (var a of b.c) {} %>', ['b']);
assertValues('<% for (let {a} of b) {} %>', ['b']);
assertValues('<% for ({a} of b) {} %>', ['b'], ['a']);
assertValues('<% for (var {[a + b]: c} of d) {} %>', ['a', 'b', 'd']);
assertValues('<% for ({[a + b]: c} of d) {} %>', ['a', 'b', 'd'], ['c']);
assertValues('<% for (var a of b) {a = a + c;} %>', ['b', 'c']);
assertValues('<% for (const a of b) console.log(a); %>', ['b', 'console']);
assertValues('<% for (let a of b) console.log(a); %>', ['b', 'console']);
assertValues('<% for (let a of b) {let b = 5;} %>', ['b']);
assertValues('<% for (let a of b) {let b = 5;} console.log(a); %>', ['console', 'a', 'b']);
assertValues('<% for (const a of b) {let b = 5;} console.log(a); %>', ['console', 'a', 'b']);
assertValues('<% for (var a of b) {let b = 5;} console.log(a); %>', ['console', 'b']);
assertValues('<% for (var i = 0 ; i < 10 ; ++i) {} %>');
assertValues('<% for (var i = 0 ; i < len ; ++i) {} %>', ['len']);
assertValues('<% for (var i = 0, len ; i < len ; ++i) {} %>');
assertValues('<% for (i = 0 ; i < len ; ++i) {} %>', ['i', 'len'], ['i']);
assertValues('<% for ( ; i < len ; ++i) {} %>', ['i', 'len'], ['i']);
assertValues('<% var i; for ( ; i < len ; ++i) {} %>', ['len']);
assertValues('<% for (var i = 0 ; i < 10 ; ++i) {i += j;} %>', ['j']);
assertValues('<% for (var i = 0 ; i < 10 ; ++i) {j += i;} %>', ['j'], ['j']);
assertValues('<% for (const i = 0; i < 10 ; ++i) console.log(i); %>', ['console']);
assertValues('<% for (let i = 0 ; i < 10 ; ++i) console.log(i); %>', ['console']);
assertValues('<% for (let i = 0 ; i < len ; ++i) {let len = 5;} %>', ['len']);
assertValues('<% for (let i = 0 ; i < len ; ++i) {let len = 5;} console.log(i); %>', ['console', 'i', 'len']);
assertValues('<% for (var i = 0 ; i < len ; ++i) {let len = 5;} console.log(i); %>', ['console', 'len']);
assertValues('<% while(++i){console.log(i);} %>', ['console', 'i'], ['i']);
assertValues('<% myLabel:while(true){break myLabel;} %>');
assertValues('<% var a = `Hello ${user.name}`; %>', ['user']);
assertValues('<% this; null; true; false; NaN; undefined; %>', ['NaN', 'undefined']);
// Scoping
assertValues([
'<%',
'var a = 7, b;',
'let c = 8;',
'a = b + c - d;',
'{',
'let e = 6;',
'f = g + e + b + c;',
'}',
'%>'
].join('\n'), ['d', 'g'], ['f']);
assertValues([
'<%',
'var a = 7, b;',
'let c = 8;',
'a = b + c - d;',
'{',
'let e = 6;',
'f = g + e + b + c;',
'}',
'e = c;',
'%>'
].join('\n'), ['d', 'g'], ['e', 'f']);
assertValues([
'<%',
'var a = 7, b;',
'let c = 8;',
'a = b + c - d;',
'{',
'var e = 6;',
'f = g + e + b + c;',
'}',
'e = c;',
'%>'
].join('\n'), ['d', 'g'], ['f']);
assertValues([
'<%',
'var a;',
'let b;',
'const c = 0;',
'{',
'var d;',
'let e;',
'const f = 1;',
'}',
'var g = function h(i) {',
'arguments.length;',
'a(); b(); c(); d(); e(); f(); g(); h(); i();',
'};',
'%>'
].join('\n'), ['e', 'f']);
assertValues([
'<%',
'var a;',
'let b;',
'const c = 0;',
'{',
'var d;',
'let e;',
'const f = 1;',
'}',
'var g = function h(i) {};',
'arguments.length;',
'a(); b(); c(); d(); e(); f(); g(); h(); i();',
'%>'
].join('\n'), ['e', 'f', 'h', 'i']);
assertValues([
'<%',
'var a;',
'let b;',
'const c = 0;',
'{',
'var d;',
'let e;',
'const f = 1;',
'arguments.length;',
'a(); b(); c(); d(); e(); f(); g(); h(); i();',
'}',
'var g = function h(i) {};',
'%>'
].join('\n'), ['h', 'i']);
assertValues([
'<%',
'var a;',
'let b;',
'const c = 0;',
'{',
'var d;',
'let e;',
'const f = 1;',
'var g = function h(i) {',
'arguments.length;',
'a(); b(); c(); d(); e(); f(); g(); h(); i();',
'};',
'}',
'%>'
].join('\n'));
assertValues([
'<%',
'var a;',
'let b;',
'const c = 0;',
'var g = function h(i) {',
'{',
'var d;',
'let e;',
'const f = 1;',
'}',
'arguments.length;',
'a(); b(); c(); d(); e(); f(); g(); h(); i();',
'};',
'%>'
].join('\n'), ['e', 'f']);
assertValues([
'<%',
'var a;',
'let b;',
'const c = 0;',
'var g = function h(i) {',
'{',
'var d;',
'let e;',
'const f = 1;',
'arguments.length;',
'a(); b(); c(); d(); e(); f(); g(); h(); i();',
'}',
'};',
'%>'
].join('\n'));
// EJS parsing
assertValues('Hello <%= user.name %>', ['user']);
assertValues('Hello <%- user.name %>', ['user']);
assertValues('Hello <%# user.name %>');
assertValues('Hello <%_ user.name _%>', ['user']);
assertValues('Hello <%_ user.name _%>', ['user']);
assertValues('Hello <%% console.log("<%= user.name %>") %%>', ['user']);
assertValues('Hello <% console.log("<%% user.name %%>") %>', ['console']);
assertValues('<% %><%a%>', ['a']);
assertValues('<% %><%=a%>', ['a']);
assertValues('<% %><%-a_%>', ['a']);
assertValues('<% %><%__%>');
assertValues([
'<body>',
'<h1>Welcome <%= user.name %></h1>',
'<% if (admin) { %>',
'<a href="/admin">Admin</a>',
'<% } %>',
'<ul>',
'<% friends.forEach(function(friend, index) { %>',
'<li class="<%= index === 0 ? "first" : "" %> <%= friend.name === selected ? "selected" : "" %>"><%= friend.name %></li>',
'<% }); %>',
'</ul>',
'</body>'
].join('\n'), ['user', 'admin', 'friends', 'selected']);
assertValues([
'<body>',
'<h1>Welcome <%= user.name %></h1>',
'<% if (admin) { %>',
'<a href="/admin">Admin</a>',
'<% } %>',
'<ul>',
'<% friends.forEach(function(user, index) { %>',
'<li class="<%= index === 0 ? "first" : "" %> <%= user.name === selected ? "selected" : "" %>"><%= user.name %></li>',
'<% }); %>',
'</ul>',
'</body>'
].join('\n'), ['user', 'admin', 'friends', 'selected']);
console.log('Tests complete, if you didn\'t see any other messages then they passed');
}
</script>
<script>
function runDemo() {
var script = document.getElementById('demo-ejs'),
tpl = script.innerText,
js = ejsprima.compile(tpl);
console.log(ejsprima.extractGlobals(js));
}
</script>
<button onclick="runTests()">Run Tests</button>
<button onclick="runDemo()">Run Demo</button>
</body>
因此,总而言之,我相信这将使您能够准确识别
locals
的所有必需条目。 .通常,不可能识别这些对象中使用的属性。如果您不介意准确性的损失,那么您不妨只使用 RegExp。
关于javascript - 如何从我的 EJS 模板中获取属性列表?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46162102/