Convertir una cadena en una cadena de plantilla


¿Es posible crear una cadena de plantilla como una cadena habitual

let a="b:${b}";

Y luego convertirlo en una cadena de plantilla

let b=10;
console.log(a.template());//b:10

Sin eval, new Function y otros medios de generación de código dinámico?

Author: Damjan Pavlica, 2015-03-21

17 answers

Como su cadena de plantilla debe obtener referencia a la variable b dynamicly (en tiempo de ejecución), la respuesta es: NO, es imposible prescindir de la generación de código dinámico.

Pero con eval es bastante simple:

let tpl = eval('`'+a+'`');
 47
Author: alexpods,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-03-21 12:36:46

Lo que estás pidiendo aquí:

//non working code quoted from the question
let b=10;
console.log(a.template());//b:10

Es exactamente equivalente (en términos de potencia y, er, seguridad) a eval: la capacidad de tomar una cadena que contiene código y ejecutar ese código; y también la capacidad para que el código ejecutado vea variables locales en el entorno de la persona que llama.

No hay forma en JS de que una función vea variables locales en su llamador, a menos que esa función sea eval(). Ni siquiera Function() puede hacerlo.


Cuando escuchas que hay algo llamado "template strings" llegando a JavaScript, es natural asumir que es una biblioteca de plantillas incorporada, como Mustache. Se trata principalmente de interpolación de cadenas y cadenas multilíneas para JS. Creo que esto va a ser un error común por un tiempo, sin embargo. :(

 28
Author: Jason Orendorff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-23 00:33:18

No, no hay una manera de hacer esto sin la generación de código dinámico.

Sin embargo, he creado una función que convertirá una cadena regular en una función que se puede proporcionar con un mapa de valores, utilizando cadenas de plantilla internamente.

Generar Cadena de plantilla Gist

/**
 * Produces a function which uses template strings to do simple interpolation from objects.
 * 
 * Usage:
 *    var makeMeKing = generateTemplateString('${name} is now the king of ${country}!');
 * 
 *    console.log(makeMeKing({ name: 'Bryan', country: 'Scotland'}));
 *    // Logs 'Bryan is now the king of Scotland!'
 */
var generateTemplateString = (function(){
    var cache = {};

    function generateTemplate(template){
        var fn = cache[template];

        if (!fn){
            // Replace ${expressions} (etc) with ${map.expressions}.

            var sanitized = template
                .replace(/\$\{([\s]*[^;\s\{]+[\s]*)\}/g, function(_, match){
                    return `\$\{map.${match.trim()}\}`;
                    })
                // Afterwards, replace anything that's not ${map.expressions}' (etc) with a blank string.
                .replace(/(\$\{(?!map\.)[^}]+\})/g, '');

            fn = Function('map', `return \`${sanitized}\``);
        }

        return fn;
    }

    return generateTemplate;
})();

Uso:

var kingMaker = generateTemplateString('${name} is king!');

console.log(kingMaker({name: 'Bryan'}));
// Logs 'Bryan is king!' to the console.

Espero que esto ayude a alguien. Si encuentra un problema con el código, por favor, sea tan amable de actualizar la esencia.

 23
Author: Bryan Rayner,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-17 18:10:37

En mi proyecto he creado algo como esto con ES6:

String.prototype.interpolate = function(params) {
  const names = Object.keys(params);
  const vals = Object.values(params);
  return new Function(...names, `return \`${this}\`;`)(...vals);
}

const template = 'Example text: ${text}';
const result = template.interpolate({
  text: 'Foo Boo'
});
console.log(result);

ACTUALIZACIÓN He eliminado la dependencia de lodash, ES6 tiene métodos equivalentes para obtener claves y valores.

 20
Author: Mateusz Moska,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-21 09:49:38

TLDR: https://jsfiddle.net/w3jx07vt /

Todo el mundo parece estar preocupado por acceder a las variables, ¿por qué no simplemente pasarlas? Estoy seguro de que no será demasiado difícil obtener el contexto variable en la persona que llama y pasarlo. Utilice este https://stackoverflow.com/a/6394168/6563504 para obtener los apoyos de obj. No puedo hacer la prueba por ti ahora, pero esto debería funcionar.

function renderString(str,obj){
    return str.replace(/\$\{(.+?)\}/g,(match,p1)=>{return index(obj,p1)})
}

Probado. Aquí está el código completo.

function index(obj,is,value) {
    if (typeof is == 'string')
        is=is.split('.');
    if (is.length==1 && value!==undefined)
        return obj[is[0]] = value;
    else if (is.length==0)
        return obj;
    else
        return index(obj[is[0]],is.slice(1), value);
}

function renderString(str,obj){
    return str.replace(/\$\{.+?\}/g,(match)=>{return index(obj,match)})
}

renderString('abc${a}asdas',{a:23,b:44}) //abc23asdas
renderString('abc${a.c}asdas',{a:{c:22,d:55},b:44}) //abc22asdas
 7
Author: M3D,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:47:23

El problema aquí es tener una función que tenga acceso a las variables de su llamador. Esta es la razón por la que vemos que direct eval se utiliza para el procesamiento de plantillas. Una posible solución sería generar una función tomando parámetros formales nombrados por las propiedades de un diccionario, y llamándola con los valores correspondientes en el mismo orden. Una forma alternativa sería tener algo simple como esto:

var name = "John Smith";
var message = "Hello, my name is ${name}";
console.log(new Function('return `' + message + '`;')());

Y para cualquiera que use el compilador de Babel necesitamos crear un cierre que recuerde entorno en el que fue creado:

console.log(new Function('name', 'return `' + message + '`;')(name));
 6
Author: didinko,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-28 12:18:09

Puede usar el prototipo de cadena, por ejemplo

String.prototype.toTemplate=function(){
    return eval('`'+this+'`');
}
//...
var a="b:${b}";
var b=10;
console.log(a.toTemplate());//b:10

Pero la respuesta de la pregunta original no es posible.

 4
Author: sarkiroka,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-10 07:53:40

Necesité este método con soporte para Internet Explorer. Resultó que las garrapatas posteriores no son compatibles con ni siquiera IE11. Además; usar eval o su equivalente Function no se siente bien.

Para el que se da cuenta; también uso backsticks, pero estos son eliminados por compiladores como babel. Los métodos sugeridos por otros, dependen de ellos en tiempo de ejecución. Como se dijo antes, este es un problema en IE11 y menor.

Así que esto es lo que se me ocurrió:

function get(path, obj, fb = `$\{${path}}`) {
  return path.split('.').reduce((res, key) => res[key] || fb, obj);
}

function parseTpl(template, map, fallback) {
  return template.replace(/\$\{.+?}/g, (match) => {
    const path = match.substr(2, match.length - 3).trim();
    return get(path, map, fallback);
  });
}

Ejemplo salida:

const data = { person: { name: 'John', age: 18 } };

parseTpl('Hi ${person.name} (${person.age})', data);
// output: Hi John (18)

parseTpl('Hello ${person.name} from ${person.city}', data);
// output: Hello John from ${person.city}

parseTpl('Hello ${person.name} from ${person.city}', data, '-');
// output: Hello John from -
 4
Author: s.meijer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-17 15:01:29

Actualmente no puedo comentar las respuestas existentes, por lo que no puedo comentar directamente la excelente respuesta de Bryan Raynor. Por lo tanto, esta respuesta va a actualizar su respuesta con una ligera corrección.

En resumen, su función no puede almacenar en caché la función creada, por lo que siempre se recreará, independientemente de si se ha visto la plantilla antes. Aquí está el código corregido:

    /**
     * Produces a function which uses template strings to do simple interpolation from objects.
     * 
     * Usage:
     *    var makeMeKing = generateTemplateString('${name} is now the king of ${country}!');
     * 
     *    console.log(makeMeKing({ name: 'Bryan', country: 'Scotland'}));
     *    // Logs 'Bryan is now the king of Scotland!'
     */
    var generateTemplateString = (function(){
        var cache = {};

        function generateTemplate(template){
            var fn = cache[template];

            if (!fn){
                // Replace ${expressions} (etc) with ${map.expressions}.

                var sanitized = template
                    .replace(/\$\{([\s]*[^;\s\{]+[\s]*)\}/g, function(_, match){
                        return `\$\{map.${match.trim()}\}`;
                    })
                    // Afterwards, replace anything that's not ${map.expressions}' (etc) with a blank string.
                    .replace(/(\$\{(?!map\.)[^}]+\})/g, '');

                fn = cache[template] = Function('map', `return \`${sanitized}\``);
            }

            return fn;
        };

        return generateTemplate;
    })();
 3
Author: user2501097,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-04 16:33:29

@Mateusz Moska, la solución funciona muy bien, pero cuando la usé en React Native(modo de compilación), arroja un error: Carácter no válido "', aunque funciona cuando la corro en modo de depuración.

Así que escribí mi propia solución usando regex.

String.prototype.interpolate = function(params) {
  let template = this
  for (let key in params) {
    template = template.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), params[key])
  }
  return template
}

