Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to implement a template engine with JavaScript

2025-01-18 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

This article is about how to use JavaScript to implement a template engine. The editor thinks it is very practical, so share it with you as a reference and follow the editor to have a look.

Functional analysis

A template engine, in my opinion, consists of two core functions, one for parsing the template language into ast (Abstract Syntax Tree). Another is to recompile ast into html.

First explain what ast is, what is known can be ignored.

An abstract grammar tree (abstract syntax tree or abbreviated as AST), or a grammar tree (syntax tree), is a tree representation of the abstract syntax structure of a source code, specifically the source code of a programming language. Each node on the tree represents a structure in the source code. Grammar is "abstract" because the grammar here does not represent every detail that appears in the real grammar. For example, nested parentheses are implicit in the structure of the tree and are not presented as nodes, while conditional jump statements such as if-condition-then can be represented by nodes with two branches.

Before implementing the specific logic, decide which functions of tag to implement, in my opinion, for,if else,set,raw and basic variable output, with these, the template engine is basically sufficient. In addition to tag, filter functionality is also required.

Build AST

We need to parse the template language into one syntax node after another, such as the following template language:

{if test > 1} {{test}} {endif%}

Obviously, div will be parsed into a text node, followed by a block-level node if, then a variable quantum node under the if node, and then a text node. The ast parsed into this template by json can be represented as:

