Simple implementation of the Javascript string template

  • 2020-12-19 20:55:31
  • OfStack

This comes from a problem I had two years ago when I was working on my first real website

The site uses a separate front and back end, with the REST interface on the back end returning JSON data, which is then rendered on the page by the front end.

Like many newcomers to Javascript, I started by embedding JSON data into HTML in the form of concatenated strings. It starts with a small amount of code and is acceptable for the time being. But as the page structure becomes more complex, its weaknesses become unbearable:

The writing is incoherent. Every time I write a variable I have to break 1, insert 1 + and ". Ten is easy to make mistakes. Can't be reused. HTML fragments are discrete data, and it is difficult to extract the repeated parts. Can't put it to good use < template > The label. This is one of the new tags in HTML5, where the HTML template is highly recommended < template > Tag to make the code cleaner. That was my feeling:

Is this TMD kidding me

To solve this problem, I put my project on hold and spent half an hour implementing a very simple string template.

Requirements describe

Implement 1 render(template, context) method and fill the placeholder in template with context. Requirements:

There is no need for control flow components (such as loops, conditions, and so on), as long as there is a variable substitution function
Cascading variables can also be expanded
Escaped delimiters {and} should not be rendered. White space is allowed between delimiters and variables
Example:


render('My name is {name}' .  {
  name: 'hsfzxjy'
}); // My name is hsfzxjy

render('I am in {profile.location}', {
  name: 'hsfzxjy',
  profile: {
    location: 'Guangzhou'
  }
}); // I am in Guangzhou

render('{ greeting }. \\{ This block will not be rendered }', {
  greeting: 'Hi'
}); // Hi. { This block will not be rendered }

implementation

First write down the framework of the function:


function render(template, context) {

}

Obviously, the first thing to do is match the placeholder in the template.

Match the placeholder

The match must be left to the regular expression. So what should this regular expression look like?

According to the description of requirements 1 and 2, we can write:


var reg = /\{([^\{\}]+)\}/g;

As for requirement 3, the first concept That comes to my mind is forward matching. Unfortunately, Javascript does not support it, so I have to use a compromise:


var reg = /(\\)?\{([^\{\}\\]+)(\\)?\}/g;
//  If the first 1 Or the first 3 If the group value is not null, it will not be rendered 
 Now, the code should look like this: 

function render(template, context) {

  var tokenReg = /(\\)?\{([^\{\}\\]+)(\\)?\}/g;

  return template.replace(tokenReg, function (word, slash1, token, slash2) {
    if (slash1 || slash2) { //  Matches to escape characters 
      return word.replace('\\', ''); //  if   The delimiter is escaped and is not rendered 
    }

    // ...
  })
}

Placeholder substitution

Well, the regular expression confirms that, and the next thing to do is replace.

According to requirement 2, the template engine must be able to render not only level 1 variables, but also levels of variables. How do you do that?

In fact, it is very simple: separate token by., and search level by level:


var variables = token.replace(/\s/g, '').split('.'); //  cutting  token
var currentObject = context;
var i, length, variable;

//  Step by step to find  context
for (i = 0, length = variables.length, variable = variables[i]; i < length; ++i)
  currentObject = currentObject[variable];

return currentObject;

However, it is possible that the variable specified by token does not exist, and the above code will report an error. For a better experience, the code should be fault-tolerant:


var variables = token.replace(/\s/g, '').split('.'); //  cutting  token
var currentObject = context;
var i, length, variable;

for (i = 0, length = variables.length, variable = variables[i]; i < length; ++i) {
  currentObject = currentObject[variable];
  if (currentObject === undefined || currentObject === null) return ''; //  Returns an empty string if the object currently indexed does not exist. 
}

return currentObject;

Combine all the code in one, and you get the final version:


function render(template, context) {

  var tokenReg = /(\\)?\{([^\{\}\\]+)(\\)?\}/g;

  return template.replace(tokenReg, function (word, slash1, token, slash2) {
    if (slash1 || slash2) { 
      return word.replace('\\', '');
    }

    var variables = token.replace(/\s/g, '').split('.');
    var currentObject = context;
    var i, length, variable;

    for (i = 0, length = variables.length, variable = variables[i]; i < length; ++i) {
      currentObject = currentObject[variable];
      if (currentObject === undefined || currentObject === null) return '';
    }

    return currentObject;
  })
}

Remove the blank lines for a total of 17 lines.

Hang the function to the prototype chain of String

We can even modify the prototype chain to achieve some cool effects:


String.prototype.render = function (context) {
  return render(this, context);
};

After that, we can call:


"{greeting}! My name is { author.name }.".render({
  greeting: "Hi",
  author: {
    name: "hsfzxjy"
  }
});
// Hi! My name is hsfzxjy.


Related articles: