Most of you are familiar with HTML template syntax, probably in the form of “{“, like Vue’s template syntax, which generates HTML by iterating through template strings:

<span>Message: {{ msg }}</span>
Copy the code

In addition to this type, there is also a very common template markup syntax called erb-style, which is the one we will implement next.

Although we are implementing ERB style this time, this is just a tag, and if you understand the content of this article, you can switch to any tag method you like, for example, if you want to use {{, that’s fine.

However, this article uses the ERB style as an example.

Its syntax is also relatively simple, with two main expressions:

  1. The < %... % >You can wrap a JavaScript statement:
The < %for ( let i = 0; i < 10; i++ ) { %>
  <% console.log(i) %>
<% } %>
Copy the code
  1. < % =... % >You can get variables in the current execution environment:

So let’s say we’ve written our template function, let’s call it template.

Our use will be:

const render = template('<div><%= data.name %></div>');
console.log(render({name: 'hi'})) // <div>hi</div>
Copy the code

Let’s do another example using <%=… %>, which is a usage scenario in Webpack:

// @filename: webpack.config.js
plugins: [
  new HtmlWebpackPlugin({
    title: 'Custom template'.// Load a custom template (lodash by default)
    template: 'index.html'})]// @filename: index.html<! DOCTYPE html><html>
  <head>
    <meta charset="utf-8"/>
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
  </body>
</html>
Copy the code

After the above examples, we must be clear about the ERB style template is right?

In addition to the two tag syntax mentioned above, there are other tags, such as <%-… %>, in fact, its conversion principle and <%=… %> is the same, but the inner HTML string is escaped, but this article doesn’t explain how to escape HTML strings, so we’ll skip that notation. This article is recommended for those who want to understand how it works

Now let’s implement an ERB style template engine.

Ps: The following code is the underscorejs _. Template approach, but skips compatibility for some boundary cases.

We have an index.html file:

<! DOCTYPEhtml>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title></title>
</head>

<body>
  <script id="templ" type="text/html">The < %for ( var i = 0; i < data.list.length; i++ ) { %>
      <li>
          <a data-id="<%= data.list[i].id %>">
              <%=data.list[i].name %>
          </a>
      </li>The < %} % ></script>
  <script src="./template.js"></script>
</body>

</html>
Copy the code

First, we get this template:

let content = document.querySelector('#templ').innerHTML
Copy the code

What is at the heart of our template engine? It’s the use of new Funtion. In fact, we can construct a function as follows:

const print = new Function('str'.'console.log(str)')
print('hello world') // hello world
Copy the code

It is equivalent to:

const print = function (str) {console.log(str)}
Copy the code

With this magical feature, we wondered if we could convert the above template into a string of legitimate JavaScript code, called string X.

So can we make a template engine?

new Funtion('data', x);
Copy the code

The answer is: yes, that’s what we’re going to do.

Now the key to the problem is how do we convert the content value into a string of JavaScript code.

<%for ( var i = 0; i < data.list.length; i++ ) { %>
  <li>
      <a data-id="<%= data.list[i].id %>">
          <%=data.list[i].name %>
      </a>
  </li>
<% } %>
Copy the code

We can:

  1. Use a regular/<%=([\s\S]+?) %>/gMatch to the< % =... % >Formatted string
  2. Use a regular/<%([\s\S]+?) %>/gMatch to theThe < %... % >Formatted string

Note that the second re contains the first, so we must replace the first re first.

If we match <%=… %>, we will change it to: ‘+\n… +\n’

content = content.replace(/<%=([\s\S]+?) %>/g.function(_, evaluate) {
 return "'+\n" + evaluate + "+\n'"
})
Copy the code

B: well… A little weird? That’s all right. Read on.

Next, we match <%… % >.

Change it to: ‘; \n … \ n_p + = ‘.

content = content.replace(/<%([\s\S]+?) %>/g.function(match,interpolate) {
  return "'; \n" + interpolate + "\n_p +='";
})
Copy the code

Isn’t it a bit presentable now? But this isn’t legitimate JavaScript code yet.

We also need to add something to the end of it.

Add let _p = ” to the header; \nwith (data){\n_p+=’}return _p ‘;

This is almost presentable, but there is a problem. Look at line 5 in the figure above, because there is a \n character at the end of the line, so it wraps after ‘.

But in JavaScript, line breaks are not allowed, and if we copy this code to the console, we will still get an error.

We can either think about changing ‘to ES6’s template string syntax, or we can think about handling special characters like this, so let’s go special.

If we use an editor to write two lines of code in a JS file:

const a = 1;
const b = 2;
Copy the code

It’s really stored in a file that looks more like this: const a = 1; \nconst b = 2; . To preserve \n’s original appearance in the string, we need to escape it. When we say ‘const a = 1; \\nconst b = 2; ‘represents the actual stored result above.

And \n also have the following, a unified table:

Escape character To translate into
\ `
\ \ \
\r \\r
\n \\n
\u2028 \\u2028
\u2029 \\u2029

At the code level, it looks like this:

var escapes = {
  "'": "'".'\ \': '\ \'.'\r': 'r'.'\n': 'n'.'\u2028': 'u2028'.'\u2029': 'u2029'
};

var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;

function escapeChar(match) {
  if (match === "'") {
    return "\ '" 
  }
  return '\ \' + escapes[match];
}
Copy the code

Notice that the escapeChar function is specially compatible with the single quotation mark, because it is different from the others. Compared to our column table, its conversion result is preceded by only one \, but we can also remove this, that is to use the single quotation mark. Since “\'” equals “\ \ “, the code can drop the if statement and write:

function escapeChar(match) {
  return '\ \' + escapes[match];
}
Copy the code

Given the unique nature of \ as a translation sequence, the second entry of our escapes object actually represents one \ and the result of our escapes actually represents two \:

byte[] byteArray1 = "\ \".getBytes();
byte[] byteArray2 = "\ \ \ \".getBytes();
System.out.println(byteArray1) / / [92]
System.out.println(byteArray2) / / [92, 92]
Copy the code

After we get the content at the beginning and add the logic to handle the translation sequence, look at the final result:

content = content.replace(escapeRegExp, function(match) {
  return escapeChar(match);
}
Copy the code

There is no problem, and we can safely pass it to the second parameter of the new Function.

const render = new Function('data', content);
Copy the code

Calling our render function later would do this:

render({
    list: {name: 'Bob', id: 1}
})
Copy the code

We can get the following result:

Perfect. Logic we’re finally done.

Underscore does the same thing, but does it more succinctly.

<%=… <%=… %> <%… %> get rid of it, and then refine the header and tail of the code.

It uses a different re, /<%=([\s\ s]+?). %>|<%([\s\S]+?) % > | $/ g, the key point is here.

Underscore just needs to iterate over once and hit <%=… %> or <%… After %>, the special characters from the end of the last matching result to the end of the current matching result are processed, and then the syntax of the template to be matched is determined, and the sequence is iterated until the end of the string is matched.

Some of you might wonder, does this match last? If our template ends with pure strings instead of <%=… %> or <%… %>, the re will not match the end? This is the underscore in order to add the regular last | $, ensure that can match in the end, so it can get rid of this a special characters also.

In addition, underscore handles the template syntax <%=… %> adds null and undefined. If it is either of these, our initial script will simply print the string ‘undefined’ or ‘null’. But underscore lets these cases output empty strings.

var interpolate = '123'; 
var __t;
(__t= (interpolate)) == null ? ' ' : __t
Copy the code

To make it human, it’s equivalent to:

interpolate == null ? interpolate : ' '
Copy the code

With these points in mind, it should be easier to look at the _. Template source code.

The _. Template function underscore () is used to specify the underscore function. The _. Template implementation should be easier to understand when broken down than when explained directly 🙂

In order to facilitate debugging, I put the executable code in the following, students need to take ~

I have a small wish, that is, the total number of likes reached 100, now there are 91, see here students, if this article is helpful to you, can you please give me a like, it doesn’t matter if not, read here, has been my biggest support, thank you.

Thank you for reading

Refer to the link

  1. Implement a template engine
  2. Use of new Functon

The complete code

<! -- @filename: index.html -->
<! DOCTYPEhtml>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title></title>
</head>

<body>
  <script id="templ" type="text/html">The < %for ( var i = 0; i < data.list.length; i++ ) { %>
      <li>
          <a data-id="<%= data.list[i].id %>">
              <%=data.list[i].name %>
          </a>
      </li>The < %} % ></script>
  <script src="./index.js"></script>
</body>

</html>
Copy the code
// @filename: main.js
let content = document.querySelector('#templ').innerHTML

var settings = {
  evaluate: /<%([\s\S]+?) %>/g,
  interpolate: /<%=([\s\S]+?) %>/g};var escapes = {
  "'": "'".'\ \': '\ \'.'\r': 'r'.'\n': 'n'.'\u2028': 'u2028'.'\u2029': 'u2029'
};

var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;

function escapeChar(match) {
  if (match === "'") {
    return '\ \ `' 
  }
  return '\ \' + escapes[match];
}

function template(text) {
  var matcher = RegExp([
    (settings.interpolate || noMatch).source,
    (settings.evaluate || noMatch).source
  ].join('|') + '| $'.'g');

  var index = 0;
  var source = "__p+='";
  text.replace(matcher, function (match, interpolate, evaluate, offset) {
    source += text.slice(index, offset).replace(escapeRegExp, escapeChar);
    index = offset + match.length;

   if (interpolate) {
      source += "'+\n((__t=(" + interpolate + "))==null? '':__t)+\n'";
    } else if (evaluate) {
      source += "'; \n" + evaluate + "\n__p+='";
    }

    return match;
  });
  source += "'; \n";

  var argument = 'data';
  source = 'with('+ argument + '||{}){\n' + source + '}\n';

  source = "var __t,__p='';" +
    source + 'return __p; \n';

  var render;
  try {
    render = new Function(argument, source);
  } catch (e) {
    e.source = source;
    throw e;
  }

  var template = function (data) {
    return render.call(this, data);
  };

  return template;
}

const render = template(content);

var list = [
  {name: 'Bob'.id: 1},
  {name: 'Jack'.id: 2},]console.log(render({
  list
}))
Copy the code