Ejs is a template with a long history and is characterized by simplicity, good performance and wide use. Although it is not as popular as vUE and React, it still has occasions to use and value to learn. Here I will introduce the source code of ejS project. See the readme for your project, or here.
Philosophy EJS is a string template engine, generating is a string, in fact, can be used in many places, as long as it is dynamically generated string, can be used. The idea is that template + data => final string. A template is a string format containing variable parts and fixed parts, which are controlled by data. Reference other templates by using include methods. This model is more in line with the needs of front-end development.
Some of the concepts
template
: templates, such as this chestnut 👇.
<% if (user) { %>
<h2><% = user.name% ></h2>
<%} % >
Copy the code
data
The data corresponding to the template has all the variables used in the template, such as the above chestnut must haveuser.name
This data.option
: template configuration item. There are these 👇 :cache
Compiled functions are cached, requiresfilename
filename
The name of the file being rendered. Not required if you are usingrenderFile()
. Used bycache
to key caches, and for includes.root
Set project root for includes with an absolute path (/file.ejs).context
Function execution contextcompileDebug
Whenfalse
no debug instrumentation is compiledclient
Whentrue
, compiles a function that can be rendered in the browser without needing to load the EJS Runtime (ejs.min.js).delimiter
Character to use with angle brackets for open/closedebug
Output generated function bodystrict
When set totrue
, generated function is in strict mode_with
Whether or not to usewith() {}
constructs. Iffalse
then the locals will be stored in thelocals
object. Set tofalse
in strict mode.localsName
Name to use for the object storing local variables when not usingwith
Defaults tolocals
rmWhitespace
Remove all safe-to-remove whitespace, including leading and trailing whitespace. It also enables a safer version of- % >
line slurping for all scriptlet tags (it does not strip new lines of tags in the middle of a line).escape
The escaping function used with< % =
construct. It is used in rendering and is.toString()
ed in the generation of client functions. (By default escapes XML).outputFunctionName
Set to a string (e.g., ‘echo’ or ‘print’) for a function to print output inside scriptlet tags.async
Whentrue
, EJS will use an async function for rendering. (Depends on async/await support in the JS runtime.
compile
Compile the function, convert template and option into a function, inject data into the function, and generate the final string, not necessarily HTML, but various strings.render
Render function that converts template, data, and option directly to the final string.
The main process EJS engine implementation idea is to configure the template into a rendering function, and then through the data to generate strings. The process of turning templates into rendering functions is compile. Its main job is to generate a string from the Function’s input and body, and then generate the Function from the Function class. The execution process is as follows:
- Cut templates based on regular expressions, for example
{ key1 = <%= key1 %>, 2key1 = <%= key1+key1 %> }
Will be cut into[ '{ key1 = ', '<%=', ' key1 ', '%>', ', 2key1 = ', '<%=', ' key1+key1 ', '%>', ' }' ]
- From the cut data, generate the execution steps in the render function. In the chestnut above, the procedure is
'; __append("{ key1 = ")\n ; __append(escapeFn( key1 ))\n ; __append(", 2key1 = ")\n ; __append(escapeFn( key1+key1 ))\n ; __append(" }")\n'
Copy the code
- Assembler the function, which generates the string of the function body by prepend+ the execution step +append, and finally generates the function header, the function body, and the rendering function.
opts.localsName + ', escapeFn, include, rethrow'
Copy the code
var __output = [], __append = __output.push.bind(__output);
with (locals || {}) {
; __append("{ key1 = "); __append(escapeFn( key1 )) ; __append(", 2key1 = "); __append(escapeFn( key1+key1 )) ; __append("}")}return __output.join("");
Copy the code
function (data) {
var include = function (path, includeData) {
var d = utils.shallowCopy({}, data);
if (includeData) {
d = utils.shallowCopy(d, includeData);
}
return includeFile(path, opts)(d);
};
return fn.apply(opts.context, [data || {}, escapeFn, include, rethrow]);
}
Copy the code
React generates results using rendering functions and data. However, EJS does not support nesting directly. Instead, it calls the rendering function of the subtemplate through the include method.
Some details render function with 4 parameters: data, escapeFn, include, rethrow.
data
: Incoming data.escapeFn
: Escape function.include
: Introduces subtemplate functions. The main logic is to get the template according to the path, compile and generate the render function for caching, and finally render.
var include = function (path, includeData) {
var d = utils.shallowCopy({}, data);
if (includeData) {
d = utils.shallowCopy(d, includeData);
}
return includeFile(path, opts)(d);
};
Copy the code
rethrow
: throws an exception function.
Generates the execution steps of the render function. This step is done after the template is cut. First the template is cut when it encounters the EJS label. The cut string contains labels in pairs, as mentioned above.
[ '{ key1 = '.'< % ='.' key1 '.'% >'.', 2key1 = '.'< % ='.' key1+key1 '.'% >'.' }' ]
Copy the code
Ejs generates different execution steps for different tags. We’re going to iterate over the entire array. Because tags cannot be nested, and appear in pairs, it is possible to use global variables to save the type of the current tag. When executing the contents sandwiched in a pair of tags, you can obtain the information of the outer tag. When closing a label, reset the label information.
Consider the include method for paths. Ejs syntax does not support nesting, so you can only use this method to duplicate templates. Here is a chestnut in use.
<ul>
<% users.forEach(function(user){ %>
<%- include('user/show', {user: user}) %>
<% }); %>
</ul>
Copy the code
When you use the include method, you pass in the path and data to reuse the template. The logic of a path will first check whether it is an absolute path, and then concatenate the passed path parameters with options.filename. If the file does not exist, finally check whether the file exists in the views directory, see 👇 for the code
function getIncludePath(path, options) {
var includePath;
var filePath;
var views = options.views;
// Abs path
if (path.charAt(0) == '/') {
includePath = exports.resolveInclude(path.replace(/^\/*/,' '), options.root || '/'.true);
}
// Relative paths
else {
// Look relative to a passed filename first
if (options.filename) {
filePath = exports.resolveInclude(path, options.filename);
if (fs.existsSync(filePath)) {
includePath = filePath;
}
}
// Then look in any views directories
if(! includePath) {if (Array.isArray(views) && views.some(function (v) {
filePath = exports.resolveInclude(path, v, true);
returnfs.existsSync(filePath); })) { includePath = filePath; }}if(! includePath) { throw new Error('Could not find the include file "' +
options.escapeFunction(path) + '"'); }}return includePath;
}
Copy the code
This means that when using include, the child template file can only be in the views directory with the suffix EJS. Or set the options.filename variable to distribute the files in different directories. This is more pit, it is very inconvenient to use. How to reuse templates when the nesting level is high? It looks like the only way to do it is by absolute path.