Author: Cui Jing

preface

While there are various front-end frameworks available to improve development efficiency, in some cases components implemented in native JavaScript are also indispensable. For example, in our project, we need to provide a common payment component to the business side, but the technology stack used by the business side may be Vue, React, etc., or even native JavaScript. In order to achieve universality and maintain component maintainability, it is necessary to implement a native JavaScript component.

Below is the general appearance of our Panel components on the left, and the general directory structure of our project on the right:

We split a component into.html,.js and.css files, such as Panel component, which contains panel.html, panel.js and panel.css files, so that the view, logic and style can be separated for easy maintenance. In order to improve the flexibility of components, the title of Panel, the copy of button, and the number and content of intermediate items are controlled by configuration data. In this way, we can dynamically render components according to configuration data. In this process, in order to make the flow of data and events clearer, we introduced the concept of data processing center data Center in reference to the design of Vue, where the data required by components are stored uniformly. Data Center data changes trigger component updates, and this update process is to re-render the view based on different data.

Panel.html is what we call a “string template,” and parsing it into executable JavaScript code is what a “template engine” does. There are many template engines to choose from, and they generally offer a wealth of functionality. But in many cases, we might just be working with a simple template that doesn’t have too much logic, so a simple string template is enough for us to use.

Several string template methods and simple principles