[{type: 1, text:''}, {type: 2, tag: 'if', item:' test > 1, children: [{type: 3, item: 'test'}]}, {type: 1, text:''}]

Basically, it can be divided into three types, one is plain text node, one is block-level node, and the other is variable node. In that case, you only need to find the text of each node and abstract it into an object. Generally speaking, nodes are found according to the template syntax. For example, the above block-level nodes and variable nodes must start with {% or {{, then you can start with these two key characters:

.. const matches = str.match (/ {{| {% /); const isBlock = matches [0] = ='{%'; const endIndex = matches.index;...

Through the above code, you can get the {{or {% position at the front of the text.

Now that you have obtained the location of * non-text nodes, the previous positions of this node are all text nodes, so you can get * nodes, that is, the above.

After obtaining the div text node, we can also know that the * key characters obtained are {%, that is, the endIndex above is the index we want. Remember to update the remaining characters by updating them directly through slice:

/ / 2 is the length of {% str = str.slice (endIndex + 2)

At this point, we can know that the current key character matched is {%, then his closure must be%}, so we can pass it again.

Const expression = str.slice (0, str.indexOf ('%}'))

Got the string if test > 1. Then through the regular / ^ if\ s + ([\ s\ S] +) $/ match, we can know that this string is the label of if, and we can get a capture group of test > 1, and then we can create our second node, the block node of if.

Because if is a block-level node, when we continue to match, all nodes before we encounter {% endif%} are children of the if node, so when we create the node, we give it a children array attribute to save the child nodes.

Then repeat the above operation to get the next {% and the position of {{, which is similar to the logic above. After getting the position of {{and then judging the location of}}, you can create a third node, the variable node of test, and push to the list of child nodes of the if node.

After creating the variable node, continue to repeat the above, you will be able to get the closed node {% endif%}. When you encounter the node after that node, it cannot be saved to the list of child nodes of the if node. This is followed by another text node.

The relatively complete implementation is as follows:

Const root = []; let parent;function parse (str) {const matches = str.match (/ {{| {% /); const isBlock = matches [0] = ='{%'; const endIndex = matches.index;const chars = str.slice (0, matches? EndIndex: str.length); if (chars.length) {... Create text node} if (! matches) return; str = str.slice (endIndex + 2); const leftStart = matches [0]; const rightEnd = isBlock?'%}':'}}'; const rightEndIndex = str.indexOf (rightEnd); const expression = str.slice (0, rightEndIndex) if (isBlock) {. Create a block-level node elparent = el;} else {... Create a variable node el} (parent? Parent.children: root) .push (el); parse (str.slice (rightEndIndex + 2));}

Of course, there are other things to consider when implementing, for example, if a text is {% {test}}, you have to consider {% interference and so on. There is also, for example, the processing of else and elseif nodes, which need to be associated with the if tag, and this also requires special handling. But probably the logic is basically the above.

Combined html

After the ast is created, when you want to render the html, you just need to traverse the syntax tree and do different processing according to the node type.

For example, if it is a text node, you can simply html + = el.text. If it is an if node, then judge the expression, such as test > 1 above, there are two ways to evaluate the expression, one is eval, the other is new Function, eval will have security problems, so do not consider it, but use new Function to implement. The same is true for the calculation of variable nodes, which is implemented with new Function.

The specific implementation after encapsulation is as follows:

Function computed_Expression (obj, expression) {const methodBody = `return (${expression}) `; const funcString = obj? `with (_ _ obj__) {${methodBody}}`: methodBody; const func = new Function ('_ obj__', funcString); try {let result = func (obj); return (result = undefined | | result = null)?': result;} catch (e) {return';}}

Using with, you can associate objects with statements executed in function, such as

With ({a: '123'}) {console.log (a); / / 123}

Although with is not recommended when writing code because it makes it impossible for the js engine to optimize the code, it is suitable for this kind of template compilation and is much more convenient. Including render function in vue is also wrapped in with. However, nunjucks does not use with, it parses expressions on its own, so in the template syntax of nunjucks, we need to follow its specifications, such as the simplest conditional expressions, if you use with, write {{test? 'good':' bad'}}, but it is written as'{'good' if test else' bad'}} in nunjucks.

Anyway, each has its own. Okay.

Achieve multi-level scope

When converting ast to html, a common scenario is multi-level scopes, such as nesting a for loop within a for loop. And how to do this scope segmentation, in fact, is also very simple, is through recursion.

For example, my method for dealing with an ast tree is named processAst (ast, scope), and the original scope is

{list: [{subs: [1, 2, 3]}, {subs: [4, 5, 6]}]}

Then processAst can be implemented as follows:

Function processAst (ast, scope) {... if (ast.for) {const list = scope [ast.item]; / / ast.item is naturally the key of the list, such as the listlist.forEach above (item = > {processAst (ast.children, Object.assign ({}, scope, {[ast.key]: item, / / ast.key is key} in for key in list)}.}

The scope can be passed on all the time through a simple recursion.

Filter function realization

After the implementation of the above functions, the component already has the basic template rendering capabilities, but when using the template engine, there is another very common function is filter. Generally speaking, filter is used like this {{test | filter1 | filter2}}. The implementation of this part is also mentioned. I refer to the parsing method of vue, which is quite interesting.

Let me give you an example:

{{test | filter1 | filter2}}

When you build an AST, you can get the test | filter1 | filter2, and then we can simply get the strings filter1 and filter2. At first, my approach was to throw these filter strings into the filters array of ast nodes, and then take them out one by one during rendering.

However, I later felt that for the sake of performance, the work that can be done in the AST phase should not be put into the rendering phase. Therefore, it is changed to the method combination of vue. That is, change the above string to:

_ $f ('filter2', _ $f (' filter1', test))

Wrap it with a method in advance, and when rendering, you no longer need to loop to get the filter and execute it. The specific implementation is as follows:

Const filterRE = / (?:\ |\ s *\ w +\ s *) + $/; const filterSplitRE = /\ s *\ |\ s function processFilter (expr, escape) {let result = expr; const matches = expr.match (filterRE); if (matches) {const arr = matches [0] .trim (). Split (filterSplitRE); result = expr.slice (0, matches.index); / / add filter method wrappingutils.forEach (arr, name = > {if (! name) {return) } / / do not escape if has safe filter if (name = = 'safe') {escape = false;return;} result = `_ $f (' ${name}', ${result})`;} return escape? `_ $f ('escape', ${result})`: result;}

There is another one above is the processing of safe, if there is a filter of safe, there will be no escape. After this is done, variable with filter will change to the form of _ $f ('filter2', _ $f (' filter1', test)). Therefore, the previous computedExpression method also needs to be modified.

Function processFilter (filterName, str) {const filter = filters [filterName] | | globalFilters [filterName]; if (! filter) {throw new Error (`unknown filter ${filterName} `);} return filter (str);} function computed_Expression (obj, expression) {const methodBody = `return (${expression})`; const funcString = obj? `with (_ o) {$methodBody} `: methodBody; const func = new Function ('_ $oasis,'_ $fallow, funcString) Try {const result = func (obj, processFilter); return (result = undefined | | result = null)?': result;} catch (e) {/ / only catch the not defined errorif (e.message.indexOf ('is not defined') > = 0) {return';} else {throw e;}

In fact, it is also very simple, that is, in new Function, you can pass in one more method to obtain filter, and then the variable with filter can be recognized and parsed normally.

Thank you for reading! On "how to use JavaScript to achieve a template engine" this article is shared here, I hope the above content can be of some help to you, so that you can learn more knowledge, if you think the article is good, you can share it out for more people to see it!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report