const template = 'Example text: ${text}',
  result = template.interpolate({
    text: 'Foo Boo'
  })

console.log(result)

Demo: https://es6console.com/j31pqx1p/

NOTA: Dado que no conozco la causa raíz de un problema, levanté un ticket en react-native repo, https://github.com/facebook/react-native/issues/14107 , para que una vez que puedan capaz de fijar / guiarme sobre el mismo:)

 2
Author: Mohit Pandey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 15:31:20

Sigue siendo dinámico pero parece más controlado que solo usar una evaluación desnuda:

const vm = require('vm')
const moment = require('moment')


let template = '### ${context.hours_worked[0].value} \n Hours worked \n #### ${Math.abs(context.hours_worked_avg_diff[0].value)}% ${fns.gt0(context.hours_worked_avg_diff[0].value, "more", "less")} than usual on ${fns.getDOW(new Date())}'
let context = {
  hours_worked:[{value:10}],
  hours_worked_avg_diff:[{value:10}],

}


function getDOW(now) {
  return moment(now).locale('es').format('dddd')
}

function gt0(_in, tVal, fVal) {
  return _in >0 ? tVal: fVal
}



function templateIt(context, template) {
  const script = new vm.Script('`'+template+'`')
  return script.runInNewContext({context, fns:{getDOW, gt0 }})
}

