ES6 (ES2015) introduces a number of new features to JavaScript, including a new feature related to string handling, template literals, which provide multi-line strings, string templates, and I believe many people already use them. Template literals are simple to use, but most developers use them just as syntactic sugar for string concatenation, which is much more powerful than that. To exaggerate, this is probably the most underrated feature of ES6. Here is according to.

Basic features

Template Literals are called Template Literals in the ES2015 specification, and Template Strings in earlier versions of the specification, so many Chinese documents we’ve seen also refer to them as Template Strings, sometimes informally referred to as ES6 templates for ease of expression.

In JavaScript prior to ES6, strings were the basic type, and the only way to represent strings in code was to enclose them in quotes (single ‘or double’), while ES6 template literals (hereinafter referred to as ES6 templates) used backapostrophes (‘) as string representations.

The regular string between two backapostrophes is left unchanged, as in:

`hello world`= = ="hello world"  // --> true
`hello "world"`= = ='hello "world"' // --> true
`hello 'world'`= = ="hello 'world'" // --> true
` \ ` ` // --> "`" // --> true
Copy the code

Newlines are also just one character, so template literals naturally support multiple lines:

console.log(`TODO LIST: * one * two `)
// TODO LIST:
// * one
// * two
Copy the code

Any JavaScript expression in the ${expression} format between the two backapostrophes is converted to a string and concatenated with the strings before and after the expression. When expression is expanded to a string, the expression.tostring () function is used.

const name = "Alice"
const a = 1
const b = 2
const fruits = ['apple'.'orange'.'banana']
const now = new Date(a)console.log(`Hello ${name}`)
console.log(` 1 + 2 =${a + b}`)
console.log(`INFO: ${now}`)
console.log(`Remember to bring: ${ fruits.join(', ')}. `)
console.log(` 1 < 2The ${1 < 2 ? '✔ ︎' : "✘"}`)
// Hello Alice
// 1 + 2 = 3
// INFO: Sun May 13 2018 22:28:26 GMT+0800
// Remember to bring: apple, orange, banana.
// 1 < 2 ✔︎
Copy the code

Because expression can be any JavaScript expression, and any template string is itself an expression, expression in a template can nest another template.

const fruits = ['apple'.'orange'.'banana']
const quantities = [4.5.6]
console.log(`I got ${
    fruits
        .map((fruit, index) => `${quantities[index]} ${fruit}s`)
        .join(', ')}`)
// I got 4 apples, 5 oranges, 6 bananas
Copy the code

Contrast with a traditional template engine

From a few examples so far, we’ve learned the basics of the ES6 template, but enough to see what it can do. It makes it easy to refactor string concatenation code so that it doesn’t have to be a mess of single quotes, double quotes, + operators, and backslashes.

Therefore, it is natural for us to think that in the actual application of the most complex string concatenation scenario — HTML template, if using ES6 template can be competent? Traditionally we have used a dedicated template engine to do this, so compare ES6 templates to template engines. We chose LoDash’s _. Template engine, which isn’t mustache or Pug, but is pretty much complete, so let’s compare a few of its core features and scenarios.

First, basic string interpolation. _. Template uses <%= expression %> as the template interpolation delimiter. The value of expression is the same as the original output of the ES6 template. So in this one feature, the ES6 template is fully competent.

  • lodash

    const compiled = _.template('hello <%= user %>! ')
    console.log(compiled({ user: 'fred' }))
    // hello fred
    Copy the code
  • ES6 template

    const greeting = data= > `hello ${data.user}`
    console.log(greeting({ user: 'fred' }))
    // hello fred
    Copy the code

Second, string escape output, which is a standard function of THE HTML template engine to defend against XSS. The principle is to escape the characters contained in the values of interpolation expressions, which may be used for XSS attacks, as HTML Entity. To get LoDash to print the escaped interpolation, use the <% -expression %> syntax; If you use the ES6 template scheme, you have to implement a single function call yourself. In the example code below, you define the simple escapeHTML function, which is called from within the template string to escape the string.

ES6 can indeed do the same with this feature, but at the cost of defining escape functions and calling them in expressions that are less convenient to use than the interface encapsulated by the template engine.

  • lodash

    const compiled = _.template('<b><%- value %></b>')
    Copy the code
  • ES6 template

    const entityMap = {
        '&': '& '.'<': '< '.'>': '> '.'"': '" '."'": '& # 39; '.'/': '/ '.'`: '` '.'=': '= '
    }
    const escapeHTML = string= > String(string).replace(/[&<>"'`=\/]/g, (s) => entityMap[s]);
    const greeting = data= > `hello ${escapeHTML(data.user)}`
    console.log(greeting({ user: '<script>alert(0)</script>'}));
    // hello < script> alert(0)< / script>
    Copy the code

