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

时间:2021-11-30 18:33:19

I'm storing response strings in a database in EJS form and filling out the data in Node. What I want to do is be able to use any property I want, no matter what model it comes from, then in Node, async/await those models once I have the template, based around what properties are required.

我将响应字符串以EJS格式存储在数据库中,并在Node中填写数据。我想要做的是能够使用我想要的任何属性,无论它来自哪个模型,然后在Node中,async /等待那些模型,一旦我有模板,基于所需的属性。

So if I have a template like:

所以,如果我有一个模板,如:

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

I want to be able to look at that template and extract something like:

我希望能够查看该模板并提取以下内容:

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

Or something like that.

或类似的东西。

3 个解决方案

#1


5  

If you just want to pull out simple things like user.firstName then running a RegExp over the EJS file is probably as good a way as any. Chances are you'd be looking for a specific and known set of objects and properties so you could target them specifically rather than trying to extract all possible objects/properties.

如果您只想提取像user.firstName这样的简单内容,那么在EJS文件上运行RegExp可能与任何方法一样好。您可能正在寻找一组特定且已知的对象和属性,因此您可以专门针对它们而不是尝试提取所有可能的对象/属性。

In the more general case things get difficult very quickly. Something like this is very tricky to handle:

在更一般的情况下,事情很快变得困难。这样的事情很难处理:

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

It's a silly example but it's just the tip of that particular iceberg. Whereas user is being read from the locals and is an object of interest, u could be just about anything and we can't easily draw lines connecting firstName and user via u. Similarly something like a forEach on an array or a for/in on an object will quickly make it impossible to link properties to the appropriate locals entry.

这是一个愚蠢的例子,但它只是特定冰山的一角。虽然用户正在从当地人那里阅读并且是一个感兴趣的对象,但是你可以做任何事情,我们不能轻易地画出通过u连接firstName和用户的线条。类似地,类似于数组上的forEach或对象上的for / in之类的东西很快就会无法将属性链接到适当的本地条目。

However, what we can do is identify the entries in locals, or at least something very close to that.

但是,我们可以做的是识别当地人的条目,或者至少是非常接近的条目。

Using the example of <%= user.firstName %> the identifier user could refer to one of 3 things. Firstly, it could be an entry in the locals. Secondly, it could be a property of the global object. Thirdly, it could be a variable created within the scope of the template (like u in the earlier example).

使用<%= user.firstName%>的示例,标识符用户可以参考3件事之一。首先,它可能是当地人的入口。其次,它可能是全球对象的属性。第三,它可以是在模板范围内创建的变量(如前面示例中的u)。

We can't really tell the difference between the first two cases but chances are you can separate out the globals pretty easily. Things like console and Math can be identified and discarded.

我们无法真正区分前两种情况,但很有可能你可以很容易地分离出全局数据。可以识别和丢弃控制台和数学等内容。

The third case is the tricky one, telling the difference between an entry in the locals and a variable in the template, like in this example:

第三种情况是棘手的,告诉本地的条目和模板中的变量之间的区别,如下例所示:

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

In this case users is coming directly from the locals but user is not. For us to work that out requires variable scope analysis similar to that found in an IDE.

在这种情况下,用户直接来自当地人,但用户不是。对我们来说,需要进行类似于IDE中的变量范围分析。

So here's what I tried:

所以这就是我的尝试:

  1. Compile the template to JS.
  2. 将模板编译为JS。
  3. Parse the JS into an AST using esprima.
  4. 使用esprima将JS解析为AST。
  5. Walk the AST to find all the identifiers. If they appear to be global they get returned. Here 'global' means either genuinely global or that they're an entry in the locals object. EJS uses with (locals) {...} internally so there really is no way to know which one it is.
  6. 走AST,找到所有的标识符。如果他们看起来是全球性的,他们会被退回。这里的“全球”意味着要么真正全球化,要么意味着它们是本地对象的入口。 EJS在内部使用(locals){...},所以真的无法知道它是哪一个。

I've imaginatively called the result ejsprima.

我有想象力地把结果称为ejsprima。

I haven't attempted to support all the options that EJS supports, so if you're using custom delimiters or strict mode it won't work. (If you're using strict mode you have to explicitly write locals.user.firstName in your template anyway, which is crying out to be done via a RegExp instead). It won't attempt to follow any include calls.

我没有尝试支持EJS支持的所有选项,因此如果您使用自定义分隔符或严格模式,它将无法工作。 (如果你使用的是严格模式,你必须在你的模板中明确地写一些locals.user.firstName,这很难通过RegExp来完成)。它不会尝试跟随任何包含调用。

I'd be very surprised if there aren't bugs lurking somewhere, even with some piece of basic JS syntax, but I've tested all of the nasty cases I could think of. Test cases are included.

如果没有潜伏在某处的bug,即使有一些基本的JS语法,我会感到非常惊讶,但我已经测试了所有我能想到的令人讨厌的案例。包括测试用例。