console.log(templateIt(context, template))

Https://repl.it/IdVt/3

 2
Author: Robert Moskal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-22 16:30:19

Similar a la respuesta de Daniel (y s. meijer esencia ) pero más legible:

const regex = /\${[^{]+}/g;

export default function interpolate(template, variables, fallback) {
    return template.replace(regex, (match) => {
        const path = match.slice(2, -1).trim();
        return getObjPath(path, variables, fallback);
    });
}

//get the specified property or nested property of an object
function getObjPath(path, obj, fallback = '') {
    return path.split('.').reduce((res, key) => res[key] || fallback, obj);
}

Nota: Esto mejora ligeramente el original de s. meijer, ya que no coincidirá con cosas como ${foo{bar} (la expresión regular solo permite caracteres de corchete no rizados dentro de ${ y }).


ACTUALIZACIÓN: Me pidieron un ejemplo usando esto, así que aquí tienes:

const replacements = {
    name: 'Bob',
    age: 37
}

interpolate('My name is ${name}, and I am ${age}.', replacements)
 2
Author: Matt Browne,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-20 17:34:07

Me gustó la respuesta de s. meijer y escribí mi propia versión basada en la suya:

function parseTemplate(template, map, fallback) {
    return template.replace(/\$\{[^}]+\}/g, (match) => 
        match
            .slice(2, -1)
            .trim()
            .split(".")
            .reduce(
                (searchObject, key) => searchObject[key] || fallback || match,
                map
            )
    );
}
 1
Author: Daniel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-01 05:42:11

Esta solución funciona sin ES6:

function render(template, opts) {
  return new Function(
    'return new Function (' + Object.keys(opts).reduce((args, arg) => args += '\'' + arg + '\',', '') + '\'return `' + template.replace(/(^|[^\\])'/g, '$1\\\'') + '`;\'' +
    ').apply(null, ' + JSON.stringify(Object.keys(opts).reduce((vals, key) => vals.push(opts[key]) && vals, [])) + ');'
  )();
}

render("hello ${ name }", {name:'mo'}); // "hello mo"

Nota: el constructor Function siempre se crea en el ámbito global, lo que podría causar que las variables globales se sobrescriban por la plantilla, por ejemplo, render("hello ${ someGlobalVar = 'some new value' }", {name:'mo'});

 0
Author: cruzanmo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-06 22:41:09

Ya que estamos reinventando la rueda en algo que sería una característica encantadora en javascript.

Utilizo eval(), que no es seguro, pero javascript no es seguro. Admito fácilmente que no soy excelente con javascript, pero tenía una necesidad, y necesitaba una respuesta, así que hice una.

Elegí estilizar mis variables con un @ en lugar de un $, particularmente porque quiero usar la función multilínea de literales sin evaluar hasta que esté listo. Tan variable la sintaxis es @{OptionalObject.OptionalObjectN.VARIABLE_NAME}

No soy experto en javascript, así que con mucho gusto tomaría consejos sobre la mejora, pero...

var prsLiteral, prsRegex = /\@\{(.*?)(?!\@\{)\}/g
for(i = 0; i < myResultSet.length; i++) {
    prsLiteral = rt.replace(prsRegex,function (match,varname) {
        return eval(varname + "[" + i + "]");
        // you could instead use return eval(varname) if you're not looping.
    })
    console.log(prsLiteral);
}

Una implementación muy simple sigue

myResultSet = {totalrecords: 2,
Name: ["Bob", "Stephanie"],
Age: [37,22]};

rt = `My name is @{myResultSet.Name}, and I am @{myResultSet.Age}.`

var prsLiteral, prsRegex = /\@\{(.*?)(?!\@\{)\}/g
for(i = 0; i < myResultSet.totalrecords; i++) {
    prsLiteral = rt.replace(prsRegex,function (match,varname) {
        return eval(varname + "[" + i + "]");
        // you could instead use return eval(varname) if you're not looping.
    })
    console.log(prsLiteral);
}

En mi implementación real, elijo usar @{{variable}}. Un juego más de aparatos. Absurdamente improbable encontrar eso inesperadamente. La expresión regular para eso se vería como /\@\{\{(.*?)(?!\@\{\{)\}\}/g

Para que sea más fácil de leer

\@\{\{    # opening sequence, @{{ literally.
(.*?)     # capturing the variable name
          # ^ captures only until it reaches the closing sequence
(?!       # negative lookahead, making sure the following
          # ^ pattern is not found ahead of the current character
  \@\{\{  # same as opening sequence, if you change that, change this
)
\}\}      # closing sequence.

Si no tienes experiencia con regex, una regla bastante segura es escapar cada carácter no alfanumérico, y no nunca innecesariamente escapan letras ya que muchas letras escapadas tienen un significado especial para prácticamente todos los sabores de expresiones regulares.

 0
Author: Regular Joe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-17 05:33:14

Deberías probar este pequeño módulo JS, de Andrea Giammarchi, de github : https://github.com/WebReflection/backtick-template

/*! (C) 2017 Andrea Giammarchi - MIT Style License */
function template(fn, $str, $object) {'use strict';
  var
    stringify = JSON.stringify,
    hasTransformer = typeof fn === 'function',
    str = hasTransformer ? $str : fn,
    object = hasTransformer ? $object : $str,
    i = 0, length = str.length,
    strings = i < length ? [] : ['""'],
    values = hasTransformer ? [] : strings,
    open, close, counter
  ;
  while (i < length) {
    open = str.indexOf('${', i);
    if (-1 < open) {
      strings.push(stringify(str.slice(i, open)));
      open += 2;
      close = open;
      counter = 1;
      while (close < length) {
        switch (str.charAt(close++)) {
          case '}': counter -= 1; break;
          case '{': counter += 1; break;
        }
        if (counter < 1) {
          values.push('(' + str.slice(open, close - 1) + ')');
          break;
        }
      }
      i = close;
    } else {
      strings.push(stringify(str.slice(i)));
      i = length;
    }
  }
  if (hasTransformer) {
    str = 'function' + (Math.random() * 1e5 | 0);
    if (strings.length === values.length) strings.push('""');
    strings = [
      str,
      'with(this)return ' + str + '([' + strings + ']' + (
        values.length ? (',' + values.join(',')) : ''
      ) + ')'
    ];
  } else {
    strings = ['with(this)return ' + strings.join('+')];
  }
  return Function.apply(null, strings).apply(
    object,
    hasTransformer ? [fn] : []
  );
}

template.asMethod = function (fn, object) {'use strict';
  return typeof fn === 'function' ?
    template(fn, this, object) :
    template(this, fn);
};

Demo (todas las siguientes pruebas devuelven true):

const info = 'template';
// just string
`some ${info}` === template('some ${info}', {info});

// passing through a transformer
transform `some ${info}` === template(transform, 'some ${info}', {info});

// using it as String method
String.prototype.template = template.asMethod;

`some ${info}` === 'some ${info}'.template({info});

transform `some ${info}` === 'some ${info}'.template(transform, {info});
 0
Author: colxi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-12 01:16:24

A veces lo más fácil funciona mejor. Simplemente envuelva su plantilla en una función que devuelva la evaluación. Todavía obtienes todos los beneficios de los literales de plantilla sin la expresión regular y funciones adicionales:

const formatter = (options) => `${options.title}: ${options.subtitle}`
formatter({title: 'Hey There', subtitle: 'Return of the Sequel'});
// Produces 'Hey There: Return of the Sequel'

Todas las advertencias habituales sobre los literales de plantilla todavía se aplican: https://caniuse.com/#feat=template-literals

 0
Author: radicand,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-14 17:17:38