Third, templates have embedded JavaScript statements, that is, the template engine supports generating HTML by executing JavaScript statements in templates. To put it bluntly, the principle is the same as the idea of PHP, the world’s best language. In loDash templates, JS statements can be executed using <% statement %>. A typical use scenario is to use a for loop to iterate over the contents of an array in the template.

But the placeholder ${} in the ES6 template only supports insertion expressions, so you can’t execute JavaScript statements like a for loop directly inside of it. But it doesn’t matter, we can also solve the same output results with some simple skills, such as the array processing, we only need to make use of the array map, reduce, filter, so that the expression results meet our needs.

  • lodash

    /* Use the for loop to iterate over the contents of the array and print */
    const compiled = _.template('<ul><% for (var i = 0; i < users.length; i++) { %><li><%= list[i] %></li><% } %></ul>')
    console.log(compiled({ users: ['fred'.'barney']}))// <ul><li>fred</li><li>barney</li></ul>
    Copy the code
  • ES6 template

    const listRenderer = data= > `<ul>${data.users.map(user => `<li>${user}</li>`).join(' ')}</ul>`
    console.log(listRenderer({ users: ['fred'.'barney']}))
    // <ul><li>fred</li><li>barney</li></ul>
    Copy the code

In these three sample scenarios, we found that the LODash template can do everything ES6 templates can do, so should we abandon the template engine?

Indeed, if we just used these basic template engine functions in our development, we could have used ES6 templates directly instead, the API is lighter and simpler, and we saved the cost of introducing an additional template library.

However, if we are using a large and comprehensive template engine such as PUG, artTemplate, Handlebars, using ES6 template replacement is not necessarily wise. In particular, ES6 templates do not provide the template caching, pipeline output, and modular management of template files provided by these template engines in server-side scenarios. And the template engine is a stand-alone library, with API encapsulation and extensibility better than just inserting expressions into ES6 templates.

Step up – label the template

With the features introduced so far, we can discover the fact from the code of the two examples of HTML template string escape and array iteration output above:

To implement more complex string-handling logic using ES6 templates, we need to write functions to implement it.

Fortunately, in addition to finding ways to call various string conversion functions in template interpolation, ES6 offers a more elegant and easily reusable solution: tagged Template literals.

The syntax for a label template is simple: a label precedes the beginning backapostrophe of the template string. This tag is of course not arbitrary; it must be the name of a callable function or method. The output of a tagged template is computed as no longer the default processing logic, as illustrated by the following line of code.

const message = l10n`I bought a ${brand} watch on ${date}, it cost me ${price}. `
Copy the code