The EJS used in the main demo can be found at the top of the HTML. I've included a gratuitous example of a 'global write' just to demonstrate what they look like but I'd imagine that they aren't something you'd normally want. The interesting bit is the reads section.

主要演示中使用的EJS可以在HTML的顶部找到。我已经包含了一个“全局写作”的无偿例子,只是为了展示它们的样子,但我想它们并不是你通常想要的东西。有趣的是读取部分。

I developed this against esprima 4 but the best CDN version I could find is 2.7.3. The tests all still pass so it doesn't seem to matter too much.

我针对esprima 4开发了这个,但我能找到的最好的CDN版本是2.7.3。测试仍然通过所以它似乎并不重要。

The only code I included in the JS section of the snippet is for 'ejsprima' itself. To run that in Node you should just need to copy it across and tweak the top and bottom to correct the exports and requires stuff.

我在代码片段的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>

So, in summary, I believe this will allow you to accurately identify all the required entries for your locals. Identifying the properties used within those objects is, in general, not possible. If you don't mind the loss of accuracy then you might as well just use a RegExp.

因此,总而言之,我相信这将使您能够准确识别当地人所需的所有条目。通常,识别这些对象中使​​用的属性是不可能的。如果您不介意失去准确性,那么您也可以使用RegExp。

#2


1  

Unfortunately EJS doesn't provide functionality for parsing and extracting variable names from template. It has compile method, but this method returns a function which can be used to render a string by template. But you need to get some intermediate result, to extract variables.

遗憾的是,EJS不提供从模板解析和提取变量名称的功能。它有编译方法,但是这个方法返回一个函数,可以用来按模板渲染字符串。但是你需要得到一些中间结果来提取变量。

You can do that using Mustache template system.

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

Mustache default delimiters are {{ }}. You can replace them to custom delimiters. Unfortunately Mustache doesn't allow to define multiple delimiters (<%= %> and <% %> for example), so if you'll try to compile template which contains multiple delimiters, Mustache will throw an error. The possible solution for that is to create a function which accepts a template and delimiters, and replaces all other delimeters to something neutral. And call this function for each pair of delimiters:

Mustache默认分隔符为{{}}。您可以将它们替换为自定义分隔符。不幸的是,Mustache不允许定义多个分隔符(例如<%=%>和<%%>),因此如果您尝试编译包含多个分隔符的模板,那么Mustache将抛出错误。可能的解决方案是创建一个接受模板和分隔符的函数,并将所有其他分隔符替换为中性的。并为每对分隔符调用此函数:

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

Below the simple variant which works only with one pair of delimiters:

在仅与一对分隔符一起使用的简单变体下面:

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']

#3


-1  

I think res.locals is what you are looking for in this case,

我认为在这种情况下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');
})

In any ejs file, we can use the properties as we like,

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

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

#1


5  

If you just want to pull out simple things like user.firstName then running a RegExp over the EJS file is probably as good a way as any. Chances are you'd be looking for a specific and known set of objects and properties so you could target them specifically rather than trying to extract all possible objects/properties.

如果您只想提取像user.firstName这样的简单内容,那么在EJS文件上运行RegExp可能与任何方法一样好。您可能正在寻找一组特定且已知的对象和属性,因此您可以专门针对它们而不是尝试提取所有可能的对象/属性。

In the more general case things get difficult very quickly. Something like this is very tricky to handle:

在更一般的情况下,事情很快变得困难。这样的事情很难处理:

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

It's a silly example but it's just the tip of that particular iceberg. Whereas user is being read from the locals and is an object of interest, u could be just about anything and we can't easily draw lines connecting firstName and user via u. Similarly something like a forEach on an array or a for/in on an object will quickly make it impossible to link properties to the appropriate locals entry.

这是一个愚蠢的例子,但它只是特定冰山的一角。虽然用户正在从当地人那里阅读并且是一个感兴趣的对象,但是你可以做任何事情,我们不能轻易地画出通过u连接firstName和用户的线条。类似地,类似于数组上的forEach或对象上的for / in之类的东西很快就会无法将属性链接到适当的本地条目。

However, what we can do is identify the entries in locals, or at least something very close to that.

但是,我们可以做的是识别当地人的条目,或者至少是非常接近的条目。

Using the example of <%= user.firstName %> the identifier user could refer to one of 3 things. Firstly, it could be an entry in the locals. Secondly, it could be a property of the global object. Thirdly, it could be a variable created within the scope of the template (like u in the earlier example).

使用<%= user.firstName%>的示例,标识符用户可以参考3件事之一。首先,它可能是当地人的入口。其次,它可能是全球对象的属性。第三,它可以是在模板范围内创建的变量(如前面示例中的u)。

We can't really tell the difference between the first two cases but chances are you can separate out the globals pretty easily. Things like console and Math can be identified and discarded.

我们无法真正区分前两种情况,但很有可能你可以很容易地分离出全局数据。可以识别和丢弃控制台和数学等内容。