It is mainly divided into the following categories:

  1. Rough and simple — regular substitution

    The simplest and most crude way is to use strings directly for regular substitution. But you can’t handle loop statements and if/else judgments.

    A. Define how to write a string variable, such as wrapping it with <%%>

    const template = (
      '<div class="toast_wrap">' +
        '<div class="msg"><%text%></div>' +
        '<div class="tips_icon <%iconClass%>"></div>' +
      '</div>'
    )
    Copy the code

    B. Find all <%%> and replace the variables in the <%%>

    function templateEngine(source, data) {
      if(! data) {return source
      }
      return source.replace([^ % > / < % (] +)? %>/g.function (match, key) {  
        return data[key] ? data[key] : ' '
      })
    }
    templateEngine(template, {
      text: 'hello'.iconClass: 'warn'
    })
    Copy the code
  2. Simple and elegant – ES6 template syntax

    Using the template string in ES6 syntax, the above global substitution, implemented by regular expressions, can be written simply

    const data = {
      text: 'hello'.iconClass: 'warn'
    }
    const template = `
      <div class="toast_wrap">
        <div class="msg">${data.text}</div>
        <div class="tips_icon ${data.iconClass}"></div>
      </div>
    `
    Copy the code

    You can write any expression in the template string ${}, but again, if/else judgments and loops are not handled.

  3. Simple template engine

    In many cases, when we render HTML templates, especially UL elements, a for loop is necessary. Then you need to add logical processing statements to the simple logic above.

    For example, we have the following template:

    var template = (
      'I hava some menu lists:' +
      '<% if (lists) { %>' +
        '<ul>' +
          '<% for (var index in lists) { %>' +
            '<li><% lists[i].text %></li>' +
          '< %} % >' +
        '</ul>' +
      '<% } else { %>' +
        '<p>list is empty</p>' +
      '< %} % >'
    )
    Copy the code

    Intuitively, we want the template to look like this:

    'I hava some menu lists:'
    if (lists) {
      '<ul>'
      for (var index in lists) {
        '<li>'
        lists[i].text
        '</li>'
      }
      '</ul>'
    } else {
     '<p>list is empty</p>'
    }
    Copy the code

    To get the final template, we push scattered HTML fragments into an array OF HTML, and finally join together the final template with html.join(” “).

    const html = []
    html.push('I hava some menu lists:')
    if (lists) {
      html.push('<ul>')
      for (var index in lists) {
        html.push('<li>')
        html.push(lists[i].text)
        html.push('</li>')
      }
      html.push('</ul>')}else {
     html.push('<p>list is empty</p>')}return html.join(' ')
    Copy the code

    Thus, we have executable JavaScript code. By contrast, it’s easy to see that from templates to JavaScript code, there are several transformations:

    1. The < % % >If it is in the logic statements (if/else/for/switch/case/break), then in the middle of the content directly into JavaScript code. By regular expression/ (^ ()? (var|if|for|else|switch|case|break|;) ) (. *)? /gFilter out the logical expressions to be processed.
    2. <% xxx %>If the statement is not logical, then we replace it withhtml.push(xxx)The statement of
    3. The < % % >For everything else, let’s replace it withHtml.push (string)
    const re = / < % (. +?) %>/g
    const reExp = / (^ ()? (var|if|for|else|switch|case|break|;) ) (. *)? /g
    let code = 'var r=[]; \n'
    let cursor = 0
    let result
    let match
    const add = (line, js) = > {
      if (js) { // Process the contents of '<%%>',
        code += line.match(reExp) ? line + '\n' : 'r.push(' + line + '); \n'
      } else { // Process contents other than '<%%>'code += line ! = =' ' ? 'r.push("' + line.replace(/"/g.'\ \ "') + '"); \n' : ' '
      }
      return add
    }
    
    while (match = re.exec(template)) { // loop to find all <%%>
      add(template.slice(cursor, match.index))(match[1].true)
      cursor = match.index + match[0].length
    }
    // Process everything after the last <%%>
    add(template.substr(cursor, template.length - cursor))
    // Finally return
    code = (code + 'return r.join(""); } ').replace(/[\r\t\n]/g.' ')
    Copy the code

    At this point we have a “text” version of JavaScript code, which we can turn into actual executable code using new Function.

    The last thing left to do is pass in arguments and execute the function.

    Method 1: You can encapsulate all the parameters in the template in a single object (data), and then use Apply to bind this to this object. So in the template, we can get the data through this.xx.

    new Function(code).apply(data)
    Copy the code

    Method 2: Always write this. It feels a little cumbersome. You can run the function wrapped in with(obj) and pass in the data used by the template as obj arguments. This way, you can use variables directly in the template as you did in the previous example.

    let code = 'with (obj) { ... '. newFunction('obj', code).apply(data, [data])
    Copy the code

    Note, however, that the with syntax itself has some drawbacks.

    At this point we have a simple template engine.

    On this basis, some packaging can be carried out to expand the function. For example, an I18N multilingual processing method could be added. This allows you to separate the language text from the template and use it directly in later rendering after setting the language globally.

    Basic idea: Wrap the data passed into the template and add a $i18n function to it. Then when we write

    <%$i18n(“something”)%>

    in the template, it will be resolved to push($i18n(“something”))

    The specific code is as follows:

    // template-engine.js
    import parse from './parse' // The simple template engine implemented earlier
    class TemplateEngine {
      constructor() {
        this.localeContent = {}
      }
    
      ParentEl, TPL, data = {} or TPL, data = {}
      renderI18nTpl(tpl, data) {
        const html = this.render(tpl, data)
        const el = createDom(`<div>${html}</div>`)
        const childrenNode = children(el)
        // Multiple elements are wrapped in 
            
    , and individual elements are returned directly
    const dom = childrenNode.length > 1 ? el : childrenNode[0] return dom } setGlobalContent(content) { this.localeContent = content } // Add an extra $i18n function to the data passed to the template. render(tpl, data = {}) { returnparse(tpl, { ... data,$i18n: (key) = > { return this.i18n(key) } }) } i18n(key) { if (!this.localeContent) { return ' ' } return this.localeContent[key] } } export default new TemplateEngine() Copy the code

    Set the global copywriting using the setGlobalContent method. It can then be used directly in the template with <%$i18n(“contentKey”)%>

    import TemplateEngine from './template-engine'
    const content = {
      something: 'zh-CN'
    }
    TemplateEngine.setGlobalContent(content)
    const template = '<p><%$i18n("something")%></p>'
    const divDom = TemplateEngine.renderI18nTpl(template)
    Copy the code

    We use ‘<%%>’ to wrap logical blocks and variables, and a more common way is to use double curly braces {{}}, also known as mustache. This markup is used in Vue, Angular, and wechat applet template syntax, also known as interpolation. Let’s look at the implementation of a simple Mustache grammar template engine.

  4. How the template engine Mustache. Js works

    With the foundation of Method 3, it is a little easier to understand how other template engines work. Let’s take a look at the principle behind mustache, a lightweight template that’s used extensively.

    Here’s a simple example:

     var source = ` 
            
    {{#author}}

    {{name.first}}

    {{/author}}
    `
    var rendered = Mustache.render(source, { author: true.name: { first: 'ana'}})Copy the code
    • The template parsing

      The template engine first parses the template. Here’s how Mustache’s template parsing goes:

      1. For the regular matching part, the pseudocode is as follows:
      tokens = []
      while(! Whether the remaining template string to be processed is empty) {value = scanner.scanuntil (openingTagRe); Value = everything up to the first {{in the template stringif(value) {handle value, split characters into tokens. For example, < div class ="entry">
          tokens = [
            {'text'."<", 0, 1},
            {'text'."d"< 1, 2},... ] }if(! Match the {{)break;
        type= matches the first character after the opening character {{, resulting in a type such as {{# tag}}, {{/ tag}}, {{tag}}, {{> tag}}, etcToken = [value = tag token = [value = tag token = [value = tag token = [type, value, start, end ]
        tokens.push(token)
      }
      Copy the code
      1. Then merge contiguous arrays of text type by iterating through tokens.

      2. Traverse the tokens, processing section type (in the template {{# tag}} {{/ tag}}, {{^ tag}} {{/ tag}}). Sections appear in pairs in the template and need to be nested according to section. Finally, the nested type matches that of our template.

    • Apply colours to a drawing

      Once you’ve parsed the template, it’s time to render: from the incoming data, you get the final HTML string. The general process of rendering is as follows:

      The render template data is first stored in a variable context. Because in templates, variables are represented as strings, such as ‘name.first’. TrueValue = Context [‘name’][‘first’]; trueValue = context[‘name’][‘first’] Cache [‘name.first’] = trueValue to improve performance.

      The core process of rendering is to iterate through tokens, get the true values of the types and variables, then render according to the types and values, and finally join the results together to get the final result.

Find the right template engine

Of all the template engines, how do we lock down which one we need? Here are a few directions to consider, hoping to help you choose:

  • function

    The most important thing in choosing a tool is whether it meets our needs. For example, whether variables, logical expressions are supported, whether subtemplates are supported, whether HTML tags are escaped, and so on. The following table is just a simple comparison of a few template engines.In addition to basic functions, different template engines also provide their own unique functions, such as artTemplate support template file breakpoints, convenient debugging, and some auxiliary methods; Handlesbars also provides a Runtime version that precompiles templates; Ejs logical expressions are written in the same way as JavaScript; And so on here is not an example.

  • The size of the

    For a lightweight component, we care about the final size of the component. Feature-rich template engines tend to be bulky, so there is a trade-off between functionality and size. ArtTemplate and doT are smaller and only a few KB compressed, while Handlebars are larger and still 70+KB compressed in 4.0.11.(Note: The figure above is partly derived from the size of min.js on https://cdnjs.com/ and partly from the size of git. Size is not gzip size)

  • performance

    If you have a lot of frequent DOM updates or a lot of DOM to render, you need to pay attention to the performance of the template engine when rendering.

Finally, in the case of our project, the component we want to implement is a lightweight component (mainly a floating layer interface, two page-level full-coverage interfaces) with simple user interaction, and the component does not need to be re-rendered frequently. But the overall size of the component is very important, and in particular, we need to support multiple languages in the component’s text. So we finally decided on the third option introduced above.

Reference documentation
  • ES6: template string
  • format.js
  • JavaScript template engine in just 20 lines
  • mustache
  • art-template
  • doT.js
  • handlebars