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
  • dataThe data corresponding to the template has all the variables used in the template, such as the above chestnut must haveuser.nameThis data.
  • option: template configuration item. There are these 👇 :
    • cache Compiled functions are cached, requires filename
    • filename The name of the file being rendered. Not required if you are using renderFile(). Used by cache to key caches, and for includes.
    • root Set project root for includes with an absolute path (/file.ejs).
    • context Function execution context
    • compileDebug When false no debug instrumentation is compiled
    • client When true, 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/close
    • debug Output generated function body
    • strict When set to true, generated function is in strict mode
    • _with Whether or not to use with() {} constructs. If false then the locals will be stored in the locals object. Set to false in strict mode.
    • localsName Name to use for the object storing local variables when not using with Defaults to locals
    • 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 When true, EJS will use an async function for rendering. (Depends on async/await support in the JS runtime.
  • compileCompile 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.
  • renderRender 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:

  1. Cut templates based on regular expressions, for example{ key1 = <%= key1 %>, 2key1 = <%= key1+key1 %> }Will be cut into[ '{ key1 = ', '<%=', ' key1 ', '%>', ', 2key1 = ', '<%=', ' key1+key1 ', '%>', ' }' ]
  2. 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
  1. 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.