Introduction to the
Template engine, in fact, is a template and data output results of a tool.
We’re going to develop a tool to convert template files into what we actually want to use, and that tool is a template engine. We pass the content of the template file as a string to the template engine, and the template engine parses the string according to certain syntax, and then returns a function. When we execute the function, we pass the data in and get the new string based on the template and data. In the end, what we want to do with the string depends on the requirements. If we’re using the front-end template generation, we can append the content with the innerHTML property of the DOM.
There are a lot of front-end template engines out there, and there are a lot of syntax features, but in industry terms, what we want to implement is a string-based template engine.
The process is briefly summarized as follows:
-- -- -- -- -- -- -- -- > > input to the template engine generating function -- -- -- - > the data as a parameter, to perform this function -- -- -- -- > outputCopy the code
Pros and cons
- This template engine can be used on either end, plug and play on both ends, not just in the syntax of generating content, as long as the content is string text. For example, if you want to create a CSS location file based on the size and position of the image in the Merge Sprite graphics tool, you can also use this engine to generate the CSS location file without having to write another engine.
- The template engine needs to re-render the template for changes to the data, so the initial rendering and subsequent template updates cost the same amount of resources.
- When applied to the front end, the template engine relies on innerHTML and has injection problems.
demand
This time, we want to implement a string-based template engine. Provide as simple a way to use it as possible, such as the following:
Var HTML = window.parse('
${content}
', { content: 'june' }); // const parse = require(' TPL '); var html = parse('
${content}
', {
content: 'june'
});
Copy the code
And you want to provide at least four of the following syntax:
conditional
{if condition1}
// code1
{elseif condition2}
// code2
{else}
// code3
{/if}
Copy the code
Array traversal
{list array as item} // code // PS: insert a variable item_index, which points to the item's sequence number {/list}Copy the code
Variable definitions
{var var1 = 1}
Copy the code
The interpolation
/ / direct interpolation ${var1} / / the use of filter interpolation way ${var1 | filter1 | filter2: var2, var3}Copy the code
starts
STEP 1
According to the requirements set before, we first implement an external interface, the code is as follows:
'use strict'; Var __PARSE__ = (function() {/** * defaultFilter */ const defaultFilter = {// some code}; // let doParseTemplate(content, data, filter) {// some code}; return function(content, data, filter) { try { data = data||{}; filter = Object.assign({}, defaultFilter, filter); // let f = doParseTemplate(content, data, filter); return f(data, filter); } catch(ex) { return ex.stack; }}; }) (); if(typeof module ! == 'undefined' && typeof exports === 'object') { module.exports = __PARSE__; } else { window.parse = __PARSE__; }Copy the code
Here, f is the generated function, which I named doParseTemplate and takes three arguments, content is the string content of the template file we input, data is the data we pass in, and filter is the filter that can be passed in from the template. The doParseTemplate function is not yet implemented, so let’s implement it.
STEP 2
To generate a usable Function, we call new Function(‘DATA’, ‘FILTER’, content); Construct a function where the content is the string content of the function body.
Let’s set the structure of the function f to be generated as follows:
function(DATA, FILTER) { try { var OUT = []; // other code return out.join (''); // other code return out.join (''); } catch(e) { throw new Error('parse template error! '); }}Copy the code
In fact, the parts of the annotation that handle variables, handle filters, and handle content are determined by the external incoming, so only this part is mutable; the rest of the code is fixed. To do this, we can use arrays to hold the relevant content, and then leave a placeholder in the variable section, which can be inserted when parsing to process variables, process filters, and process content sections. The code is as follows:
let doParseTemplate = function(content, data, filter) { content = content.replace(/\\t/g, ' ').replace(/\\n/g, '\\\\n').replace(/\\r/g, '\\\\r'); Var struct = ['try {var OUT = [];', '', // place placeholder 'return out.join (\\'\\'); } catch(e) { throw new Error("parse template error!"); }' ]; // some code return new Function('DATA', 'FILTER', struct.join('')); }Copy the code
Now that the fixed structure is in place, we need to deal with the template-related stuff, appending content to the place where the generator placeholder is placed. Var a = 1; var a = 1; var a = 1; Something like this:
doParseTemplate = function(content, data) { content = content.replace(/\\t/g, ' ').replace(/\\n/g, '\\\\n').replace(/\\r/g, '\\\\r'); // initialize the template generator structure let out = []; Let struct = [' try {var OUT = []; ', ' ', / / place template generator placeholder 'return OUT. Join (\ \' \ \ ');} the catch (e) {throw e;} ']. // Initialize the template variable let vars = []; Object.keys(data).forEach((name) => { vars.push(`var ${name} = DATA['${name}']; `); }); out.push(vars.join('')); // Initialize filters let filters = ['var filters = {};']; Object.keys(filter).forEach((name) => { if(typeof filter[name] === 'function') { filters.push(`FILTERS['${name}'] = FILTER['${name}']; `); }}); out.push(filters.join('')); Struct [1] = out.join("); struct[1] = out.join("); return new Function('DATA', 'FILTER', struct.join('')); }Copy the code
As mentioned above, the values needed to process variables and FILTERS are obtained directly from the DATA and FILTER variables passed in. The important point to note is that FILTERS are stored in a separate FILTER object to prevent unnecessary effects of changes to the FILTER object passed in. Some code for parse content will be used to parse the template.
// let beg = 0; // let stmbeg = 0; // let stmend = 0; // let len = content.length; let preCode = ''; // let endCode = ''; // let stmJs = ''; While (beg < len) {/* start */ stmbeg = content.indexof ('{', beg); While (content.charat (stmbeg + 1) === '\\\\') {// Stmbeg = content.indexof ('{', stmbeg + 1); } if(stmbeg === -1) {endCode = content.substr(beg); out.push('OUT.push(\\'' + endCode + '\\'); '); break; } /* End */ stmend = content.indexof ('}', stmbeg); While (content.charat (stmene-1) === '\\\\') {stmend = content.indexof ('}', stmend + 1); } if(stmend === -1) {// No break; } preCode = content.substring(beg, stmbeg); if(content.charAt(stmbeg - 1) === 'Copy the code
) {/ / in view of the state variable out. Push (` out. Push (\ \ ‘${preCode. Substr (0, preCode. Length – 1)} \ \’); `); stmJs = content.substring(stmbeg + 1, stmend); // handle filter let TMP = “; StmJs. Split (‘ | ‘). The forEach ((item, index) = __JJ_GT_JJ__ {if (index = = = 0) {/ / variables, mandatory transcoding TMP = item; } else {// filter let farr = item.split(‘:’); tmp = `FILTERS[‘${farr[0]}’](${tmp}`; If (farr [1]) {/ / filter with variable farr [1]. The split (‘, ‘). The forEach ((fitem) = __JJ_GT_JJ__ {TMP = ` ${TMP}, ${fitem} `; }); } tmp = `${tmp})`; // append end}}); out.push(`OUT.push((${tmp}).toString()); `); } else {/ / for js statements out. Push (` out. Push (\ \ “${preCode} \ \”); `); stmJs = content.substring(stmbeg + 1, stmend); out.push(transStm(stmJs)); } beg = stmend + 1; }
For parsing the contents of the template, the while loop is used directly because the syntax is relatively simple. In the syntax we defined above, the structure-dependent syntax is surrounded by {and}, and interpolation is ${and}, so the two syntax needs to be treated separately. The judgment of the whole process is as follows:
- Search statement start character {;
- Check whether {is preceded by an escape character \;
- Search statement terminator};
- Check if} is preceded by an escape character \;
- Check whether {is preceded by the value sign $;
- Extract the statement contents, that is, the contents inside {and};
- Put the contents of the statement not in the cache before {or ${into the cache;
- Parse the statement and put the parse result into the cache;
- Loop the above process from 1 to 8 until the statement start character {is not found, then it is judged as the end, and the rest of the content is put into the cache;
- Stores the contents of the current cache into the array to be output.
The cache mentioned above is the OUT array in the code above. When the template content is iterated, the cache is merged into a string, which is appended to the end of the placeholder. TransStm, the function used for statement parsing, will now be implemented.
STEP 3
The transStm function is simpler to implement because the syntax set in our requirements is not complex. The code is as follows:
*/ let transStm = function(stmJs) {stmJs = stmjs.trim (); for(let item of regmap) { if(item.reg.test(stmJs)) { return (typeof item.val === 'function') ? stmJs.replace(item.reg, item.val) : item.val; }}};Copy the code
As mentioned above, the content in the statement is matched one by one with the re. When the statement belonging to a certain rule is matched, it is processed and the result is returned. For example, if I have a statement {if a > 1}, and the re matches it, it will match the if statement in the condition judgment, and then it will process the js code if(a > 1) {and return it. The {/if} statement is processed as} and returns. So the following code:
{if a > 1}.css{margin: 0; }{/if}Copy the code
Will be processed into:
if(a > 1) { out.push('.css{margin: 0; } '); // Here is the output template content}Copy the code
The regex and return processing for syntactic matching are as follows:
/ * * grammar regular * / const regmap = [/ / if statement beginning {reg: / ^ if \ \ s + (. +)/I, val: (all, condition) = > {return ` if (${condition} {`;}}, / / elseif statements begin {reg: / ^ elseif \ \ s + (. +)/I, val: (all, condition) => {return '} else if(${condition}) {'}}, // else statement end {reg: /^else/ I, val: '} else {}, / / end of the if statement {reg: / ^ \ \ / \ \ s * if/I, val: '}}, / / the list statements begin {reg: / ^ list \ \ s + ([\ \ s] +) \ \ s + as \ \ s + ([\ \ s] +)/I, val: (all, arr, item) => {return `for(var __INDEX__=0;__INDEX__<${arr}.length; __index__++) {var="" ${item}="${arr}[__INDEX__];var" ${item}_index="__INDEX__;`; }}," List statements end = "{" reg:" "^ \ \ = =" "\ \ s * list =" I, "" =" "val: =" "'}}," "var = = =" "" "statements ^ var \ \ s + (. +) = "" (all, =" "expr) =" "> {return `var ${expr};`;}} ];Copy the code
The reg field is a regular expression. If the match is successful, the value of the val field is executed or directly returned.
STEP 4
If you look closely at the code posted above, you’ll notice a variable called defaultFilter that defines the filters that the template engine needs to come with. Ejs, ejS, ejS, ejS, ejS, ejS, ejS, ejS, ejS, ejS, ejS, ejS, EJS, EJS
/ * * * the default filter * / const defaultFilter = {/ / prevent injection with the escape: (STR) = > {/ / injection prevention transcoding mapping table var escapeMap = {' < ':' < ', = "" =" "> ': '>', '&': '&', ' ': ' ', '"': '"', "'": ''', '\\n': '
', '\\r': '' }; return str.replace(/\\<|\\>|\\&|\\r|\\n|\\s|\\'|\\"/g, (one) => { return escapeMap[one]; }); }};Copy the code
The usage is very simple, when we have a variable a, the content is zero
This is because we often insert the template engine’s content directly into the node with innerHTML, whereas if we use this variable directly like ${a}, the page will show only a red.
In order to prevent such injection, I implemented a call on the escape of the filter, will use the way to ${a | escape} can be special symbol of escape, directly on the page displays the contents of a variable a
.
The end of the
At this point, a complete string-based template engine is complete. The above code is written using some of the features of es6 syntax. If compatibility is required, Babel can be used to convert the code to ES5 syntax.
As mentioned earlier, the biggest advantage of string-based template engines is the syntax freedom, you can do not care about the type of template, you can write a CSS file template, you can write an HTML file template, as long as the corresponding template will have the corresponding output, and the front and back end can be shared.
If you want to see the full code, please click here.