如何从我的EJS模板获取属性列表?

我将响应string存储在EJSforms的数据库中,并填写Node中的数据。 我想要做的是能够使用任何我想要的属性,无论它来自哪种模型,然后在Node中,asynchronous/等待这些模型,一旦我有模板,基于什么属性是必需的。

所以,如果我有一个像这样的模板:

"Hello <%=user.firstName%>." 

我希望能够看到该模板,并提取像这样的东西:

 ejsProperties = ["user", "user.firstName"] 

或类似的东西。

如果你只是想拉出像user.firstName这样简单的东西,那么通过EJS文件运行一个RegExp可能是一个好的方法。 机会是你会寻找一个特定的和已知的对象和属性集,所以你可以专门针对他们,而不是试图提取所有可能的对象/属性。

在更一般的情况下,事情变得很困难。 像这样的事情是非常棘手的处理:

 <% var u = user; %><%= u.firstName %> 

这是一个愚蠢的例子,但它只是特定冰山的一angular。 而user正在从locals读取,是一个感兴趣的对象, u可能只是任何事情,我们不能很容易地绘制连接firstNameuser通过u 。 类似于数组上的forEach或者对象上的for/in ,会很快导致将属性链接到适当的locals条目。

但是,我们可以做的是确定locals的条目,或至less是非常接近的条目。

使用<%= user.firstName %>的例子,标识符user可以引用3件事情之一。 首先,这可能是locals一个入口。 其次,它可能是全球客体的一个属性。 第三,它可能是一个在模板范围内创build的variables(如前面例子中的u )。

我们无法区分前两种情况,但是您很容易就可以分离出全局variables。 consoleMath等东西可以被识别和丢弃。

第三种情况是非常棘手的,比如在locals一个条目和模板中的一个variables之间的区别,就像这个例子:

 <% users.forEach(function(user) { %> <%= user.firstName %> <% }); %> 

在这种情况下, users直接来自localsuser不是。 对于我们来说,这需要variables范围分析,类似于IDE中的variables范围分析。

所以这是我的尝试:

  1. 编译模板到JS。
  2. 使用esprima将JSparsing为AST。
  3. 走AST查找所有的标识符。 如果他们似乎是全球性的,他们会得到回报。 这里的“全球”意味着真正的全球化,或者说它们是locals对象。 EJS with (locals) {...}内部使用,所以真的无法知道它是哪一个。

我有想象力地称为结果ejsprima

我没有试图支持EJS支持的所有选项,所以如果你使用自定义分隔符或严格模式,它将不起作用。 (如果你使用的是严格模式,那么你必须在你的模板中显式编写locals.user.firstName ,而不是通过RegExp来完成)。 它不会尝试跟随任何include呼叫。

如果没有任何漏洞存在,即使使用了一些基本的JS语法,我也会感到非常惊讶,但是我已经testing了所有我能想到的令人讨厌的情况。 包括testing用例。

在主演示中使用的EJS可以在HTML的顶部find。 我列举了一个“全球写作”的无理例证,只是为了展示他们的样子,但我想他们不是你通常想要的东西。 有趣的是reads部分。

我开发这个反对esprima 4,但我能find的最好的CDN版本是2.7.3。 testing全部仍然通过,所以它似乎并不太重要。

我包含在代码片段的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: de} = {b: c}); %>', ['c', 'd']); assertValues('<% ({[a]: de} = {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('<% ++ab %>', ['a']); assertValues('<% var a; ++a %>'); assertValues('<% a += 1 %>', ['a'], ['a']); assertValues('<% var a; a += 1 %>'); assertValues('<% ab = 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 bc; %>', ['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 bc) {} %>', ['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 bc) {} %>', ['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> 

不幸的是,EJS不提供从模板分析和提取variables名称的function。 它有compile方法,但是这个方法返回一个可以用来通过模板呈现string的函数 。 但是你需要得到一些中间结果来提取variables。

你可以使用Mustache模板系统来做到这一点。

小胡子的默认分隔符是{{ }} 。 您可以将它们replace为自定义分隔符 。 不幸的是,胡须不允许定义多个分隔符(例如<%= %><% %> ),所以如果您尝试编译包含多个分隔符的模板,胡须就会引发错误。 可能的解决scheme是创build一个接受模板和分隔符的函数,并将其他所有的分隔符replace为中性。 并为每对分隔符调用此函数:

 let vars = []; vars.concat(parseTemplate(template, ['<%', '%>'])); vars.concat(parseTemplate(template, ['<%=', '%>'])); ... let uniqVars = _.uniq(vars); 

在仅与一对分隔符一起工作的简单变体之下:

 let _ = require('lodash'); let Mustache = require('Mustache'); let template = 'Hello <%= user.firstName %> <%= user.lastName %> <%= date %>'; let customTags = ['<%=', '%>']; let tokens = Mustache.parse(template, customTags); let vars = _.chain(tokens) .filter(token => token[0] === 'name') .map(token => { let v = token[1].split('.'); return v; }) .flatten() .uniq() .value(); console.log(vars); // prints ['user', 'firstName', 'lastName', 'date'] 

我认为res.locals是你在这种情况下寻找的,

 app.set('view engine', 'ejs'); var myUser = { user : { username: 'myUser', lastName: 'userLastName', location: 'USA' } } app.use(function(req, res, next){ res.locals = myUser; next(); }) app.get('/', function(req, res){ res.render('file.ejs'); }) 

在任何ejs文件中,我们可以使用我们喜欢的属性,

  <body> <h3>The User</h3> <p><%=user.username%></p> <p><%=user.lastName%></p> <p><%=user.location%></p> </body>