In this case, l10n is the tag name and the content between the backapostrophes is the template content. This line assigns the value of the template expression to message as follows:

  1. The JS engine splits the template content with placeholders and stores the resulting string in an array called Strings (the following code is only used to illustrate the principle).

    const strings = "I bought a ${brand} watch on ${date}, it cost me ${price}".split([^ / $\ \ {}] + \} /)
    // ["I bought a ", " watch on ", ", it cost me ", "."]
    Copy the code
  2. The placeholder expressions are then extracted from the template contents and stored in another array, REST

    const rest = [brand, date, price]
    Copy the code
  3. Perform i18n (strings,… Call l10n and pass in two parameters, strings as the first parameter and rest expanded as the rest parameter.

    constmessage = l10n(strings, ... rest)Copy the code

Thus, if the above one-step decomposition is combined, this is the equivalent:

const message = l10n(["I bought a "." watch on ".", it cost me "."."], brand, date, price)
Copy the code

That is, when we prefix the template with the l10n tag, we are actually calling the l10n function and passing in the call argument as above. The l10n function can be given to us to customize so that the above line 👆 prints the desired string. For example, if we want the date and price to be displayed as a local string, we can do this:

const l10n = (strings, ... rest) = > {
    return strings.reduce((total, current, idx) = > {
        const arg = rest[idx]
        let insertValue = ' '
        if (typeof arg === 'number') {
            insertValue = ` selections${arg}`
        } else if (arg instanceof Date) {
            insertValue = arg.toLocaleDateString('zh-CN')}else if(arg ! = =undefined) {
            insertValue = arg
        }
        return total + current + insertValue
    }, ' ');
}
const brand = 'G-Shock'
const date = new Date(a)const price = 1000

l10n`I bought a ${brand} watch on ${date}, it cost me ${price}. `
// I bought a G-Shock watch on 2018/5/16, it cost me ¥1000.
Copy the code

The L10N here is a crude, dumb-ass localized template tag that converts the date to 2018/5/16 in zh-CN locale format by taking the number in the template content as the amount and adding the RMB symbol.

At first glance, this may not seem like a big deal, but imagine how the same effect could be achieved with a template without labels. ${date.tolocaledateString (‘ zh-cn ‘)} and ${‘¥’ + price} are required to call the corresponding conversion functions on the date expression and price number expression within the template. One call didn’t make much of a difference, but after two or three calls, the tagged template clearly won. Not only does it comply with the DRY (Don’t Repeat Yourself) principle, but it also makes template code simpler and easier to read.

Beyond template strings

Tagged template literals are based on a very simple principle — you can customize the output of the template through your own functions.

tag`template literals`
Copy the code

The ES6 specification does not place any restrictions on the tag functions that can be used here, meaning that any function can be added to the template as a tag, which means that this function:

  • They can be synchronous functions that return immediately, or they can be asynchronousasyncFunctions (support within functionsawaitAsynchronous statement and return Promise)
  • It can be a function that returns a string, or it can be a function that returns a number, an array, an object, or anything else
  • They can be pure functions, or they can be impure functions with side effects

So A: You can put any JavaScript statement inside A tag function if you want; B: You can have the label function return any value as template output. With such extensibility, it’s no wonder that the ES6 standard originally named the Template specification Template Strings, but then officially changed its name to Template Literals, as its capabilities have gone beyond Template Strings to poetry and beyond.

Of course, in the actual use or should be rational, not holding a hammer to see everything like a nail. Here are five applications that are the real deal breakers.

Use case 1: UseString.rawKeep the original string

String.raw is a static member method of a String that is added to the ES2015 specification. It is not called as a function directly, but as a template literal tag that comes with the language standard and preserves the original value of the template content.

This is similar to the effect of prefixing r before quotation marks on string expressions in Python. JavaScript needs this feature and just happens to implement it with String.raw.

var a = String.raw`\n\n`
var b === '\n\n'
// a === b -> false
// a.length === 4 -> true
// b.length === 2 -> true
Copy the code

The String.raw tag can save us a lot of tedious work in some scenarios. One of the typical scenarios is when you’re in development and you’re transferring protocol data through JSON text between different programming languages. If you encounter text content that contains escape characters and “(JSON attributes need to be quoted), it’s easy to get JSON parsing wrong because of the details.

I have encountered such a real case, that is, when an Android terminal was transmitting JSON data to JavaScript, there was a data in which the user’s nickname contained the character “. JavaScript receives JSON string text as follows:

{"nickname":"Gun pot &[{pot}]\" Pot ": pot"."foo":"bar"}
Copy the code

But here if content directly to use quotation marks to JSON. The parse (‘ {” nickname “:” gun pot & [} {pan] \ “pan \” : pan “, “foo” : “bar”} “) will encounter SyntaxError: Unexpected token error in JSON at position 22, because \ in single quotes is escaped and not retained in input values. To get the correct JSON object, use string.raw:

JSON.parse(String.raw'{"nickname":" pan &[{pan}]\" pan ": pan ","foo":"bar"}')
// {nickname: 'booty' &[{nickname}]" booty ": 'booty ', foo: 'bar'}
Copy the code

Use case 2: From tag templates to HTML strings

The comparison between ES6 templates and the template engine mentioned the problem of the template engine escaping unsafe characters through the manual escapeHTML template. Now that we know about the tag template, we can put the escapeHTML logic defined externally directly into the tag function so that we don’t need to call the escapeHTML function every time we insert an expression into the template.

const safeHTML = (strings, ... rest) = > {
    const entityMap = {
        '&': '& '.'<': '< '.'>': '> '.'"': '" '."'": '& # 39; '.'/': '/ '.'`: '` '.'=': '= '
	}
    const escapeHTML = string= > String(string).replace(/[&<>"'`=\/]/g, (s) => entityMap[s]);
    return strings.reduce((total, current, idx) = > {
	    const value = rest[idx] || ' '
		return total + current + escapeHTML(value)
    }, ' ');
}

const evilText = '<script>alert(document.cookie)</script>'
safeHTML`${evilText}`
// "< script> alert(document.cookie)< / script>"
Copy the code

The safeHTML we implemented here as a demo does not guarantee a complete production environment.

As you can imagine, common template tags such as HTML templates are already available in NPM libraries. Yes, the common-tags library is exactly what we want. Common-tags is a small, elegant library of template tags that many large projects rely on, including Angular, Slack, Ember, and more. It contains more than a dozen common functions for string template processing, such as HTML, safeHTml, etc., so we can be lazy and build fewer wheels.

const safeHtml = require('common-tags/lib/safeHtml')
const evilText = '<script>alert(document.cookie)</script>'
safeHtml`<div>${evilText}</div>`
Copy the code

Common-tags also provide an extension API to generate tags that make it easier to implement custom tags.

Use case 3: From template tags to DOM elements

The HTML and safeHtm template tags contained in the common-Tags library are still functions that return strings. This means that if we use it in the browser to get the template value output, we still need to use element.innerhtml = output to render the template content into the DOM element and display it in the interface.

But since the tag function of a template literal can return any value, we can go one step further and put the element.innerhtml = output statement into the tag function as well, with element as the return value. This allows us to directly use the value of the template literal as the actual DOM Element and directly manipulate its return value using the DOM API.

Not surprisingly, this idea has already been thought of, and @Choojs /nanohtml does just that. Choojs is a very young front-end framework (again), but we won’t discuss it today, just the nanoHTML it contains. The basic idea behind nanohtml can be illustrated with a Hello World like this:

var nanohtml = require('nanohtml')
var element = nanohtml`
      
Hello World!
`
document.body.appendChild(element) Copy the code

In addition to returning a DOM Element as shown in the code, nanoHTML also supports inserting any standard DOM Element into the interpolation in the template, which is rendered according to the template markup as a child Element of the returned value Element.

var nanohtml = require('nanohtml')
var h3 = document.createElement('h3')
h3.textContent = 'Hello World'
var element = nanohtml`<div>${h3}</div>`
document.body.appendChild(element)
Copy the code

Use case 4: Asynchronous operation

Combined with the async await feature of ES2017, we can also define an async label function and call an asynchronous operation in the function to achieve this.

For example, the idiot L10N tag implemented above, we can make it a little more stupid, the template finally splicing out of English sentences, translated into the local language after output. The translation process can be realized by asynchronously invoking third-party translation apis such as Google Translate API. So we have pseudocode like this:

const l10n = async(strings, ... rest) => {return strings.reduce((total, current, idx) = > {
        const arg = rest[idx]
        let insertValue = ' '
        if (typeof arg === 'number') {
            insertValue = ` selections${arg}`
        } else if (arg instanceof Date) {
            insertValue = arg.toLocaleDateString('zh-CN')}else if(arg ! = =undefined) {
            insertValue = arg
        }
        const line = total + current + insertValue
        const language = navigator.language // "zh-CN"
        // Pretend TRANSLATE_SERVICE is an available translation service
        const translated = await TRANSLATE_SERVICE.translate(line, { language })
        return translated
    }, ' ')
}

l10n`I bought a The ${'G-Shock'} watch on The ${new Date()}, it cost me The ${1000}`.then(console.log)
// "I bought a G-Shock watch on 5/16, 2018 and it cost me 1000 yuan"
Copy the code

Of course, in an environment that does not support async, await or generator, asynchronous operations can also be achieved by wrapping asynchronous calls with promises and making label functions return Promise instances.

const tag = (strings, ... rest) = > {
    return new Promise((resolve) = > {
        setTimeout(resolve, 1000.'Hello World')
    })
}

tag`Anything`.then(console.log)
// After 1s, the console will output:
// "Hello World"
Copy the code

Use case 5: DSL

DSL (Domain-specific language) is a rather spooky concept. As the name implies, A DSL is a language that does not solve a problem in a particular domain. A DSL is a language because it has rules that are bound by certain conditions — syntax. DSLS are usually used in the programming world, where DSLS are handled by computer programs, and some powerful DSL languages are also considered mini programming languages.

Typical DSLS are:

  • Regular expression
  • Database Query Language (SQL)
  • A CSS Selector rule is also a DSL
  • The Swiss Army knife of text processing awK, SED is also known as DSL because of its complex usage rules

The proper use of ES6 tag templates can make JavaScript’s handling of embedded DSLS much simpler. Strictly speaking, the HTML class template tags in our use case 2 and use Case 3 pairs above are the domain of DSLS. Here are some typical examples:

Note: The following template tags are descriptions only and need to be self-implemented

Case 1: DOM selector

For example, we could implement a DOM selector with tags, something like this:

var elements = query`.${className}` // the new way
Copy the code

While at first glance it looks like just another jQuery selector, this call naturally allows us to embed arbitrary interpolations in templates, improving the EXPRESSIVE nature of the API. And the query function is implemented by us freely, which means that we are free to normalize the selector value in the Query function, normalize the return value, and so on.

Case 2: Shell script

In node. js, we can also implement an sh tag to describe shell script calls:

var proc = sh`ps aux | grep ${pid}`
Copy the code

The intuitive thing to think about this line is that the API calls the shell to execute the command concatenated by the template, and the proc should be the result or output of that command execution.

Case 3: Regular expression constructor

Implement a dynamic regular expression constructor with the RE tag. This run-time generated regular expression is usually implemented by defining its own function and calling new RegExp.

var match = input.match(re`\d+${separator}\d+`)
Copy the code

Case 4: Internationalization and localization

An I18N and L10N template tag can be implemented: after the interpolation expression ${expression} in the template string, specify the expected text display format of the expression in :type format, and realize the custom template output function.

var message = l10n`Hello ${name}; you are visitor number ${visitor}:n!
You have ${money}:c in your account! `;
Copy the code

The L10N tag here adds the :type identifier to indicate categories compared to our hard-code version above, for example :n for numbers and :c for currency. Rules for these type identifiers can be agreed upon in the implementation code of L10N. This convention, however, smacks of a custom DSL.

What these four cases have in common is that we first agreed on apis with similar patterns, which are represented as tagged templates — a template name followed by template content.

Although we, as implementers, know that when we call the tag template, we are essentially reorganizing the template content into (strings,… The rest form is passed to the tag function call. But such API calls look like they have only one function and one argument, making it easy to guess the purpose of the API.

A good API should be self-descriptive, encapsulate complex implementation details, and try to do one thing well. From this perspective, tagged ES6 templates are ideal for dealing with JS embedded DSLS, and can even help us implement a Mini DSL in specific business logic.

To explore more

That is an introduction to the syntax and utility of ES6 templates. Speaking of practice, we can immediately enjoy its benefits thanks to its simplicity of principle. With Node.js, there is no doubt that we can use it immediately without hesitation; In a browser environment, you can convert this into compatible ES5 code using our old friend Babel.

In summary, the most exciting feature of the ES6 template is the tag, a small tag that provides an extraordinary amount of extensibility with a simple principle. Based on it, the JavaScript community has generated a lot of new ideas and a lot of real tool libraries.

In addition to the common Tags and Nano-HTML libraries mentioned briefly in the previous example, there are also many tag libraries that implement domain-specific functionality, such as SQL related, internationalization related, and localization related. An NPM search using tagged Template literals will find wheels made by others.

But more than any other topic, the most community-focused exploration has focused on combining template literals with HTML templates, Three representative frameworks aim to take the template Literals scheme and combine it with other Good Stuff to achieve fast rendering schemes comparable to Virtual DOM.

  • HyperHTML is intended to be a repository for Virtual DOM alternatives. It is explicitly stated in the official documentation. The core concept of hyperHTML is to use ES6 templates as Virtual DOM Alternative
  • lit-htmlThe Google Polymer team has developed a library similar in concept to hyperHTML, combining ES6 templates with HTML<template>Element to achieve fast rendering and updating of DOM elements
  • Choojs is a modern front-end development framework with only 4KB after Minify +gzip. Unlike hyperHTML and lit-HTML, choojs is a more full-featured framework with more stars than either currently (as of May 2018)

What all three frameworks have in common is the adoption of tagged Template literals and the abandonment of Virtual DOM, which is a very popular concept now, but they claim to be able to render and update the real DOM quickly. From the data provided by their own, it is also a good performance. We won’t discuss it here for lack of space.

These frameworks also reflect the potential of the ES6 template and leave it to us to explore further. The following articles are good references.

  • Template string – MDN document
  • What can you do with ES6 string template literals? – dherman/template-literals.md
  • Template strings: embedded DSLs in ECMAScript 6
  • ES6 Template Literals, the Handlebars killer?
  • Lit-html demo at Chrome Dev Summit 2017
  • Efficient, Expressivelit-HTML demo at Polymer Summit 2017
  • Compare hyperHTML with lit-HTML
  • Compare hyperHTML with Virtual DOM
  • Choo framework architecture and performance introduction

This post is published simultaneously on SegmentFault and Personal blog