The third case is the tricky one, telling the difference between an entry in the locals and a variable in the template, like in this example:

第三种情况是棘手的,告诉本地的条目和模板中的变量之间的区别,如下例所示:

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

In this case users is coming directly from the locals but user is not. For us to work that out requires variable scope analysis similar to that found in an IDE.

在这种情况下,用户直接来自当地人,但用户不是。对我们来说,需要进行类似于IDE中的变量范围分析。

So here's what I tried:

所以这就是我的尝试:

  1. Compile the template to JS.
  2. 将模板编译为JS。
  3. Parse the JS into an AST using esprima.
  4. 使用esprima将JS解析为AST。
  5. Walk the AST to find all the identifiers. If they appear to be global they get returned. Here 'global' means either genuinely global or that they're an entry in the locals object. EJS uses with (locals) {...} internally so there really is no way to know which one it is.
  6. 走AST,找到所有的标识符。如果他们看起来是全球性的,他们会被退回。这里的“全球”意味着要么真正全球化,要么意味着它们是本地对象的入口。 EJS在内部使用(locals){...},所以真的无法知道它是哪一个。

I've imaginatively called the result ejsprima.

我有想象力地把结果称为ejsprima。

I haven't attempted to support all the options that EJS supports, so if you're using custom delimiters or strict mode it won't work. (If you're using strict mode you have to explicitly write locals.user.firstName in your template anyway, which is crying out to be done via a RegExp instead). It won't attempt to follow any include calls.

我没有尝试支持EJS支持的所有选项,因此如果您使用自定义分隔符或严格模式,它将无法工作。 (如果你使用的是严格模式,你必须在你的模板中明确地写一些locals.user.firstName,这很难通过RegExp来完成)。它不会尝试跟随任何包含调用。

I'd be very surprised if there aren't bugs lurking somewhere, even with some piece of basic JS syntax, but I've tested all of the nasty cases I could think of. Test cases are included.

如果没有潜伏在某处的bug,即使有一些基本的JS语法,我会感到非常惊讶,但我已经测试了所有我能想到的令人讨厌的案例。包括测试用例。

The EJS used in the main demo can be found at the top of the HTML. I've included a gratuitous example of a 'global write' just to demonstrate what they look like but I'd imagine that they aren't something you'd normally want. The interesting bit is the reads section.

主要演示中使用的EJS可以在HTML的顶部找到。我已经包含了一个“全局写作”的无偿例子,只是为了展示它们的样子,但我想它们并不是你通常想要的东西。有趣的是读取部分。

I developed this against esprima 4 but the best CDN version I could find is 2.7.3. The tests all still pass so it doesn't seem to matter too much.

我针对esprima 4开发了这个,但我能找到的最好的CDN版本是2.7.3。测试仍然通过所以它似乎并不重要。

The only code I included in the JS section of the snippet is for 'ejsprima' itself. To run that in Node you should just need to copy it across and tweak the top and bottom to correct the exports and requires stuff.

我在代码片段的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>

So, in summary, I believe this will allow you to accurately identify all the required entries for your locals. Identifying the properties used within those objects is, in general, not possible. If you don't mind the loss of accuracy then you might as well just use a RegExp.

因此,总而言之,我相信这将使您能够准确识别当地人所需的所有条目。通常,识别这些对象中使​​用的属性是不可能的。如果您不介意失去准确性,那么您也可以使用RegExp。

#2


1  

Unfortunately EJS doesn't provide functionality for parsing and extracting variable names from template. It has compile method, but this method returns a function which can be used to render a string by template. But you need to get some intermediate result, to extract variables.

遗憾的是,EJS不提供从模板解析和提取变量名称的功能。它有编译方法,但是这个方法返回一个函数,可以用来按模板渲染字符串。但是你需要得到一些中间结果来提取变量。

You can do that using Mustache template system.

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

Mustache default delimiters are {{ }}. You can replace them to custom delimiters. Unfortunately Mustache doesn't allow to define multiple delimiters (<%= %> and <% %> for example), so if you'll try to compile template which contains multiple delimiters, Mustache will throw an error. The possible solution for that is to create a function which accepts a template and delimiters, and replaces all other delimeters to something neutral. And call this function for each pair of delimiters:

Mustache默认分隔符为{{}}。您可以将它们替换为自定义分隔符。不幸的是,Mustache不允许定义多个分隔符(例如<%=%>和<%%>),因此如果您尝试编译包含多个分隔符的模板,那么Mustache将抛出错误。可能的解决方案是创建一个接受模板和分隔符的函数,并将所有其他分隔符替换为中性的。并为每对分隔符调用此函数:

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

Below the simple variant which works only with one pair of delimiters:

在仅与一对分隔符一起使用的简单变体下面:

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']

#3


-1  

I think res.locals is what you are looking for in this case,

我认为在这种情况下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');
})

In any ejs file, we can use the properties as we like,

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

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